Methods and Interfaces in Go
Attaching behavior to types with methods, and Go's structurally-typed, implicitly-satisfied interfaces.
3 min read
With no classes, Go attaches behavior to types through methods — functions with a special receiver argument — and expresses shared behavior across unrelated types through interfaces, which work differently from interfaces in most other languages.
Methods: functions with a receiver
type Rectangle struct {
Width, Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
rect := Rectangle{Width: 10, Height: 5}
fmt.Println(rect.Area()) // 50(r Rectangle) is the receiver — it's what makes Area a method on Rectangle rather than a standalone function. Inside the method, r refers to the specific Rectangle the method was called on, similar to this or self in other languages, but declared explicitly rather than implicitly available.
Value receivers vs pointer receivers
func (r Rectangle) Scale(factor float64) {
r.Width *= factor // modifies the copy, not the original
}
func (r *Rectangle) ScaleInPlace(factor float64) {
r.Width *= factor // modifies the actual struct
r.Height *= factor
}A value receiver (r Rectangle) gets a copy — mutations inside the method vanish once it returns. A pointer receiver (r *Rectangle) operates on the original. The rule of thumb: use a pointer receiver whenever the method needs to mutate the receiver, and consider using pointer receivers consistently across all of a type's methods once at least one of them needs to — mixing the two on the same type is a common source of confusion. Go automatically takes the address for you when calling a pointer-receiver method on an addressable value, so rect.ScaleInPlace(2) works even without writing (&rect).ScaleInPlace(2).
Interfaces: satisfied implicitly
This is the part that surprises people most. In Go, a type doesn't declare which interfaces it implements — it just implements the required methods, and the compiler figures out the rest:
type Shape interface {
Area() float64
}
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}Circle never mentions Shape anywhere, but because it has an Area() float64 method, it automatically satisfies the Shape interface. Rectangle from above does too. Any code that accepts a Shape will happily take either:
func printArea(s Shape) {
fmt.Printf("area: %.2f\n", s.Area())
}
printArea(rect) // works
printArea(Circle{Radius: 3}) // also worksThis is called structural typing — "if it has the right shape, it satisfies the interface" — and it means interfaces can be defined after the fact, even by a completely different package than the one that wrote the concrete type. You can write an interface today describing behavior a struct from a library published years ago already happens to satisfy.
Small interfaces are idiomatic
Go's standard library leans heavily on tiny, single-method interfaces:
type Writer interface {
Write(p []byte) (n int, err error)
}
type Stringer interface {
String() string
}io.Writer (the real standard-library type this mirrors) is implemented by files, network connections, in-memory buffers, and dozens of other types — none of which know or care that io.Writer exists. The Go proverb "the bigger the interface, the weaker the abstraction" captures the idiom well: prefer several small, focused interfaces over one large one that's harder for a type to fully satisfy.
The empty interface and any
func describe(i interface{}) { /* accepts literally anything */ }
func describe2(i any) { /* identical, "any" is an alias added in Go 1.18 */ }An interface with zero required methods is satisfied by every type, which makes it Go's escape hatch for "I don't know or care about the type yet" — paired, usually, with the type switch from the control-flow lesson to figure out what was actually passed in.
Methods plus implicitly-satisfied interfaces are how Go achieves polymorphism without inheritance — a different shape than most object-oriented languages, but one that keeps types decoupled from the interfaces they end up satisfying.
Test what you just learned
4 quick questions. Get all of them right to unlock the next lesson.
You can take the quiz without an account — logging in just lets your result count toward your progress.