Interfaces
Interfaces in Go are the cornerstone of polymorphism. They are defined as contracts (method signatures) and satisfied implicitly β no implements keyword required. This "duck typing" approach enables elegant, decoupled designs.
π― Learning Objectives
- Define interfaces and understand implicit satisfaction
- Understand the two-word interface value: (type, value)
- Use type assertions and type switches safely
- Compose interfaces from smaller interfaces
- Explain the nil interface vs interface holding nil pointer
- Apply the SOLID principles using Go interfaces
- Use common interfaces: io.Reader, io.Writer, error, fmt.Stringer
π Interface Definition & Implicit Satisfaction
An interface defines a set of method signatures. Any type that implements all those methods automatically satisfies the interface β no declaration needed. This is called structural typing (duck typing).
package main
import (
"fmt"
"math"
)
// Define an interface
type Shape interface {
Area() float64
Perimeter() float64
}
// Circle implicitly satisfies Shape
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 }
// Rectangle implicitly satisfies Shape
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) }
// Triangle also satisfies Shape
type Triangle struct{ A, B, C float64 } // side lengths
func (t Triangle) Perimeter() float64 { return t.A + t.B + t.C }
func (t Triangle) Area() float64 {
s := t.Perimeter() / 2
return math.Sqrt(s * (s - t.A) * (s - t.B) * (s - t.C)) // Heron's formula
}
// Function that accepts any Shape
func printShapeInfo(s Shape) {
fmt.Printf("Type: %T\n Area: %.4f\n Perimeter: %.4f\n", s, s.Area(), s.Perimeter())
}
func totalArea(shapes []Shape) float64 {
total := 0.0
for _, s := range shapes {
total += s.Area()
}
return total
}
func main() {
shapes := []Shape{
Circle{Radius: 5},
Rectangle{Width: 4, Height: 6},
Triangle{A: 3, B: 4, C: 5},
}
for _, s := range shapes {
printShapeInfo(s)
}
fmt.Printf("Total area: %.4f\n", totalArea(shapes))
// Compile-time interface check (no runtime cost)
var _ Shape = Circle{} // ensures Circle implements Shape
var _ Shape = Rectangle{} // ensures Rectangle implements Shape
}π¬ Interface Internals β Two-Word Values
An interface value is a two-word struct containing: (1) a type pointer (itab), and (2) a data pointer (or the actual value for small types). Understanding this explains nil interface gotchas.
package main
import "fmt"
type Animal interface {
Sound() string
}
type Dog struct{ Name string }
func (d *Dog) Sound() string { return "Woof" }
func getAnimal(createDog bool) Animal {
var d *Dog = nil // d is a nil *Dog
if createDog {
d = &Dog{Name: "Rex"}
}
return d // β THIS IS THE CLASSIC GOTCHA!
// Even when d is nil, we return an interface with:
// type word: *Dog (non-nil!)
// data word: nil
// So the interface itself is NOT nil!
}
func main() {
// β
Correct nil check
a1 := getAnimal(false)
fmt.Println("a1 == nil:", a1 == nil) // false!!! (type word is *Dog)
// Calling a1.Sound() would PANIC β data is nil
// The fix: return nil directly from the function, not a nil typed value
// return nil // β this sets both words to nil
// Normal usage
a2 := getAnimal(true)
fmt.Println("a2 == nil:", a2 == nil) // false (and correctly has a value)
fmt.Println(a2.Sound()) // Woof
// Proper nil interface
var a3 Animal // zero value: both words nil
fmt.Println("a3 == nil:", a3 == nil) // true
}A function returning an interface type should return nil directly β never a typed nil. Returning a typed nil (like (*Dog)(nil)) creates a non-nil interface with a nil data pointer. Calling methods on it panics. This is one of Go's most subtle and dangerous bugs.
π Type Assertions & Type Switches
package main
import "fmt"
type Animal interface{ Sound() string }
type Dog struct{ Breed string }
type Cat struct{ Indoor bool }
func (d Dog) Sound() string { return "Woof" }
func (c Cat) Sound() string { return "Meow" }
func describe(a Animal) {
// ββ Type assertion (single type) ββ
// Form 1: panics if wrong type β use only when you're certain!
d := a.(Dog) // panics if a is not Dog
fmt.Println("dog breed:", d.Breed)
}
func safeDescribe(a Animal) {
// Form 2: comma-ok β safe, no panic
if d, ok := a.(Dog); ok {
fmt.Println("It's a dog!", d.Breed)
} else if c, ok := a.(Cat); ok {
fmt.Println("It's a cat! indoor:", c.Indoor)
} else {
fmt.Println("Unknown animal")
}
}
// ββ Type switch β handle multiple types elegantly ββ
func processValue(v interface{}) {
switch x := v.(type) {
case nil:
fmt.Println("nil value")
case int:
fmt.Printf("int: %d (squared: %d)\n", x, x*x)
case float64:
fmt.Printf("float64: %f\n", x)
case string:
fmt.Printf("string: %q (len=%d)\n", x, len(x))
case bool:
fmt.Printf("bool: %v\n", x)
case []int:
fmt.Printf("[]int with %d elements\n", len(x))
case Dog:
fmt.Printf("Dog: %s\n", x.Breed)
case Animal: // catch-all for other Animal types
fmt.Printf("some animal: %s\n", x.Sound())
default:
fmt.Printf("unknown type: %T\n", x)
}
}
func main() {
var a Animal = Dog{Breed: "Labrador"}
safeDescribe(a)
a = Cat{Indoor: true}
safeDescribe(a)
processValue(42)
processValue(3.14)
processValue("hello")
processValue(Dog{Breed: "Poodle"})
processValue(nil)
processValue([]int{1, 2, 3})
}π― Interface Composition
Go interfaces are composable. You can embed one interface inside another to build larger contracts from smaller pieces. The standard library uses this extensively.
package main
import (
"bytes"
"fmt"
"io"
"strings"
)
// Small, focused interfaces
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
// Composed interfaces
type ReadWriter interface {
Reader
Writer
}
type ReadWriteCloser interface {
Reader
Writer
Closer
}
// Real stdlib composition:
// io.Reader, io.Writer, io.Closer, io.ReadWriter, io.ReadWriteCloser
func processData(r io.Reader) (string, error) {
buf := new(bytes.Buffer)
_, err := io.Copy(buf, r)
return buf.String(), err
}
func main() {
// strings.NewReader implements io.Reader
r := strings.NewReader("Hello, World!")
data, _ := processData(r)
fmt.Println(data) // Hello, World!
// bytes.Buffer implements io.Reader AND io.Writer
var buf bytes.Buffer
buf.WriteString("buffered data")
result, _ := processData(&buf)
fmt.Println(result)
// Accept interface, return interface principle:
// Write functions accepting the smallest interface they need
// io.Reader if you only need to read
// io.Writer if you only need to write
// This makes functions testable and composable
}π Common Standard Library Interfaces
| Interface | Package | Methods | Used for |
|---|---|---|---|
error | builtin | Error() string | Error handling |
fmt.Stringer | fmt | String() string | Custom string representation |
io.Reader | io | Read([]byte) (int, error) | Reading data streams |
io.Writer | io | Write([]byte) (int, error) | Writing data streams |
io.Closer | io | Close() error | Closing resources |
http.Handler | net/http | ServeHTTP(ResponseWriter, *Request) | HTTP request handling |
sort.Interface | sort | Len() int; Less(i,j int) bool; Swap(i,j int) | Custom sorting |
encoding.Marshaler | json | MarshalJSON() ([]byte, error) | Custom JSON encoding |
context.Context | context | Deadline(); Done(); Err(); Value() | Cancellation, timeouts |
π€ Interview Questions
In Java/C#, a class must explicitly declare which interfaces it implements (implements Runnable, : IDisposable). This is nominal typing β satisfaction is declared by name.
Go uses structural typing (duck typing): any type that has the required methods automatically satisfies an interface, with zero explicit declaration. No coupling between the interface definition and the implementing type.
Benefits: (1) You can define interfaces for types you don't own (adaptor pattern without wrappers); (2) Interfaces can be defined after types are written; (3) Promotes small, focused interfaces (interface segregation principle); (4) Makes testing trivially easy β wrap any dependency with an interface and mock it.
The Go proverb: "Accept interfaces, return concrete types."
An interface value consists of two words: (type, value). A nil interface has both words set to nil. But a function returning a typed nil (e.g., return (*MyError)(nil) where the return type is error) creates an interface where the type word is non-nil but the value word is nil.
The result: the interface is NOT equal to nil, so if err != nil returns true even though the underlying value is nil. Calling methods on it panics.
Fix: always return nil directly from functions with interface return types. Never return typed nils from interface-returning functions.
This is Go's most notoriously confusing bug for newcomers.
π§ͺ Quiz
interface{} (or any) mean?π Cheat Sheet
type I interface { Method() }func (t T) Method() { } // no declaration!type RW interface { Reader; Writer }interface{} or any // accepts all typesv, ok := i.(T) // safeswitch v := i.(type) { case T: ... }i == nil // both type+data must be nil16 bytes (2 words: type + data pointers)func (T) String() stringvar _ I = (*T)(nil) // assert T implements I