Back to Notes

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 nil before 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.Reader has 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

GotchaDetail
nil interface vs interface holding nilvar p *T = nil; var i I = pi is NOT nil even though p is
Pointer vs value receiver on interfaceIf a method uses a pointer receiver, only *T satisfies the interface, not T
Type assertion panicv := i.(T) panics if wrong type — always use comma-ok form
Empty interface loses type infoOnce stored in any, you need type assertion to get the original type back
Interface comparisonTwo interface values are equal only if both their type and value are equal