πŸ”Œ Chapter 11 of 20

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.

⏱ β€”πŸ“Š Intermediate–Advanced

🎯 Learning Objectives

βœ… After this chapter you will:
  • 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).

go
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
}
βœ… Output
Type: main.Circle Area: 78.5398 Perimeter: 31.4159 Type: main.Rectangle Area: 24.0000 Perimeter: 20.0000 Type: main.Triangle Area: 6.0000 Perimeter: 12.0000 Total area: 108.5398

πŸ”¬ 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.

πŸ“Š Interface Value Layout (16 bytes on 64-bit)
word 0*itab→ {type info, method table} for Circle
word 1*data→ actual Circle value (on heap if needed)
Nil interface: both words are nil (0). Interface with nil pointer: type word β‰  nil, data word = nil.
go
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
}
βœ… Output
a1 == nil: false a2 == nil: false Woof a3 == nil: true
🚨 The Nil Interface Gotcha

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

go
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})
}
βœ… Output
It's a dog! Labrador It's a cat! indoor: true int: 42 (squared: 1764) float64: 3.140000 string: "hello" (len=5) Dog: Poodle nil value []int with 3 elements

🎯 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.

go
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

InterfacePackageMethodsUsed for
errorbuiltinError() stringError handling
fmt.StringerfmtString() stringCustom string representation
io.ReaderioRead([]byte) (int, error)Reading data streams
io.WriterioWrite([]byte) (int, error)Writing data streams
io.CloserioClose() errorClosing resources
http.Handlernet/httpServeHTTP(ResponseWriter, *Request)HTTP request handling
sort.InterfacesortLen() int; Less(i,j int) bool; Swap(i,j int)Custom sorting
encoding.MarshalerjsonMarshalJSON() ([]byte, error)Custom JSON encoding
context.ContextcontextDeadline(); Done(); Err(); Value()Cancellation, timeouts

🎀 Interview Questions

1
How does Go's interface system differ from Java's or C#'s?
β–Ά
Detailed Answer

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."

2
What is the "nil interface" gotcha in Go?
β–Ά
Detailed Answer

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

🧩
Interfaces
5 questions
1. How does a Go type declare that it implements an interface?
2. An interface value is nil when:
3. What is the safe form of a type assertion in Go?
4. What does interface{} (or any) mean?
5. Which standard library interface is used for reading data streams?
β€”

πŸ“Œ Cheat Sheet

πŸ“Œ Interfaces β€” Quick Reference
Define
type I interface { Method() }
Satisfy (implicit)
func (t T) Method() { } // no declaration!
Compose
type RW interface { Reader; Writer }
Empty interface
interface{} or any // accepts all types
Type assertion
v, ok := i.(T) // safe
Type switch
switch v := i.(type) { case T: ... }
Nil check
i == nil // both type+data must be nil
Interface size
16 bytes (2 words: type + data pointers)
Stringer
func (T) String() string
Compile check
var _ I = (*T)(nil) // assert T implements I