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:
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:
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:
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 byfmterror: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):
// 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:
type DataStore interface {
Get(id string) (Record, error)
Save(r Record) error
}
// Production: RealDB implements DataStore
// Test: MockDB implements DataStoreThis pattern keeps production code testable without hitting real databases or external services.
Code Examples
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.
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.
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?