thecodex.expert · The Codex Family of Knowledge
Go

Interfaces and Types

No classes, no inheritance — Go achieves polymorphism through implicit interfaces and composition, keeping the type system honest and dependencies minimal.

Go 1.26 Structural typing Last verified:
Canonical Definition

Go interfaces are satisfied implicitly — a type implements an interface by having all the required methods, with no implements declaration needed; this structural typing allows retroactive interface satisfaction and decouples packages. Go has no class hierarchy, instead using structs with methods and struct embedding for composition over inheritance.

The central idea

Java and C++ make you declare class Dog implements Animal. Go does not. If your type has the right methods, it automatically satisfies any interface that requires those methods — even interfaces written after your type. This sounds like a minor syntax difference, but it fundamentally changes how packages relate to each other.

Structs and methods

Go has no class keyword. You define data in a struct and attach behaviour by writing functions with a receiver. The receiver is the type the function belongs to — analogous to this or self in other languages.

Gostructs.go
package main

import (
    "fmt"
    "math"
)

// Struct: named collection of fields
type Circle struct {
    Radius float64
}

type Rectangle struct {
    Width, Height float64
}

// Method: function with a receiver
// (c Circle) is the receiver — c is like "this" in Java
func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

func (c Circle) Perimeter() float64 {
    return 2 * math.Pi * c.Radius
}

