Methods and interfaces
Go does not have classes; it has types with methods and interfaces. Methods are functions with a receiver; the receiver is the type the method is attached to. Interfaces specify a set of method signatures; a type implements an interface implicitly by having the required methods (no implements keyword). The mechanism is structural — any type with the right method set satisfies an interface, regardless of declaration. The combination — methods on any type, structural interfaces, the empty interface for runtime polymorphism, embedding for composition — is the substance of Go’s polymorphism story.
Methods
A method is a function with a receiver:
type Counter struct {
n int
}
func (c *Counter) Increment() {
c.n++
}
func (c Counter) Get() int {
return c.n
}
c := Counter{}
c.Increment() // *Counter method called on Counter value
c.Increment()
fmt.Println(c.Get()) // 2
The receiver appears between func and the method name. The two forms:
- Value receiver
(c Counter)— receives a copy of the value. - Pointer receiver
(c *Counter)— receives a pointer; admits modification.
Methods may be defined on any type defined in the same package — including non-struct types:
type Celsius float64
func (c Celsius) Fahrenheit() float64 {
return float64(c)*9/5 + 32
}
t := Celsius(20)
fmt.Println(t.Fahrenheit()) // 68
A method cannot be defined on a type from another package; the conventional defence is to declare a new type:
// type StringSlice []string // OK; new type
// type Person struct{ ... } // OK
Value vs pointer receivers
The conventional discipline:
- Use a pointer receiver if the method modifies the receiver.
- Use a pointer receiver for large structs (avoid the copy).
- Use a value receiver for small, immutable values.
- Be consistent within a type — all methods on one type conventionally use the same form.
type Account struct {
Balance int
}
// Pointer receiver — modifies the account:
func (a *Account) Deposit(amount int) {
a.Balance += amount
}
// Pointer receiver for consistency:
func (a *Account) Get() int {
return a.Balance
}
The method set
A type’s method set is the set of methods callable on values of that type. The rules:
- For a value type
T: the method set contains all methods with receiverT. - For a pointer type
*T: the method set contains all methods with receiverTor*T.
type Counter struct{ n int }
func (c Counter) Get() int { return c.n }
func (c *Counter) Increment() { c.n++ }
var c Counter
c.Get() // value receiver — OK
c.Increment() // pointer receiver — Go automatically takes &c
var p *Counter = &c
p.Get() // OK; *Counter has Get
p.Increment() // OK
The compiler automatically takes the address (&c) for pointer-receiver method calls on addressable values. For non-addressable values (e.g., map elements), the call fails:
m := map[string]Counter{}
// m["key"].Increment() // ERROR: cannot call pointer method
The conventional defence is to use *Counter in the map:
m := map[string]*Counter{
"key": &Counter{},
}
m["key"].Increment() // OK
Interfaces
An interface specifies a set of method signatures:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
A type implements an interface implicitly by having the required methods:
type File struct{ /* ... */ }
func (f *File) Read(p []byte) (int, error) { /* ... */ }
func (f *File) Write(p []byte) (int, error) { /* ... */ }
func (f *File) Close() error { /* ... */ }
var r Reader = &File{} // *File implements Reader
var w Writer = &File{} // *File implements Writer
There is no implements keyword; the language infers satisfaction from the method set. The mechanism is one of Go’s most distinctive features.
Interface composition
Interfaces may embed other interfaces:
type ReadCloser interface {
Reader
Closer
}
type ReadWriteCloser interface {
Reader
Writer
Closer
}
The composed interface requires all the embedded methods. The mechanism admits substantial reuse and modular interface design.
The empty interface
The interface interface{} (or any since Go 1.18) requires no methods — every type satisfies it:
var x interface{} = 42
var y interface{} = "hello"
var z interface{} = []int{1, 2, 3}
var a any = 3.14 // any is an alias for interface{}
The empty interface admits “any value” — useful for generic containers, JSON values, and APIs that handle heterogeneous data.
To extract the concrete type from an empty interface, use a type assertion or type switch:
var i interface{} = "hello"
s := i.(string) // type assertion
fmt.Println(s) // "hello"
s, ok := i.(string) // two-value form
if ok {
fmt.Println(s)
}
switch v := i.(type) { // type switch
case string:
fmt.Println("string:", v)
case int:
fmt.Println("int:", v)
}
Treated in Switch and pattern dispatch.
Type assertions
A type assertion extracts a concrete type from an interface:
var w io.Writer = os.Stdout
f, ok := w.(*os.File) // assert to *os.File
if ok {
fmt.Println("got file:", f.Name())
}
// Single-value form panics on failure:
f := w.(*os.File) // panics if w is not *os.File
The two-value form is the conventional defence; the single-value form is for cases where failure indicates a programmer error.
The Stringer interface
The fmt.Stringer interface admits custom string representation:
type Stringer interface {
String() string
}
type Point struct{ X, Y int }
func (p Point) String() string {
return fmt.Sprintf("(%d, %d)", p.X, p.Y)
}
p := Point{1, 2}
fmt.Println(p) // "(1, 2)" — uses String()
Implementing Stringer admits substantial integration with the fmt package; types that implement it produce custom output for %v and similar verbs.
The error interface
The built-in error is an interface:
type error interface {
Error() string
}
Any type with an Error() string method satisfies error:
type MyError struct {
Code int
}
func (e *MyError) Error() string {
return fmt.Sprintf("error code %d", e.Code)
}
var err error = &MyError{Code: 42}
fmt.Println(err) // "error code 42"
Treated in Error handling.
Embedding
Embedding admits composition — including a type’s fields and methods in another struct:
type Animal struct {
Name string
}
func (a Animal) Describe() string {
return fmt.Sprintf("Animal named %s", a.Name)
}
type Dog struct {
Animal // embedded; promotes Name and Describe
Breed string
}
d := Dog{
Animal: Animal{Name: "Rex"},
Breed: "Labrador",
}
fmt.Println(d.Name) // "Rex" (promoted)
fmt.Println(d.Describe()) // "Animal named Rex" (promoted method)
fmt.Println(d.Animal.Name) // also accessible
The embedded type is accessed by its type name (d.Animal); promoted fields and methods admit “as-if-inheritance” syntax.
Embedding is not inheritance — there is no virtual dispatch, no overriding (one-can shadow), and the embedded type is just a field with its name promoted. The mechanism admits substantial reuse without the complexity of class hierarchies.
Embedding interfaces
A struct can embed an interface:
type Server struct {
io.ReadWriter // any type satisfying ReadWriter
Name string
}
s := Server{
ReadWriter: &bytes.Buffer{},
Name: "myserver",
}
s.Read(buf) // calls Buffer's Read
The pattern admits delegation — the struct delegates method calls to the embedded value.
Common patterns
Standard library interfaces
The conventional standard-library interfaces:
io.Reader // Read
io.Writer // Write
io.Closer // Close
io.ReadWriter // Read + Write
io.ReadWriteCloser // Read + Write + Close
fmt.Stringer // String() string
sort.Interface // Len, Less, Swap
error // Error() string
The interfaces compose: a type with Read, Write, and Close methods is automatically an io.ReadWriteCloser.
Implementing Stringer
type Status int
const (
StatusActive Status = iota
StatusInactive
StatusBanned
)
func (s Status) String() string {
switch s {
case StatusActive:
return "Active"
case StatusInactive:
return "Inactive"
case StatusBanned:
return "Banned"
default:
return fmt.Sprintf("Status(%d)", int(s))
}
}
fmt.Println(StatusActive) // "Active"
Polymorphic dispatch
type Shape interface {
Area() float64
}
type Circle struct{ R float64 }
type Square struct{ Side float64 }
func (c Circle) Area() float64 { return math.Pi * c.R * c.R }
func (s Square) Area() float64 { return s.Side * s.Side }
shapes := []Shape{
Circle{R: 5},
Square{Side: 4},
}
for _, s := range shapes {
fmt.Println(s.Area())
}
The pattern is conventional for heterogeneous collections.
Mock implementations for testing
type Database interface {
Get(key string) (string, error)
Set(key, value string) error
}
type MockDB struct {
data map[string]string
}
func (m *MockDB) Get(key string) (string, error) {
v, ok := m.data[key]
if !ok { return "", fmt.Errorf("not found") }
return v, nil
}
func (m *MockDB) Set(key, value string) error {
m.data[key] = value
return nil
}
// In tests:
db := &MockDB{data: map[string]string{"a": "1"}}
service := NewService(db)
The pattern is conventional for testing — interfaces admit substituting a mock for the real implementation.
Embedding for default methods
type defaultLogger struct{}
func (defaultLogger) Debug(s string) { /* default no-op */ }
func (defaultLogger) Info(s string) { fmt.Println(s) }
func (defaultLogger) Error(s string) { fmt.Fprintln(os.Stderr, s) }
type ServiceA struct {
defaultLogger // gets all the default methods
}
// Override only what's needed:
func (s *ServiceA) Info(message string) {
fmt.Printf("[ServiceA] %s\n", message)
}
The pattern admits “default implementations” through embedding.
Type assertion for optional interface
type Reader interface {
Read(p []byte) (int, error)
}
type Closer interface {
Close() error
}
func processAndClose(r Reader) {
/* ... use r.Read ... */
if c, ok := r.(Closer); ok {
c.Close()
}
}
The pattern admits checking for an optional capability.
Interface as parameter type
func writeAll(w io.Writer, lines []string) error {
for _, line := range lines {
if _, err := fmt.Fprintln(w, line); err != nil {
return err
}
}
return nil
}
writeAll(os.Stdout, lines)
writeAll(file, lines)
writeAll(&bytes.Buffer{}, lines)
The conventional Go discipline is to take interface parameters and return concrete types — admit any caller, return a specific implementation.
Adapter pattern
type HandlerFunc func(http.ResponseWriter, *http.Request)
func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
f(w, r)
}
// Now any function with the right signature can be an http.Handler:
http.Handle("/", HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
/* ... */
}))
The pattern is conventional in net/http and similar APIs.
A note on the absence of inheritance
Go does not have class inheritance. The conventional substitutes:
- Embedding for code reuse and method promotion.
- Interfaces for polymorphism and abstraction.
- Composition — including types as fields.
// Java/C#:
// class Dog extends Animal { ... }
// Go:
type Dog struct {
Animal // composition + promotion
Breed string
}
The mechanism admits substantial reuse without the complexities of virtual dispatch, abstract classes, and the diamond-inheritance problem.
A note on the conventional discipline
The contemporary Go interface advice:
- Define interfaces where they are used — at the consumer side, not the producer side.
- Keep interfaces small — the convention is “single-method interfaces” (
io.Reader,io.Writer). - Compose interfaces via embedding rather than producing larger interfaces.
- Accept interfaces, return concrete types — the conventional Go API design.
- Be consistent with receivers — pointer or value, not both.
- Use type assertions or type switches for optional capabilities.
- Use embedding for shared behaviour — composition, not inheritance.
- Implement
Stringeron types meant for human display. - Implement
erroron types meant to be used as errors.
The combination — methods on any type, structural interfaces, embedding for composition, the empty interface for any value, type assertions and switches for runtime dispatch — is the substance of Go’s polymorphism. The discipline produces flexible, decoupled code without inheritance hierarchies.