Interfaces

Interfaces are Go's way of achieving polymorphism. They define behavior, not data.

The Key Insight

Go interfaces are implicit: a type implements an interface by implementing its methods, without declaring it. This is sometimes called "duck typing" (if it walks like a duck and quacks like a duck, it's a duck).

Defining Interfaces

// Interface with one method
type Reader interface {
    Read(p []byte) (n int, err error)
}

// Interface with multiple methods
type ReadWriter interface {
    Read(p []byte) (n int, err error)
    Write(p []byte) (n int, err error)
}

// Embedding interfaces
type ReadWriteCloser interface {
    Reader  // Embeds Reader interface
    Writer  // Embeds Writer interface
    Close() error
}

Implementing Interfaces

No explicit declaration needed. Just implement the methods:

type Stringer interface {
    String() string
}

type Person struct {
    Name string
    Age  int
}

// Person now implements Stringer
func (p Person) String() string {
    return fmt.Sprintf("%s (%d years old)", p.Name, p.Age)
}

// Can be used anywhere Stringer is expected
func PrintAnything(s Stringer) {
    fmt.Println(s.String())
}

p := Person{Name: "Alice", Age: 30}
PrintAnything(p)  // "Alice (30 years old)"

The Empty Interface

interface{} (or any in Go 1.18+) matches any type:

func PrintAny(v interface{}) {
    fmt.Println(v)
}

// Or with the any alias (Go 1.18+)
func PrintAny(v any) {
    fmt.Println(v)
}

PrintAny(42)
PrintAny("hello")
PrintAny([]int{1, 2, 3})
PrintAny(struct{ Name string }{"Alice"})

Type Assertions

Extract the concrete type from an interface:

var i interface{} = "hello"

// Type assertion (panics if wrong type)
s := i.(string)
fmt.Println(s)  // "hello"

// Safe type assertion
s, ok := i.(string)
if ok {
    fmt.Println("It's a string:", s)
}

// Wrong type with comma-ok
n, ok := i.(int)
if !ok {
    fmt.Println("Not an int")
}

Type Switches

Check multiple types:

func describe(i interface{}) {
    switch v := i.(type) {
    case int:
        fmt.Printf("Integer %d\n", v)
    case string:
        fmt.Printf("String %q\n", v)
    case bool:
        fmt.Printf("Boolean %t\n", v)
    case nil:
        fmt.Println("Nil value")
    default:
        fmt.Printf("Unknown type %T\n", v)
    }
}

describe(42)       // Integer 42
describe("hello")  // String "hello"
describe(nil)      // Nil value

Common Standard Library Interfaces

Stringer

type Stringer interface {
    String() string
}

// Used by fmt.Print, fmt.Printf with %v and %s
type IPAddr [4]byte

func (ip IPAddr) String() string {
    return fmt.Sprintf("%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3])
}

Error

type error interface {
    Error() string
}

// Custom error type
type ValidationError struct {
    Field   string
    Message string
}

func (e ValidationError) Error() string {
    return fmt.Sprintf("%s: %s", e.Field, e.Message)
}

io.Reader and io.Writer

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

// Many types implement these: files, network connections, buffers...
func Copy(dst io.Writer, src io.Reader) (written int64, err error)

sort.Interface

type Interface interface {
    Len() int
    Less(i, j int) bool
    Swap(i, j int)
}

// Custom sorting
type ByAge []Person

func (a ByAge) Len() int           { return len(a) }
func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }
func (a ByAge) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }

people := []Person{
    {"Bob", 30},
    {"Alice", 25},
    {"Charlie", 35},
}
sort.Sort(ByAge(people))

Interface Best Practices

Accept Interfaces, Return Structs

// Good: accepts interface (flexible)
func Process(r io.Reader) error {
    // Works with files, network, buffers, etc.
}

// Good: returns concrete type
func NewBuffer() *bytes.Buffer {
    return &bytes.Buffer{}
}