// Pointer receiver: use *T when the method modifies the struct
// or when the struct is large (avoids copying)
func (c *Circle) Scale(factor float64) {
    c.Radius *= factor   // modifies the original, not a copy
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

func main() {
    c := Circle{Radius: 5}
    fmt.Printf("Circle area: %.2f\n", c.Area())
    c.Scale(2)
    fmt.Printf("After scale: radius=%.1f, area=%.2f\n", c.Radius, c.Area())

    r := Rectangle{Width: 4, Height: 6}
    fmt.Printf("Rectangle area: %.2f\n", r.Area())
}

Interfaces — implicit satisfaction

An interface defines a set of method signatures. Any type that has all those methods automatically satisfies the interface — no declaration, no registration. This is Go's structural typing (also called "duck typing" at the type level).

Gointerfaces.go
package main

import (
    "fmt"
    "math"
)

// Interface: defines required methods
// Any type with Area() and Perimeter() methods satisfies Shape
type Shape interface {
    Area() float64
    Perimeter() float64
}

type Circle struct { Radius float64 }
type Rectangle struct { Width, Height float64 }

func (c Circle) Area() float64      { return math.Pi * c.Radius * c.Radius }
func (c Circle) Perimeter() float64 { return 2 * math.Pi * c.Radius }
func (r Rectangle) Area() float64      { return r.Width * r.Height }
func (r Rectangle) Perimeter() float64 { return 2 * (r.Width + r.Height) }

// Function accepts any Shape — works for Circle, Rectangle, or any future type
func printInfo(s Shape) {
    fmt.Printf("Area: %.2f, Perimeter: %.2f\n", s.Area(), s.Perimeter())
}

func main() {
    shapes := []Shape{
        Circle{Radius: 3},
        Rectangle{Width: 4, Height: 5},
    }
    for _, s := range shapes {
        printInfo(s)
    }
    // No "implements Shape" anywhere — the compiler checks method sets
}

Struct embedding — composition over inheritance

Go has no inheritance. Instead, a struct can embed another struct — its fields and methods are promoted to the outer struct. This is composition, not inheritance: the embedded type's methods are accessible directly, but there is no "is-a" relationship. Embedding an interface in a struct (or interface) is also valid and widely used.

Goembedding.go
package main

import "fmt"

type Animal struct {
    Name string
}

func (a Animal) Breathe() {
    fmt.Printf("%s breathes air.\n", a.Name)
}

// Dog embeds Animal — gets its fields and methods promoted
type Dog struct {
    Animal          // embedded (no field name — just the type)
    Breed string
}

func (d Dog) Speak() {
    fmt.Printf("%s says: Woof!\n", d.Name)  // Name promoted from Animal
}

func main() {
    d := Dog{
        Animal: Animal{Name: "Rex"},
        Breed:  "Labrador",
    }

    d.Breathe()         // promoted from Animal — works directly
    d.Speak()
    fmt.Println(d.Name) // promoted field

    // Embedding an interface: useful for mocking and composition
    // type ReadWriter interface {
    //     io.Reader   // embeds Reader (requires Read method)
    //     io.Writer   // embeds Writer (requires Write method)
    // }
}

Type assertions and type switches

When you have a value of an interface type, you can recover the underlying concrete type with a type assertion: v, ok := i.(T). If ok is false, the concrete type is not T. A type switch tests multiple types at once — the idiomatic Go way to branch on dynamic type.

Gotypeswitch.go
package main

import "fmt"

func describe(i any) {   // any = interface{} — accepts any value
    switch v := i.(type) {
    case int:
        fmt.Printf("int: %d\n", v)
    case string:
        fmt.Printf("string: %q (len %d)\n", v, len(v))
    case bool:
        fmt.Printf("bool: %v\n", v)
    case []int:
        fmt.Printf("[]int of length %d\n", len(v))
    default:
        fmt.Printf("unknown type: %T\n", v)
    }
}

func main() {
    describe(42)
    describe("hello")
    describe(true)
    describe([]int{1, 2, 3})
    describe(3.14)

    // Single type assertion
    var i any = "hello"
    s, ok := i.(string)
    if ok {
        fmt.Println("it's a string:", s)
    }
    // i.(string) without ok panics if the assertion fails
}

Generics (Go 1.18+)

Go added generics in version 1.18 (March 2022). Type parameters use square brackets. Type constraints are interfaces — the built-in any constraint accepts all types; comparable accepts types that support ==. The constraints package (now in golang.org/x/exp) provides Ordered, Integer, and other useful constraints.

Gogenerics.go
package main

import "fmt"

// Map applies f to every element of s and returns the results
// T and U are type parameters — constrained to "any"
func Map[T, U any](s []T, f func(T) U) []U {
    result := make([]U, len(s))
    for i, v := range s {
        result[i] = f(v)
    }
    return result
}

// Filter returns elements of s where f returns true
func Filter[T any](s []T, f func(T) bool) []T {
    var result []T
    for _, v := range s {
        if f(v) {
            result = append(result, v)
        }
    }
    return result
}

// Number constraint: any type that supports arithmetic
type Number interface {
    ~int | ~int32 | ~int64 | ~float32 | ~float64
}

// Sum works for any numeric type
func Sum[T Number](nums []T) T {
    var total T
    for _, n := range nums {
        total += n
    }
    return total
}

func main() {
    nums := []int{1, 2, 3, 4, 5}
    doubled := Map(nums, func(n int) int { return n * 2 })
    fmt.Println(doubled)  // [2 4 6 8 10]

    evens := Filter(nums, func(n int) bool { return n%2 == 0 })
    fmt.Println(evens)    // [2 4]

    fmt.Println(Sum(nums))              // 15
    fmt.Println(Sum([]float64{1.1, 2.2, 3.3}))  // 6.6
}
Commonly confused
Go interfaces are implicit — no "implements" keyword. A type satisfies an interface by having the right methods. This decouples the type from the interface: you can define an interface in package B that types in package A satisfy, without changing package A at all. This is how the standard library's io.Reader and io.Writer work — any type with a Read([]byte) (int, error) method is automatically an io.Reader.
Value receivers vs pointer receivers matter for interface satisfaction. If a method is defined on *T (pointer receiver), only *T satisfies interfaces requiring that method — not T. If a method is on T (value receiver), both T and *T satisfy the interface. Use pointer receivers when the method must modify the struct or the struct is large.
Struct embedding is not inheritance. Embedding promotes the embedded type's methods to the outer type, but there is no "is-a" relationship. The outer type cannot be passed where the embedded type is expected unless it explicitly satisfies the same interface. This means Go avoids the fragile base class problem inherent to class hierarchies.

Interface representation: iface and eface

At runtime, a Go interface value is a two-word structure. For a non-empty interface (interface{ Method() }), it is an iface: a pointer to the itab (interface table — containing the type descriptor and a method dispatch table) and a pointer to the concrete value. For the empty interface (any / interface{}), it is an eface: a pointer to the type descriptor and a pointer to the value. Assigning a concrete value to an interface variable may allocate on the heap (if the value is too large to fit in a pointer) — this is a common source of allocation in tight loops. Comparing two interface values compares both the type and the value; comparing an interface holding a nil pointer to nil returns false — the interface is non-nil because it has a type pointer, even if the value pointer is nil.

Generics: reification vs erasure

Go's generics use a form of dictionary passing with GCShape stenciling (the GC Shapes proposal, implemented in Go 1.18). Types with the same underlying GC shape (same pointer layout) share a single monomorphised implementation; a dictionary is passed at runtime to distinguish the actual types. This is a middle ground between Java's type erasure (one implementation, boxing overhead) and C++'s full monomorphisation (one implementation per type instantiation, code size cost). The result: Go generics have low code-size overhead but add a small runtime dispatch cost for generic functions. For performance-critical code, benchmarking is advisable.

The io package: interface-based composition

Go's io package defines a small set of single-method interfaces — io.Reader, io.Writer, io.Closer, io.ReadWriter — that form the backbone of the entire standard library's I/O model. Files, network connections, HTTP request bodies, compression streams, buffers, and cryptographic hashers all satisfy io.Reader and/or io.Writer. This is the payoff of implicit interfaces: code written against io.Reader works with any source — a file, a socket, an in-memory buffer — without knowing which. It is the canonical example of Go's interface design principle: accept interfaces, return structs.

Specification reference

The Go Team. The Go Programming Language Specification. go.dev/ref/spec. Sections: "Interface types", "Method sets", "Type parameters". Donovan, A. A. A. & Kernighan, B. W. (2015). The Go Programming Language. Addison-Wesley. Chapter 7 (Interfaces). Go Blog: go.dev/blog/laws-of-reflection — interface internals. Griesemer, R. et al. (2021). Type Parameters Proposal. go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md.

Sources

1
The Go Team. The Go Programming Language Specification. go.dev/ref/spec. — Interface types, method sets, type parameters.
2
Donovan, A. A. A. & Kernighan, B. W. (2015). The Go Programming Language. Addison-Wesley. — Chapter 7 on interfaces is definitive.
3
Pike, R. (2011). The Laws of Reflection. go.dev/blog/laws-of-reflection. — Explains the two-word interface representation.
4
Griesemer, R. et al. (2021). Type Parameters Proposal. go.googlesource.com. — Design document for Go generics.
5
Go standard library. io package. pkg.go.dev/io. — Canonical use of interface composition.
Source confidence: High Last verified: Primary source: The Go Language Specification — go.dev/ref/spec