Interfaces, Composition & Generics

Difficulty

Structural typing is one of the most distinctive differences between Go and mainstream OOP languages.

No implements keyword needed

type Writer interface {
    Write(p []byte) (n int, err error)
}

type ConsoleLogger struct{}

func (c ConsoleLogger) Write(p []byte) (int, error) {
    fmt.Print(string(p))
    return len(p), nil
}

var w Writer = ConsoleLogger{}  // satisfies Writer automatically — no declaration needed

ConsoleLogger never mentions Writer anywhere. It satisfies the interface purely because it has a matching Write method.

Defining an interface after the fact

// Some third-party package already defines this, with no idea your interface exists:
type ThirdPartyThing struct{}
func (t ThirdPartyThing) Write(p []byte) (int, error) { return len(p), nil }

// You can define your own interface and use it immediately, with no changes to ThirdPartyThing:
type MyWriter interface {
    Write(p []byte) (n int, err error)
}
var w MyWriter = ThirdPartyThing{}  // works, no modification needed

Java/C# comparison

// Java requires explicit declaration
class ConsoleLogger implements Writer {
    public int write(byte[] p) { ... }
}

In Java or C#, ConsoleLogger must declare implements Writer at its definition site. If you want a new interface applied to an existing class you don't control, you're stuck wrapping it, since you can't retroactively add implements to someone else's class.

Why this matters practically

Structural typing lets you define small, narrow interfaces exactly where you need them, like in a test file, without touching the type being tested at all. This is a big part of why Go encourages small, single-method interfaces (io.Reader, io.Writer) defined by the consumer of an interface, not the type that happens to implement it.

Related Resources

any is powerful precisely because it accepts anything, which is also exactly why it should be reached for carefully.

Where it's genuinely useful

func Println(a ...any) (n int, err error)   // fmt's real signature, roughly

data := map[string]any{
    "name": "Alice",
    "age":  30,
}  // a natural fit for arbitrary JSON-shaped data

The cost: you lose type safety

func process(v any) {
    // v could be anything — you must assert or switch to use it meaningfully
    switch x := v.(type) {
    case int:
        fmt.Println("int:", x*2)
    case string:
        fmt.Println("string:", x+"!")
    default:
        fmt.Println("unknown type")
    }
}

Nothing stops a caller from passing a completely unexpected type, and the compiler can't catch a mismatched type assertion — it only fails at runtime, often with a panic if you skip the safe two-value form.

Before generics: any as a workaround

// Pre-1.18: writing a generic-feeling container required any, and manual assertions
type Stack struct {
    items []any
}
func (s *Stack) Push(v any) { s.items = append(s.items, v) }
func (s *Stack) Pop() any {
    n := len(s.items) - 1
    v := s.items[n]
    s.items = s.items[:n]
    return v
}

With generics, the type-safe version

type Stack[T any] struct {
    items []T
}
func (s *Stack[T]) Push(v T) { s.items = append(s.items, v) }
func (s *Stack[T]) Pop() T {
    n := len(s.items) - 1
    v := s.items[n]
    s.items = s.items[:n]
    return v
}

var s Stack[int]
s.Push(5)
// s.Push("oops")  // compile error now, caught before runtime

The practical guidance

Reach for any when a value's type genuinely doesn't matter to your code (like a generic logging function), or when working with truly dynamic data (arbitrary JSON). Prefer generics when you want a container or algorithm that works over multiple types but should still be type-checked at compile time.

Related Resources

Both let you recover a concrete type from an interface value, but they fit different shapes of problem.

Single type assertion

var i interface{} = "hello"

s, ok := i.(string)
fmt.Println(s, ok)   // "hello", true

n, ok := i.(int)
fmt.Println(n, ok)   // 0, false — no panic, since we used the two-value form
s := i.(string)   // "hello" — fine here
n := i.(int)       // panic: interface conversion: interface {} is string, not int

The single-value form is only safe when you're certain of the underlying type. Otherwise, always use the two-value form.

