Go by Example

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