GO

Go Fundamentals

18 lessons

Progress0%
1. Introduction to Go
1What is Go?
2. Variables and Data Types
1Data Types in Go
3. Control Flow
If, For, and SwitchDefer, Panic, Recover
4. Functions
Function BasicsError Handling
5. Structs and Methods
StructsMethods and Interfaces
6. Concurrency
Goroutines and ChannelsSelect and Sync
7. Maps & Slices Advanced
Slices Deep DiveMaps Operations & Patterns
8. Interfaces Deep Dive
Interface Composition & anyCommon Interfaces & Patterns
9. Packages & Modules
Package SystemGo Modules & Workspace
10. Testing & Standard Library
Testing in GoStandard Library Essentials
All Tutorials
GoInterfaces Deep Dive
Lesson 14 of 18 min
Chapter 8 · Lesson 2

Common Interfaces & Patterns

Common Interfaces & Patterns

The Go standard library defines a small set of powerful interfaces. Learning them — and the idioms around them — is essential for idiomatic Go.

io.Reader, io.Writer, io.Closer The io package provides the foundational I/O interfaces:

go
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 }

These combine into io.ReadWriter, io.ReadCloser, io.WriteCloser, and io.ReadWriteCloser. Files, network connections, HTTP bodies, and in-memory buffers all satisfy these interfaces, enabling functions to work with any I/O source.

sort.Interface The sort package sorts any collection implementing three methods:

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

Implement these on a custom slice type to sort by any criteria.

http.Handler The entire Go HTTP ecosystem is built on a single interface:

go
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

Any type with ServeHTTP can handle HTTP requests — routers, middleware, and handlers all compose through this interface.

fmt.Stringer and error

  • fmt.Stringer: String() string — controls default formatting by fmt
  • error: Error() string — the universal error representation

Interface Segregation — Prefer Small Interfaces Small, focused interfaces are easier to implement, test, and compose. The Go proverb: "The bigger the interface, the weaker the abstraction." io.Reader (one method) is used everywhere; a 10-method interface is used nowhere.

Accept Interfaces, Return Structs Functions should accept the narrowest interface that satisfies their needs (maximising flexibility for callers) and return concrete types (giving callers full access to capabilities without them needing to assert):

go
// Accept interface — caller can pass any Reader
func ProcessData(r io.Reader) error { ... }

// Return concrete — caller gets full *os.File capabilities
func OpenConfig(path string) (*os.File, error) { ... }

Mocking with Interfaces Interfaces enable dependency injection and testability. Define your dependency as an interface; in tests, substitute a lightweight mock:

go
type DataStore interface {
    Get(id string) (Record, error)
    Save(r Record) error
}
// Production: RealDB implements DataStore
// Test: MockDB implements DataStore

This pattern keeps production code testable without hitting real databases or external services.

Code Examples

Custom sort.Interface implementationgo
package main

import (
    "fmt"
    "sort"
)

type Person struct {
    Name string
    Age  int
}

// ByAge implements sort.Interface
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] }

// ByName implements sort.Interface
type ByName []Person

func (n ByName) Len() int           { return len(n) }
func (n ByName) Less(i, j int) bool { return n[i].Name < n[j].Name }
func (n ByName) Swap(i, j int)      { n[i], n[j] = n[j], n[i] }

func main() {
    people := []Person{
        {"Charlie", 30},
        {"Alice", 25},
        {"Bob", 35},
    }

    sort.Sort(ByAge(people))
    fmt.Println("By age:")
    for _, p := range people {
        fmt.Printf("  %s (%d)\n", p.Name, p.Age)
    }

    sort.Sort(ByName(people))
    fmt.Println("By name:")
    for _, p := range people {
        fmt.Printf("  %s (%d)\n", p.Name, p.Age)
    }

    // Modern alternative: sort.Slice with closure
    sort.Slice(people, func(i, j int) bool {
        return people[i].Age > people[j].Age // descending age
    })
    fmt.Println("By age descending:")
    for _, p := range people {
        fmt.Printf("  %s (%d)\n", p.Name, p.Age)
    }
}

Implementing sort.Interface on a named slice type gives full control over sort order. sort.Slice with a closure is a concise alternative when you do not need a reusable sorter type.

Implementing io.Readergo
package main

import (
    "fmt"
    "io"
    "strings"
)

// ROT13Reader wraps an io.Reader, applying ROT13 on Read
type ROT13Reader struct {
    r io.Reader
}

func rot13(b byte) byte {
    switch {
    case b >= 'A' && b <= 'Z':
        return 'A' + (b-'A'+13)%26
    case b >= 'a' && b <= 'z':
        return 'a' + (b-'a'+13)%26
    }
    return b
}

func (r ROT13Reader) Read(p []byte) (int, error) {
    n, err := r.r.Read(p)
    for i := 0; i < n; i++ {
        p[i] = rot13(p[i])
    }
    return n, err
}

func main() {
    original := strings.NewReader("Hello, World!")
    rot := ROT13Reader{original}

    // io.ReadAll reads until EOF
    decoded, err := io.ReadAll(rot)
    if err != nil {
        fmt.Println("error:", err)
        return
    }
    fmt.Println("ROT13:", string(decoded))

    // Apply twice to get back original
    rot2 := ROT13Reader{ROT13Reader{strings.NewReader("Hello, World!")}}
    doubled, _ := io.ReadAll(rot2)
    fmt.Println("ROT13 twice:", string(doubled))
}

ROT13Reader wraps any io.Reader and transforms bytes on the fly. Because it implements io.Reader itself, it can be wrapped again — composing readers is the Go way.

Interface-based mocking for testsgo
package main

import (
    "errors"
    "fmt"
)

// DataStore interface — production and test use different impls
type DataStore interface {
    Get(id string) (string, error)
    Save(id, value string) error
}

// InMemoryStore — used in tests
type InMemoryStore struct {
    data map[string]string
}

func NewInMemoryStore() *InMemoryStore {
    return &InMemoryStore{data: make(map[string]string)}
}

func (s *InMemoryStore) Get(id string) (string, error) {
    v, ok := s.data[id]
    if !ok {
        return "", errors.New("not found: " + id)
    }
    return v, nil
}

func (s *InMemoryStore) Save(id, value string) error {
    s.data[id] = value
    return nil
}

// Service depends only on the interface
type UserService struct {
    store DataStore
}

func (u *UserService) CreateUser(id, name string) error {
    return u.store.Save(id, name)
}

func (u *UserService) GetUser(id string) (string, error) {
    return u.store.Get(id)
}

func main() {
    // Inject the in-memory store (as in a test)
    svc := &UserService{store: NewInMemoryStore()}

    svc.CreateUser("u1", "Alice")
    svc.CreateUser("u2", "Bob")

    name, err := svc.GetUser("u1")
    fmt.Println("Found:", name, err)

    _, err = svc.GetUser("u99")
    fmt.Println("Missing:", err)
}

UserService depends only on the DataStore interface. Swapping InMemoryStore for a real database implementation requires zero changes to UserService — this is dependency injection via interfaces.

Quick Quiz

1. What three methods must a type implement to satisfy sort.Interface?

2. What does the Go guideline 'accept interfaces, return structs' mean?

3. Which statement best describes why small interfaces are preferred in Go?

Was this lesson helpful?

PreviousNext