Go Interfaces
Overview
An interface defines a set of method signatures. Any type that implements all those methods automatically satisfies the interface — no explicit implements declaration needed. This is called structural typing (or duck typing). Interfaces are Go's primary mechanism for polymorphism and decoupling.
Defining and Using Interfaces
An interface says what a type can do, not what it is. Any type with matching methods satisfies it.
type Shape interface {
Area() float64
Perimeter() float64
}
type Circle struct{ Radius float64 }
type Rect 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 Rect) Area() float64 { return r.Width * r.Height }
func (r Rect) Perimeter() float64 { return 2 * (r.Width + r.Height) }
// Both Circle and Rect satisfy Shape — no declaration needed
func printShape(s Shape) {
fmt.Printf("Area: %.2f, Perimeter: %.2f\n", s.Area(), s.Perimeter())
}
printShape(Circle{Radius: 5})
printShape(Rect{Width: 4, Height: 3})
Implicit Implementation
A type never declares that it implements an interface. If the methods match, it satisfies it — automatically. This means:
- Library types can satisfy your interfaces without modification
- Your types can satisfy library interfaces without modifying the library
// Standard library interface — no need to import it
type Stringer interface {
String() string
}
type Person struct{ Name string; Age int }
// Person automatically satisfies fmt.Stringer
func (p Person) String() string {
return fmt.Sprintf("%s (%d)", p.Name, p.Age)
}
p := Person{Name: "Vatsal", Age: 25}
fmt.Println(p) // Vatsal (25) — fmt.Println uses String() automatically
Multiple Interfaces
A single type can satisfy any number of interfaces simultaneously. There's no limit.
type Flyer interface { Fly() string }
type Swimmer interface { Swim() string }
type Talker interface { Talk() string }
type Duck struct{ Name string }
func (d Duck) Fly() string { return d.Name + " is flying" }
func (d Duck) Swim() string { return d.Name + " is swimming" }
func (d Duck) Talk() string { return d.Name + " says: quack" }
var f Flyer = Duck{Name: "Donald"} // ✅
var s Swimmer = Duck{Name: "Donald"} // ✅
var t Talker = Duck{Name: "Donald"} // ✅
Interface Values
An interface value holds two things internally: a (type, value) pair. A nil interface has both as nil. An interface that holds a nil pointer is not nil itself — a common surprise.
var s Shape // nil interface — both type and value are nil
fmt.Println(s == nil) // true
var c *Circle = nil
s = c // interface holds (*Circle, nil)
fmt.Println(s == nil) // FALSE — interface is not nil, it holds a type!
Key insight: Always compare to
nilbefore calling methods on an interface value when the underlying type might be nil.
Empty Interface — interface{} / any
interface{} has no method requirements — every type satisfies it. Use when you genuinely need to hold any type (like fmt.Println). In Go 1.18+, any is an alias for interface{}.
func printAnything(v any) {
fmt.Printf("(%T) %v\n", v, v)
}
printAnything(42) // (int) 42
printAnything("hello") // (string) hello
printAnything([]int{1,2,3}) // ([]int) [1 2 3]
printAnything(nil) // (<nil>) <nil>
// Slice of anything
items := []any{1, "two", 3.0, true}
Named Interface Parameters
Naming the parameters in an interface method signature improves readability, especially when types are similar.
// Unnamed — hard to tell which string is which
type Copier interface {
Copy(string, string) int
}
// Named — self-documenting
type Copier interface {
Copy(src string, dst string) (bytesCopied int)
}
Type Assertion
Extract the concrete type from an interface value. Use the two-value form (v, ok) to avoid panics — the single-value form panics on wrong type.
var s Shape = Circle{Radius: 5}
// Safe — ok=false if wrong type, no panic
c, ok := s.(Circle)
if ok {
fmt.Println("radius:", c.Radius)
}
// Unsafe — panics if s is not a Circle
c = s.(Circle) // only use when you're certain
// One-liner for optional handling
if c, ok := s.(Circle); ok {
fmt.Println(c.Radius)
}
Type Switch
A type switch handles multiple possible types cleanly — much cleaner than chained type assertions.
func describe(i any) {
switch v := i.(type) {
case int:
fmt.Printf("int: %d (doubled: %d)\n", v, v*2)
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 with %d elements\n", len(v))
default:
fmt.Printf("unknown type: %T\n", v)
}
}
describe(42) // int: 42 (doubled: 84)
describe("hello") // string: "hello" (len: 5)
describe([]int{1, 2, 3}) // []int with 3 elements
Interface Composition
Interfaces can embed other interfaces to form larger contracts:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
// ReadWriter is the composition of Reader and Writer
type ReadWriter interface {
Reader
Writer
}
Key Standard Library Interfaces
These are the most important interfaces to know — understanding them unlocks a huge part of the stdlib.
// fmt.Stringer — controls fmt.Println output
type Stringer interface {
String() string
}
// error — Go's error type
type error interface {
Error() string
}
// io.Reader — anything you can read from (files, network, buffers)
type Reader interface {
Read(p []byte) (n int, err error)
}
// io.Writer — anything you can write to
type Writer interface {
Write(p []byte) (n int, err error)
}
// sort.Interface — anything that can be sorted
type Interface interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}
Interface Best Practices
- Accept interfaces, return concrete types — functions should accept interfaces (flexible); constructors should return concrete structs (inspectable)
- Keep interfaces small — 1-3 methods is ideal.
io.Readerhas 1 method and is used everywhere - Define interfaces at the consumer — don't define the interface where the type is implemented; define it where it's used
- Don't over-interface — if only one type will ever implement it, a plain function or concrete type is simpler
// Bad — return interface (hides type info, hard to test)
func NewUser() UserInterface { ... }
// Good — return concrete type (satisfies interfaces implicitly)
func NewUser() *User { ... }
// Good — accept interface (caller can pass anything that matches)
func Save(w io.Writer, data []byte) error { ... }
Gotchas
| Gotcha | Detail |
|---|---|
| nil interface vs interface holding nil | var p *T = nil; var i I = p — i is NOT nil even though p is |
| Pointer vs value receiver on interface | If a method uses a pointer receiver, only *T satisfies the interface, not T |
| Type assertion panic | v := i.(T) panics if wrong type — always use comma-ok form |
| Empty interface loses type info | Once stored in any, you need type assertion to get the original type back |
| Interface comparison | Two interface values are equal only if both their type and value are equal |