Methods
Method declaration, pointer vs value receivers, method sets, and implicit interface satisfaction.
A method is a function with a receiver - a named type that the function is bound to. Any named type (not just structs) can have methods. Methods are how Go expresses behaviour.
Declare a method by placing the receiver before the function name. A value receiver operates on a copy - mutations do not affect the original. A pointer receiver operates on the original - mutations persist.
package main
import "fmt"
type Rectangle struct {
Width, Height float64
}
// Value receiver - read-only, works on a copy
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
// Value receiver - read-only
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}
// Pointer receiver - mutates the original
func (r *Rectangle) Scale(factor float64) {
r.Width *= factor
r.Height *= factor
}
func main() {
rect := Rectangle{Width: 10, Height: 5}
fmt.Println(rect.Area()) // 50
fmt.Println(rect.Perimeter()) // 30
rect.Scale(2)
fmt.Println(rect.Area()) // 200 - Width and Height were mutated
}Go interfaces are satisfied implicitly - there is no implements keyword. If a type has all the methods an interface requires, it satisfies the interface automatically. This allows adapters to be written without modifying the original type.
package main
import (
"fmt"
"math"
)
type Shape interface {
Area() float64
}
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
type Rectangle struct {
Width, Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func printArea(s Shape) {
fmt.Printf("%.2f\n", s.Area())
}
func main() {
// Both satisfy Shape without any explicit declaration
printArea(Circle{Radius: 5}) // 78.54
printArea(Rectangle{Width: 4, Height: 6}) // 24.00
}Because interface satisfaction is implicit, a typo in a method signature silently drops the satisfaction - no compile error. Add a compile-time assertion to catch this immediately.
package main
type Writer interface {
Write(p []byte) (n int, err error)
}
type MyWriter struct{}
func (w *MyWriter) Write(p []byte) (int, error) {
return len(p), nil
}
// Compile-time check: if *MyWriter stops satisfying Writer,
// this line produces a compile error immediately.
var _ Writer = (*MyWriter)(nil)
func main() {
var w Writer = &MyWriter{}
n, _ := w.Write([]byte("hello"))
_ = n
}In production
Go interfaces are satisfied implicitly - this is powerful (write an adapter for any third-party type without modifying it) and risky (a one-character typo in a method name silently drops interface satisfaction). The var _ SomeInterface = (*MyType)(nil) pattern adds a zero-cost compile-time assertion that produces a clear error the moment the interface is broken. Add one of these assertions in the file where the type is declared - not in a test file - so it is always compiled, not just when tests run. This pattern is especially important for interfaces that are checked at runtime (HTTP handlers, database drivers, event consumers) where a missed satisfaction surfaces as a nil-dereference panic at request time rather than a compile error.
Enjoyed this? Get more essays on software craft delivered to your inbox.
Subscribe free