Type switch for multiple possibilities

func describe(i interface{}) string {
    switch v := i.(type) {
    case int:
        return fmt.Sprintf("int: %d", v)
    case string:
        return fmt.Sprintf("string: %q", v)
    case bool:
        return fmt.Sprintf("bool: %t", v)
    case nil:
        return "nil value"
    default:
        return fmt.Sprintf("unknown type: %T", v)
    }
}

Inside each case, v is automatically narrowed to that specific type, so v.something type-checks against the concrete type in that branch, not the original interface type.

When to use which

Use a single type assertion when you expect exactly one specific type and want to confirm it. Use a type switch when a value could reasonably be one of several types, and you need different behavior per type. A generic describe or render function over a small, known set of inputs is a typical case.

Related Resources

Go has no class inheritance at all. Embedding is how it gets most of the code-reuse benefit without the complexity that comes with it.

Struct embedding and field/method promotion

type Employee struct {
    Name string
    ID   int
}

func (e Employee) Describe() string {
    return fmt.Sprintf("%s (#%d)", e.Name, e.ID)
}

type Manager struct {
    Employee        // embedded, no field name
    Reports []Employee
}

m := Manager{Employee: Employee{Name: "Alice", ID: 1}}
fmt.Println(m.Name)        // "Alice" — promoted field, no m.Employee.Name needed
fmt.Println(m.Describe())  // promoted method, works directly on Manager

Overriding a promoted method

func (m Manager) Describe() string {
    return fmt.Sprintf("Manager: %s", m.Employee.Describe())
}

fmt.Println(m.Describe())  // uses Manager's own Describe, not Employee's

Defining a method with the same name on the outer type shadows the embedded one. Note there's no super.Describe() equivalent — the overriding method must call m.Employee.Describe() explicitly if it wants the embedded behavior too.

Interface embedding

type Reader interface {
    Read(p []byte) (n int, err error)
}
type Writer interface {
    Write(p []byte) (n int, err error)
}
type ReadWriter interface {
    Reader   // embeds Reader's method set
    Writer   // embeds Writer's method set
}

Any type implementing both Read and Write automatically satisfies ReadWriter, with no extra declaration.

Why "composition over inheritance" here

Embedding gives you code reuse and promoted behavior. But the embedded type has no idea it's embedded, there's no polymorphic dispatch through a base-class pointer, and the relationship stays explicit and visible in the struct definition. This avoids the classic fragile-base-class problems of deep inheritance hierarchies in other languages.

Related Resources

This is one of the trickier corners of Go's type system, and it's a very common interview question precisely because it trips up experienced developers too.

The rule, demonstrated

type Counter struct{ n int }

func (c *Counter) Increment() { c.n++ }  // pointer receiver

type Incrementer interface {
    Increment()
}

var i Incrementer = Counter{}    // compile error!
var i Incrementer = &Counter{}   // works

The error: Counter (value) does not implement Incrementer (Increment method has pointer receiver). Only *Counter has Increment in its method set, since Increment needs a pointer to actually mutate n.

Why the language works this way

A value-receiver method gets a copy of the receiver, so it can never mutate the original. That's safe to call on either a value or a pointer, since Go automatically takes the address for you when needed (c.Increment() where c is addressable). A pointer-receiver method genuinely needs the real address to mutate the original. There's no way to get a genuine address from a plain, non-addressable value stored in an interface, so the method set of T excludes pointer-receiver methods entirely, keeping the rule simple and safe.

The practical gotcha with interfaces specifically

counters := []Counter{{n: 1}, {n: 2}}
for _, c := range counters {
    var i Incrementer = c   // still fails to compile — c here is a Counter value
}

Even though c came from a slice you could take the address of, the interface conversion still requires the value itself to have the pointer method in its set, and a plain Counter value never does.

The rule of thumb

If any method on a type uses a pointer receiver, treat the whole type as "used via a pointer" everywhere. That includes when it needs to satisfy an interface, and it avoids this whole class of compile error.

Related Resources