Interfaces
Interfaces are the single most powerful abstraction in Go. They specify
behavior (a set of method signatures a type must provide), and
Go's implicit implementation rule makes them effortless to use.
If the method set fits, the type "is" the interface. No
implements keyword, no rigid hierarchy.
Learning objectives
- Define an interface and understand method sets.
- Explain what "implicit implementation" means and why it matters.
- Use
any(empty interface) and know when to avoid it. - Extract concrete types with type assertions and type switches.
- Implement
Stringerto customizefmtoutput. - Recognize the "typed nil interface" gotcha.
Defining an interface
type Speaker interface {
Speak() string
}
That's it. An interface is a named method set with zero or more signatures. No fields, no implementations.
Implicit implementation
A type satisfies an interface simply by having the right methods. You don't declare it; Go checks at compile time:
type Dog struct{ Name string }
func (d Dog) Speak() string { return "Woof, I'm " + d.Name }
var s Speaker = Dog{Name: "Buddy"} // Dog satisfies Speaker automatically
fmt.Println(s.Speak())
This is called duck typing, verified by the compiler:
if it quacks like a Speaker (has a Speak() string method),
it is one. Two huge benefits:
- You can retroactively make existing types satisfy new interfaces.
- Packages don't need to know about interfaces their consumers might define. Define interfaces where you use them, not where the type is defined.
Polymorphism through interfaces
type Shape interface { Area() float64 }
type Circle struct{ R float64 }
func (c Circle) Area() float64 { return math.Pi * c.R * c.R }
type Square struct{ Side float64 }
func (s Square) Area() float64 { return s.Side * s.Side }
func totalArea(shapes []Shape) float64 {
var sum float64
for _, s := range shapes {
sum += s.Area() // dispatches to the concrete type's method
}
return sum
}
shapes := []Shape{Circle{R: 1}, Square{Side: 2}}
fmt.Println(totalArea(shapes))
Empty interface: any
An interface with zero methods is satisfied by every type:
var x any = 42
x = "hello"
x = []int{1, 2, 3}
As of Go 1.18, any is an alias for
interface{}. Use any in new code; it reads
better.
any is a last resort
Once a value is any, you've opted out of static typing.
You need a type assertion or type switch to do anything useful with
it. Prefer a more specific interface whenever possible.
Type assertions
var i any = "hello"
s := i.(string) // "hello": panics if i isn't actually a string
s, ok := i.(string) // safe form: ok is false if wrong type
if ok {
fmt.Println(s)
}
Use the comma-ok form unless you're 100% sure of the type.
Type switches
func describe(v any) {
switch x := v.(type) {
case int: fmt.Println("int:", x)
case string: fmt.Printf("string: %q\n", x)
case []int: fmt.Println("int slice, len", len(x))
case nil: fmt.Println("nil")
default: fmt.Printf("unknown %T\n", x)
}
}
Cleaner than a chain of type-assertion ifs when you need to branch on multiple possible types.
The Stringer idiom
The most common tiny interface in Go:
type Stringer interface {
String() string
}
If your type implements String() string, the
fmt package automatically uses it:
type Weekday int
const (
Sun Weekday = iota
Mon
Tue
// ...
)
func (d Weekday) String() string {
return []string{"Sun", "Mon", "Tue"}[d]
}
fmt.Println(Mon) // Mon: not "1"
Also handy: error is literally an interface,
type error interface { Error() string }. Chapter 23
covers that.
The typed-nil-interface gotcha
An interface value has two components: (concrete type, value).
It's nil only when both are nil:
var p *User // nil pointer
var i any = p // interface holding (type=*User, value=nil)
fmt.Println(p == nil) // true
fmt.Println(i == nil) // false: interface is NOT nil
This trips up nearly every Go programmer at least once. If you
return an interface from a function, return the literal
nil (not a typed-nil pointer wrapped in the interface).
Interface design
- Keep interfaces small.
io.Readerhas one method. That's why it's everywhere. - Accept interfaces, return structs. Be flexible about inputs; be concrete about outputs.
- Define them where they're used, not where implementing types live. This keeps packages decoupled.
Check your understanding
Practice exercises
Build a Shape interface
Define a Shape interface with Area() float64 and Perimeter() float64. Implement it for Circle, Rectangle, and Triangle. Write a function describe(s Shape) that prints the area and perimeter.
Show solution
type Shape interface {
Area() float64
Perimeter() float64
}
type Circle struct{ R float64 }
type Rectangle struct{ W, H float64 }
type Triangle struct{ A, B, C float64 }
func (c Circle) Area() float64 { return math.Pi * c.R * c.R }
func (c Circle) Perimeter() float64 { return 2 * math.Pi * c.R }
func (r Rectangle) Area() float64 { return r.W * r.H }
func (r Rectangle) Perimeter() float64 { return 2 * (r.W + r.H) }
func (t Triangle) Area() float64 { s := t.Perimeter()/2; return math.Sqrt(s*(s-t.A)*(s-t.B)*(s-t.C)) }
func (t Triangle) Perimeter() float64 { return t.A + t.B + t.C }
func describe(s Shape) {
fmt.Printf("%T: area=%.2f perimeter=%.2f\n", s, s.Area(), s.Perimeter())
}
Notice: nothing declares that Circle "implements Shape". The compiler just checks method sets.
Further reading
Interfaces, Go's superpower.