Methods & Interfaces
Go’s approach to polymorphism
Section titled “Go’s approach to polymorphism”Most object-oriented languages bolt polymorphism onto a class hierarchy: you declare that a type implements an interface, you extends a base class, and the compiler enforces the lineage. Go throws away the hierarchy entirely.
In Go, any named type can have methods (except pointer types and interface types), and any type that has the right methods automatically satisfies an interface — no declaration required. This is called implicit interface satisfaction, and it is the single most important design decision in the language.
The combination of methods and interfaces gives you:
- Polymorphism without inheritance trees.
- Dependency inversion without frameworks — accept an interface, return a struct.
- Composition without
extends— embed types to promote their fields and methods.
Every rule in this module resolves to one of two things at the keyboard: a compile error naming a missing or mismatched method, or a nil surprise inside an interface or an embedded pointer. Learning to recognize both on sight — not just knowing the rule in the abstract — is the actual skill this module builds.
Module map
Section titled “Module map”| Lesson | What you will learn |
|---|---|
| Methods | Declare methods on any named type; value vs pointer receivers; method sets. |
| Interfaces | Define and satisfy interfaces implicitly; small interfaces; the empty interface any. |
| Type Assertions | x.(T), the comma-ok form, and switch v := x.(type). |
| Embedding | Struct embedding, promoted fields/methods, interface embedding, composition over inheritance. |
A first runnable example
Section titled “A first runnable example”The snippet below defines a method on a struct, declares a one-method interface, and calls the method through the interface — all in under 20 lines.
package main
import "fmt"
type Rectangle struct { Width, Height float64}
func (r Rectangle) Area() float64 { return r.Width * r.Height}
type Sizer interface { Area() float64}
func printArea(s Sizer) { fmt.Printf("area = %.2f\n", s.Area())}
// Counter shows the other half of the story: a pointer receiver that mutates.type Counter struct{ n int }
func (c *Counter) Inc() { c.n++ }
func main() { r := Rectangle{Width: 4, Height: 3} fmt.Println(r.Area()) printArea(r)
c := &Counter{} c.Inc() c.Inc() fmt.Println(c.n) // 2 — pointer receiver mutated the original} Trade-offs
Section titled “Trade-offs”| What you get | Benefit | Cost |
|---|---|---|
| Implicit interface satisfaction | Decoupled code, no import dependency on the interface | Harder to discover which types implement which interface |
| Value receiver | Cheap to reason about, callable on non-addressable values | Copies the struct on every call |
| Pointer receiver | Can mutate, no copy | Needs a nil-check, needs an addressable value |
| Embedding (composition) | Reuses methods/fields without inheriting | No virtual dispatch — polymorphism still needs an interface |
Common Misconceptions
Section titled “Common Misconceptions”- An interface is like an abstract class. In Go an interface has only method signatures — no implementation, no state.
- Embedding is inheritance. The embedded type has no knowledge of the outer type; there is no override and no virtual dispatch.
- A nil interface is always
== nil. An interface holding a nil pointer is not itself nil —(nil, nil)is not the same as(*T, nil). See Interfaces.
💡 Real-world example
The standard library builds on
io.Readerandio.Writeras its core interfaces —os.File,bytes.Buffer,net.Conn, and an HTTP request body all implement the same two methods, so they compose across the whole ecosystem without knowing about each other.Kubernetes’
runtime.Objectinterface (k8s.io/apimachinery/pkg/runtime) is satisfied by every Kubernetes API resource — a controller written once againstruntime.Objectworks withPod,Deployment, andServicealike.
Reading AI-generated code
Section titled “Reading AI-generated code”Across every lesson in this module, the same handful of checks catch most generated-code bugs before they ship:
- Does every method on a type use the same receiver kind? A mix, or a value receiver on a type meant to mutate, is the first thing to check.
- Is a concrete value assigned to an interface variable when one of the required methods has a pointer receiver? That is a compile error, not a runtime bug — read the message, it names the method.
- Does a function return a typed
nilpointer through an interface-typed result (error, or any other interface)? The caller’s== nilcheck will be wrong. - Does a single-value type assertion (
v := x.(T)) run without a comma-ok guard on a value that might not holdT? - Does an embedded pointer, or an embedded interface used as a partial mock, get a promoted method called on it before anything assigns it a real value?
🐛 Spot the bug
type Rectangle struct { Width, Height float64}
func (r *Rectangle) Area() float64 { return r.Width * r.Height}
type Sizer interface { Area() float64}
func main() { var s Sizer = Rectangle{Width: 4, Height: 3} _ = s}Show the answer
Area has a pointer receiver, so Rectangle’s method set (the value type) does not include it — only *Rectangle’s does. Assigning a plain Rectangle{} value to the Sizer interface variable fails to compile: cannot use Rectangle{…} (value of struct type Rectangle) as Sizer value in variable declaration: Rectangle does not implement Sizer (method Area has pointer receiver). The fix is var s Sizer = &Rectangle{Width: 4, Height: 3}.