Keep Interfaces Small

// Good: focused interface
type Reader interface {
    Read(p []byte) (n int, err error)
}

// Avoid: bloated interfaces
type FileHandler interface {
    Open() error
    Read([]byte) (int, error)
    Write([]byte) (int, error)
    Close() error
    Seek(int64, int) (int64, error)
    Stat() (os.FileInfo, error)
    // ...too many methods
}

Define Interfaces Where They're Used

// Package database
type Store struct { /* ... */ }
func (s *Store) GetUser(id int) (*User, error) { /* ... */ }

// Package handler - defines its own interface
type UserGetter interface {
    GetUser(id int) (*User, error)
}

type Handler struct {
    users UserGetter  // Depends on interface, not concrete type
}

Interface Values

An interface value has two components: a type and a value.

var w io.Writer         // nil interface
fmt.Printf("%T %v\n", w, w)  // <nil> <nil>

w = os.Stdout          // *os.File
fmt.Printf("%T %v\n", w, w)  // *os.File &{...}

w = new(bytes.Buffer)  // *bytes.Buffer
fmt.Printf("%T %v\n", w, w)  // *bytes.Buffer &{}

Nil Interface vs Nil Concrete Value

var w io.Writer  // nil interface

var buf *bytes.Buffer  // nil pointer
w = buf                // interface is NOT nil!

if w == nil {
    fmt.Println("nil")  // Won't print!
}

// w has type *bytes.Buffer with nil value
fmt.Printf("Type: %T, Value: %v, IsNil: %t\n", w, w, w == nil)
// Type: *bytes.Buffer, Value: <nil>, IsNil: false

This is a common gotcha. Solution:

func returnsWriter() io.Writer {
    var buf *bytes.Buffer
    // ...
    if buf == nil {
        return nil  // Return nil interface, not nil concrete
    }
    return buf
}

Interface Composition

Build larger interfaces from smaller ones:

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
}

// Or inline
type ReadWriteCloser interface {
    io.Reader
    io.Writer
    io.Closer
}

Interfaces with Generics (Go 1.18+)

Type Constraints

// Interface as type constraint
type Number interface {
    int | int32 | int64 | float32 | float64
}

func Sum[T Number](nums []T) T {
    var sum T
    for _, n := range nums {
        sum += n
    }
    return sum
}

// Using comparable
func Contains[T comparable](slice []T, target T) bool {
    for _, v := range slice {
        if v == target {
            return true
        }
    }
    return false
}

Constraint with Methods

type Stringer interface {
    String() string
}

func PrintAll[T Stringer](items []T) {
    for _, item := range items {
        fmt.Println(item.String())
    }
}

Testing with Interfaces

Interfaces enable easy mocking:

// In production code
type EmailSender interface {
    Send(to, subject, body string) error
}

type UserService struct {
    emailer EmailSender
}

func (s *UserService) Register(email string) error {
    // ... create user
    return s.emailer.Send(email, "Welcome!", "Thanks for joining")
}

// In test code
type MockEmailer struct {
    Sent []struct{ To, Subject, Body string }
}

func (m *MockEmailer) Send(to, subject, body string) error {
    m.Sent = append(m.Sent, struct{ To, Subject, Body string }{to, subject, body})
    return nil
}

func TestRegister(t *testing.T) {
    mock := &MockEmailer{}
    service := &UserService{emailer: mock}

    service.Register("test@example.com")

    if len(mock.Sent) != 1 {
        t.Error("Expected one email sent")
    }
}

Verifying Interface Implementation

Compile-time check that a type implements an interface:

// Ensure *MyWriter implements io.Writer
var _ io.Writer = (*MyWriter)(nil)

// Ensure MyStruct implements fmt.Stringer
var _ fmt.Stringer = MyStruct{}

Next Steps

Continue to 08-error-handling.md to learn about Go's error handling philosophy.