Go by Example

Interfaces

Go interfaces are satisfied implicitly - any type that implements the required methods satisfies the interface, no declaration needed.

Go interfaces define a set of method signatures. Any type that implements those methods satisfies the interface automatically - there is no implements keyword. This implicit satisfaction is one of Go's most powerful design decisions.

Define an interface with one or more method signatures. Any type with those methods satisfies it - even types defined in other packages that were never written with your interface in mind.

package main
 
import (
    "fmt"
    "math"
)
 
type Shape interface {
    Area() float64
    Perimeter() float64
}
 
type Circle struct {
    Radius float64
}
 
func (c Circle) Area() float64      { return math.Pi * c.Radius * c.Radius }
func (c Circle) Perimeter() float64 { return 2 * math.Pi * c.Radius }
 
type Rectangle struct {
    Width, Height float64
}
 
func (r Rectangle) Area() float64      { return r.Width * r.Height }
func (r Rectangle) Perimeter() float64 { return 2 * (r.Width + r.Height) }
 
func printShape(s Shape) {
    fmt.Printf("area=%.2f perimeter=%.2f\n", s.Area(), s.Perimeter())
}
 
func main() {
    printShape(Circle{Radius: 5})
    printShape(Rectangle{Width: 3, Height: 4})
}

The empty interface any (equivalent to interface{}) is satisfied by every type. Use it when a function must accept values of unknown type, but prefer concrete types or constrained generics when possible.

package main
 
import "fmt"
 
func describe(v any) {
    fmt.Printf("value=%v type=%T\n", v, v)
}
 
func main() {
    describe(42)
    describe("hello")
    describe(true)
    describe([]int{1, 2, 3})
}

A type assertion extracts the concrete value from an interface variable. Use the two-value form v, ok := i.(T) to avoid a panic when the assertion may fail.

package main
 
import "fmt"
 
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) }
 
func tryString(v any) {
    if s, ok := v.(Stringer); ok {
        fmt.Println("Stringer:", s.String())
    } else {
        fmt.Printf("not a Stringer: %T\n", v)
    }
}
 
func main() {
    tryString(Point{1, 2})
    tryString(42)
}

Compose interfaces from smaller ones. io.ReadWriter in the standard library is exactly this pattern - it combines io.Reader and io.Writer into a single interface.

package main
 
import "fmt"
 
type Reader interface {
    Read() string
}
 
type Writer interface {
    Write(s string)
}
 
type ReadWriter interface {
    Reader
    Writer
}
 
type Buffer struct{ data string }
 
func (b *Buffer) Read() string      { return b.data }
func (b *Buffer) Write(s string)    { b.data = s }
 
func process(rw ReadWriter) {
    rw.Write("hello")
    fmt.Println(rw.Read())
}
 
func main() {
    process(&Buffer{})
}

In production

Keep interfaces small - ideally one or two methods - and define them at the point of use, not in the package that implements them. A large interface in a shared package becomes a coupling magnet: every implementer must satisfy every method even when it only needs one. The io.Reader and io.Writer pattern is the north star: one method, infinite composability. When you need a compile-time check that a type satisfies an interface, add var _ MyInterface = (*MyType)(nil) near the type definition - it costs nothing at runtime and catches method signature drift immediately.

Enjoyed this? Get more essays on software craft delivered to your inbox.

Subscribe free