Interface Patterns

Back

Loading concept...

đź§© 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:

  1. Interface Composition - Combining small LEGO sets into bigger ones
  2. Common Interfaces - The most popular LEGO pieces everyone uses
  3. io.Reader and io.Writer - Two magical pipes for moving data
  4. 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:

  1. You give it an empty bucket (p []byte)
  2. It fills the bucket with data
  3. It tells you how much it filled (n)
  4. 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:

  1. You give it a bucket full of data (p []byte)
  2. It takes the data somewhere (file, network, etc.)
  3. It tells you how much it took (n)
  4. 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:

  1. âś… How to combine small interfaces into powerful ones
  2. âś… The most popular interfaces everyone uses
  3. âś… How io.Reader and io.Writer connect everything
  4. âś… 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!

Loading story...

Story - Premium Content

Please sign in to view this story and start learning.

Upgrade to Premium to unlock full access to all stories.

Stay Tuned!

Story is coming soon.

Story Preview

Story - Premium Content

Please sign in to view this concept and start learning.

Upgrade to Premium to unlock full access to all content.