đź§© Go Interface Patterns: Building with LEGO Blocks
The Big Picture: Imagine You’re a Master Builder
Remember playing with LEGO blocks? Each block has bumps on top and holes on the bottom. Any block can connect to any other block because they all follow the same pattern.
Go interfaces work exactly like this! They’re patterns that tell different pieces of code how to connect together. Today, we’ll learn four super-powers:
- Interface Composition - Combining small LEGO sets into bigger ones
- Common Interfaces - The most popular LEGO pieces everyone uses
- io.Reader and io.Writer - Two magical pipes for moving data
- Interface Design Principles - Rules for building amazing things
🎠Interface Composition: Small Blocks, Big Buildings
What is Interface Composition?
Imagine you have three tiny LEGO sets:
- One makes a wheel 🛞
- One makes a seat đź’ş
- One makes a steering wheel 🎮
Now, combine all three and BOOM — you have a car! 🚗
That’s interface composition: taking small, simple interfaces and combining them into bigger, more powerful ones.
The Story
Little interfaces are like single superpowers. A Reader can read. A Writer can write. But what if you need BOTH superpowers? You compose them!
// Small interfaces (single superpowers)
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
// Composed interface (COMBINED superpowers!)
type ReadWriter interface {
Reader // I can read!
Writer // I can write too!
}
Why Is This Amazing?
Think about it like a job interview:
- Small interface: “Can you read?” ✅
- Composed interface: “Can you read AND write?” ✅✅
Your code becomes more flexible! A function asking for ReadWriter will accept ANYTHING that can do both. Files? Yes! Network connections? Yes! Memory buffers? Yes!
Real-World Example
// A Closer knows how to close
type Closer interface {
Close() error
}
// Combine Reader + Closer = ReadCloser
type ReadCloser interface {
Reader
Closer
}
// Now files work perfectly!
func processFile(rc ReadCloser) {
// Read data
data := make([]byte, 100)
rc.Read(data)
// Always close when done
rc.Close()
}
graph TD A["Reader"] --> D["ReadWriter"] B["Writer"] --> D A --> E["ReadCloser"] C["Closer"] --> E B --> F["WriteCloser"] C --> F D --> G["ReadWriteCloser"] E --> G F --> G
🔑 Key Insight
Small interfaces + Composition = Maximum flexibility
It’s easier to satisfy a small interface. Composition lets you ask for exactly what you need—no more, no less!
🌟 Common Interfaces: The Popular LEGO Pieces
The All-Stars of Go
Just like LEGO has popular pieces everyone uses (the 2x4 brick!), Go has interfaces that appear everywhere. Knowing these is like knowing the alphabet before writing stories.
1. Stringer — “Tell me about yourself!”
When you want anything to describe itself as text:
type Stringer interface {
String() string
}
// Example: A Person can describe itself
type Person struct {
Name string
Age int
}
func (p Person) String() string {
return fmt.Sprintf("%s, age %d", p.Name, p.Age)
}
// Now this works magically!
// fmt.Println(person) → "Alice, age 25"
2. error — “What went wrong?”
Every error in Go follows this simple interface:
type error interface {
Error() string
}
// Custom error example
type NotFoundError struct {
Item string
}
func (e NotFoundError) Error() string {
return e.Item + " was not found"
}
3. sort.Interface — “Put things in order!”
For sorting any collection:
type Interface interface {
Len() int // How many items?
Less(i, j int) bool // Is item i < item j?
Swap(i, j int) // Switch items i and j
}
4. http.Handler — “Handle web requests!”
The backbone of Go web servers:
type Handler interface {
ServeHTTP(w ResponseWriter, r *Request)
}
// Any type can become a web handler!
type HelloHandler struct{}
func (h HelloHandler) ServeHTTP(
w http.ResponseWriter,
r *http.Request,
) {
w.Write([]byte("Hello, World!"))
}
Common Interfaces Cheat List
| Interface | Method | Purpose |
|---|---|---|
Stringer |
String() |
Convert to text |
error |
Error() |
Describe errors |
Reader |
Read() |
Read data |
Writer |
Write() |
Write data |
Closer |
Close() |
Clean up |
đźšż io.Reader and io.Writer: The Magic Pipes
The Water Pipe Analogy
Imagine two types of pipes in your house:
- Reader pipe 🚰 — Water (data) flows OUT to you
- Writer pipe 🚿 — Water (data) flows IN from you
Everything in Go that deals with data uses these pipes!
io.Reader — “Give me data!”
type Reader interface {
Read(p []byte) (n int, err error)
}
How it works:
- You give it an empty bucket (
p []byte) - It fills the bucket with data
- It tells you how much it filled (
n) - It says if something went wrong (
err)
// Reading from a string
reader := strings.NewReader("Hello!")
bucket := make([]byte, 5)
n, err := reader.Read(bucket)
// bucket now contains: ['H','e','l','l','o']
// n = 5 (five bytes read)
io.Writer — “Take my data!”
type Writer interface {
Write(p []byte) (n int, err error)
}
How it works:
- You give it a bucket full of data (
p []byte) - It takes the data somewhere (file, network, etc.)
- It tells you how much it took (
n) - It says if something went wrong (
err)
// Writing to standard output
data := []byte("Hello, World!")
n, err := os.Stdout.Write(data)
// Prints: Hello, World!
// n = 13 (thirteen bytes written)
The Power: Everything Connects!
graph LR A["File"] --> |Reader| C["Your Code"] B["Network"] --> |Reader| C D["String"] --> |Reader| C C --> |Writer| E["File"] C --> |Writer| F["Network"] C --> |Writer| G["Buffer"]
Because files, network connections, and strings ALL implement Reader, your code works with ALL of them!
// This ONE function works with:
// - Files
// - Network connections
// - Compressed data
// - Encrypted data
// - Strings
// - ANYTHING that can Read!
func countBytes(r io.Reader) int {
buf := make([]byte, 1024)
total := 0
for {
n, err := r.Read(buf)
total += n
if err == io.EOF {
break
}
}
return total
}
io.Copy: The Ultimate Connector
// Copy data from any Reader to any Writer!
io.Copy(destination, source)
// Copy file to network
io.Copy(networkConn, file)
// Copy network to file
io.Copy(file, networkConn)
// Copy string to file
io.Copy(file, strings.NewReader("Hi!"))
🏗️ Interface Design Principles: The Master Builder’s Rules
Rule 1: Keep Interfaces Small
“The bigger the interface, the weaker the abstraction.” — Rob Pike (Go creator)
Bad: Giant interface with 10 methods Good: Tiny interface with 1-2 methods
// ❌ TOO BIG — Hard to implement
type DataManager interface {
Read() []byte
Write([]byte) error
Delete() error
Update([]byte) error
Backup() error
Restore() error
Compress() error
Encrypt() error
}
// ✅ JUST RIGHT — Easy to implement
type Reader interface {
Read(p []byte) (n int, err error)
}
Rule 2: Accept Interfaces, Return Structs
When your function needs input, ask for an interface. When your function gives output, return a concrete type.
// âś… GOOD: Accept interface
func Process(r io.Reader) Result {
// Works with files, strings, network...
}
// âś… GOOD: Return concrete type
func NewBuffer() *Buffer {
return &Buffer{}
}
// ❌ BAD: Accept concrete type
func Process(f *os.File) Result {
// Only works with files!
}
Rule 3: Define Interfaces Where They’re Used
Don’t define interfaces in the package that implements them. Define them where you NEED them.
// Package: database
type MySQL struct { /* ... */ }
func (m MySQL) Query(q string) []Row { }
// Package: myapp (YOUR code)
// Define interface HERE, where you use it
type Querier interface {
Query(q string) []Row
}
func GetUsers(db Querier) []User {
// Now you can test with fake database!
}
Rule 4: Don’t Force Interfaces
Only create an interface when you have a real need:
- Multiple implementations
- Testing with mocks
- Decoupling packages
// ❌ UNNECESSARY — Only one implementation
type UserServiceInterface interface {
GetUser(id int) User
}
type UserService struct{}
// ✅ BETTER — Just use the struct
type UserService struct{}
func (s UserService) GetUser(id int) User {}
The Design Principles Summary
graph TD A["Keep It Small"] --> E["Better Interfaces"] B["Accept Interfaces"] --> E C["Return Structs"] --> E D["Define at Use Site"] --> E E --> F["Flexible Code"] E --> G["Testable Code"] E --> H["Clean Code"]
🎯 Putting It All Together
Your Interface Toolkit
| Principle | Remember |
|---|---|
| Composition | Small + Small = Powerful |
| Common Interfaces | Learn the classics! |
| Reader/Writer | Universal data pipes |
| Keep Small | 1-2 methods max |
| Accept Interfaces | Maximum flexibility |
| Return Structs | Clear contracts |
The Golden Rule
If you can describe what something DOES in one sentence, that’s your interface.
- “It reads data” →
Reader - “It writes data” →
Writer - “It closes resources” →
Closer - “It converts to string” →
Stringer
You’re Now a Master Builder! 🏆
You’ve learned:
- âś… How to combine small interfaces into powerful ones
- âś… The most popular interfaces everyone uses
- âś… How io.Reader and io.Writer connect everything
- âś… Rules for designing clean, flexible interfaces
Go forth and build amazing things with your new LEGO-like superpowers! 🚀
Remember: Good interfaces are like good LEGO pieces — small, simple, and they connect to everything!
