DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Contexts in Go: A Comprehensive Guide
  • Event-Driven Pipelines With Apache Pulsar and Go
  • Beyond SOLID: Embracing CUPID for Modern Software Craftsmanship
  • Beyond Conversation: Mastering Context with Claude Code Skills and Agents

Trending

  • Compliance Automated Standard Solution (COMPASS), Part 11: Compliance as Code, the OSCAL MCP Server Way
  • The Hidden Cost of AI Tokens: Engineering Patterns for 10x Resource Efficiency
  • Building a High-Throughput Distributed Sequence Generator Using the Hi-Lo Algorithm
  • Slopsquatting: Building a Scanner That Catches AI-Hallucinated Packages Before They Reach Production
  1. DZone
  2. Coding
  3. Languages
  4. Clean Code: Interfaces in Go — Why Small Is Beautiful, Part 3

Clean Code: Interfaces in Go — Why Small Is Beautiful, Part 3

Keep interfaces to 1–3 methods, define them on the consumer side, accept interfaces but return concrete types, and never confuse nil pointer with nil interface.

By 
Vladimir Yakovlev user avatar
Vladimir Yakovlev
·
Apr. 28, 26 · Tutorial
Likes (1)
Comment
Save
Tweet
Share
2.0K Views

Join the DZone community and get the full member experience.

Join For Free

Introduction: Interfaces — Go's Secret Weapon

I've watched teams create 20-method interfaces that become impossible to test, mock, or maintain. Then they wonder why Go feels clunky. "Accept interfaces, return structs" — if you've heard only one Go idiom, it's probably this one. But why is it so important? And why are single-method interfaces the norm in Go, not the exception?

Common interface mistakes I've encountered:

  • Interfaces with 10+ methods: ~45% of enterprise Go code
  • Defining interfaces at the implementation site: ~70% of packages
  • Returning interfaces instead of concrete types: ~55% of functions
  • Using empty interface{} everywhere: ~30% of APIs
  • nil interface vs nil pointer confusion: ~25% of subtle bugs

After eight years of working with Go and debugging countless interface-related issues, I can say: proper use of interfaces is the difference between code that fights the language and code that flows like water.

Interface Satisfaction: Duck Typing for Adults

In Go, a type satisfies an interface automatically, without explicit declaration:

Go
 
// Interface defines a contract
type Writer interface {
    Write([]byte) (int, error)
}

// File satisfies Writer automatically
type File struct {
    path string
}

func (f *File) Write(data []byte) (int, error) {
    // implementation
    return len(data), nil
}

// Buffer also satisfies Writer
type Buffer struct {
    data []byte
}

func (b *Buffer) Write(data []byte) (int, error) {
    b.data = append(b.data, data...)
    return len(data), nil
}

// Function accepts interface
func SaveLog(w Writer, message string) error {
    _, err := w.Write([]byte(message))
    return err
}

// Usage - works with any Writer
file := &File{path: "/var/log/app.log"}
buffer := &Buffer{}

SaveLog(file, "Writing to file")    // OK


Small Interfaces: The Power of Simplicity

The Single Method Rule

Look at Go's standard library:

Go
 
type Reader interface {
    Read([]byte) (int, error)
}

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

type Closer interface {
    Close() error
}

type Stringer interface {
    String() string


One method — one interface. Why?

Go
 
// BAD: large interface
type Storage interface {
    Save(key string, data []byte) error
    Load(key string) ([]byte, error)
    Delete(key string) error
    List(prefix string) ([]string, error)
    Exists(key string) bool
    Size(key string) (int64, error)
    LastModified(key string) (time.Time, error)
}

// Problem: what if you only need Save/Load?
// You'll have to implement ALL methods!

// GOOD: small interfaces
type Reader interface {
    Read(key string) ([]byte, error)
}

type Writer interface {
    Write(key string, data []byte) error
}

type Deleter interface {
    Delete(key string) error
}

// Interface composition
type ReadWriter interface {
    Reader
    Writer
}

type Storage interface {
    ReadWriter
    Deleter
}

// Now functions can require only what they need
func BackupData(r Reader, keys []string) error {
    for _, key := range keys {
        data, err := r.Read(key)
        if err != nil {
            return fmt.Errorf("read %s: %w", key, err)
        }
        // backup process
    }
    return nil
}

// Function requires minimum - only Reader, not entire Storage


Interface Segregation Principle in Action

Go
 
// Instead of one monstrous interface
type HTTPClient interface {
    Get(url string) (*Response, error)
    Post(url string, body []byte) (*Response, error)
    Put(url string, body []byte) (*Response, error)
    Delete(url string) (*Response, error)
    Head(url string) (*Response, error)
    Options(url string) (*Response, error)
    Patch(url string, body []byte) (*Response, error)
}

// Create focused interfaces
type Getter interface {
    Get(url string) (*Response, error)
}

type Poster interface {
    Post(url string, body []byte) (*Response, error)
}

// Function requires only what it uses
func FetchUser(g Getter, userID string) (*User, error) {
    resp, err := g.Get("/users/" + userID)
    if err != nil {
        return nil, fmt.Errorf("fetch user %s: %w", userID, err)
    }
    // parse response
    return parseUser(resp)
}

// Testing becomes easier
type mockGetter struct {
    response *Response
    err      error
}

func (m mockGetter) Get(url string) (*Response, error) {
    return m.response, m.err
}

// Only need to mock one method, not entire HTTPClient!


Accept Interfaces, Return Structs

Why This Matters

Go
 
// BAD: returning interface
func NewLogger() Logger { // Logger is interface
    return &FileLogger{
        file: os.Stdout,
    }
}

// Problems:
// 1. Hides actual type
// 2. Loses access to type-specific methods
// 3. Complicates debugging

// GOOD: return concrete type
func NewLogger() *FileLogger { // concrete type
    return &FileLogger{
        file: os.Stdout,
    }
}

// But ACCEPT interface
func ProcessData(logger Logger, data []byte) error {
    logger.Log("Processing started")
    // processing
    logger.Log("Processing completed")
    return nil
}


Practical Example

Go
 
// Repository returns concrete types
type UserRepository struct {
    db *sql.DB
}

func NewUserRepository(db *sql.DB) *UserRepository {
    return &UserRepository{db: db}
}

func (r *UserRepository) FindByID(id string) (*User, error) {
    // SQL query
    return &User{}, nil
}

func (r *UserRepository) Save(user *User) error {
    // SQL query
    return nil
}

// Service accepts interfaces
type UserFinder interface {
    FindByID(id string) (*User, error)
}

type UserSaver interface {
    Save(user *User) error
}

type UserService struct {
    finder UserFinder
    saver  UserSaver
}

func NewUserService(finder UserFinder, saver UserSaver) *UserService {
    return &UserService{
        finder: finder,
        saver:  saver,
    }
}

// Easy to test - can substitute mocks
type mockFinder struct {
    user *User
    err  error
}

func (m mockFinder) FindByID(id string) (*User, error) {
    return m.user, m.err
}

func TestUserService(t *testing.T) {
    mock := mockFinder{
        user: &User{Name: "Test"},
    }
    
    service := NewUserService(mock, nil)
    // test with mock
}


Interface Composition

Embedding Interfaces

Go
 
// Base interfaces
type Reader interface {
    Read([]byte) (int, error)
}

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

type Closer interface {
    Close() error
}

// Composition through embedding
type ReadWriter interface {
    Reader
    Writer
}

type ReadWriteCloser interface {
    Reader
    Writer
    Closer
}

// Or more explicitly
type ReadWriteCloser interface {
    Read([]byte) (int, error)
    Write([]byte) (int, error)
    Close() error
}


Type Assertions and Type Switches

Go
 
// Type assertion - check concrete type
func ProcessWriter(w io.Writer) {
    // Check if Writer also supports Closer
    if closer, ok := w.(io.Closer); ok {
        defer closer.Close()
    }
    
    // Check for buffering
    if buffered, ok := w.(*bufio.Writer); ok {
        defer buffered.Flush()
    }
    
    w.Write([]byte("data"))
}

// Type switch - handle different types
func Describe(i interface{}) string {
    switch v := i.(type) {
    case string:
        return fmt.Sprintf("String of length %d", len(v))
    case int:
        return fmt.Sprintf("Integer: %d", v)
    case fmt.Stringer:
        return fmt.Sprintf("Stringer: %s", v.String())
    case error:
        return fmt.Sprintf("Error: %v", v)
    default:
        return fmt.Sprintf("Unknown type: %T", v)
    }
}


nil Interfaces: The Gotchas

Go
 
// WARNING: classic mistake
type MyError struct {
    msg string
}

func (e *MyError) Error() string {
    return e.msg
}

func doSomething() error {
    var err *MyError = nil
    // some logic
    return err // RETURNING nil pointer
}

func main() {
    err := doSomething()
    if err != nil { // TRUE! nil pointer != nil interface
        fmt.Println("Got error:", err)
    }
}

// CORRECT: explicitly return nil
func doSomething() error {
    var err *MyError = nil
    // some logic
    if err == nil {
        return nil // return nil interface
    }
    return err
}


Checking for nil

Go
 
// Safe nil check for interface
func IsNil(i interface{}) bool {
    if i == nil {
        return true
    }
    
    // Check if value inside interface is nil
    value := reflect.ValueOf(i)
    switch value.Kind() {
    case reflect.Ptr, reflect.Map, reflect.Slice, reflect.Chan, reflect.Func:
        return value.IsNil()
    }
    return false
}


Real Examples From Standard Library

io.Reader/Writer — Foundation of Everything

Go
 
// Copy between any Reader and Writer
func Copy(dst io.Writer, src io.Reader) (int64, error)

// Works with files
file1, _ := os.Open("input.txt")
file2, _ := os.Create("output.txt")
io.Copy(file2, file1)

// Works with network
conn, _ := net.Dial("tcp", "example.com:80")
io.Copy(conn, strings.NewReader("GET / HTTP/1.0\r\n\r\n"))

// Works with buffers
var buf bytes.Buffer
io.Copy(&buf, file1)


http.Handler — Web Server in One Method

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

// Any type with ServeHTTP method can be a handler
type MyAPI struct {
    db Database
}

func (api MyAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    switch r.URL.Path {
    case "/users":
        api.handleUsers(w, r)
    case "/posts":
        api.handlePosts(w, r)
    default:
        http.NotFound(w, r)
    }
}

// HandlerFunc - adapter for regular functions
type HandlerFunc func(ResponseWriter, *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r) // call the function
}

// Now regular function can be a handler!
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, World!")
})


Patterns and Anti-Patterns

Pattern: Conditional Interface Implementation

Go
 
// Optional interfaces for extending functionality
type Optimizer interface {
    Optimize() error
}

func ProcessData(w io.Writer, data []byte) error {
    // Basic functionality
    if _, err := w.Write(data); err != nil {
        return err
    }
    
    // Optional optimization
    if optimizer, ok := w.(Optimizer); ok {
        return optimizer.Optimize()
    }
    
    return nil
}


Anti-Pattern: Overly Generic Interfaces

Go
 
// BAD: interface{} everywhere
func Process(data interface{}) interface{} {
    // type assertions everywhere
    switch v := data.(type) {
    case string:
        return len(v)
    case []byte:
        return len(v)
    default:
        return nil
    }
}

// GOOD: specific interfaces
type Sized interface {
    Size() int
}

func Process(s Sized) int {
    return s.Size()
}


Practical Tips

  1. Define interfaces on the consumer side, not implementation.
  2. Prefer small interfaces to large ones.
  3. Use embedding for interface composition.
  4. Don't return interfaces without necessity.
  5. Remember nil interface and nil pointer.
  6. Use type assertions carefully.
  7. interface{} is a last resort, not a first.

Interface Checklist

  • Interface has 1-3 methods maximum
  • Interface defined near usage
  • Functions accept interfaces, not concrete types
  • Functions return concrete types, not interfaces
  • No unused methods in interfaces
  • Type assertions handle both cases (ok/not ok)
  • interface{} used only where necessary

Conclusion

Interfaces are the glue that holds Go programs together. They enable flexible, testable, and maintainable code without complex inheritance hierarchies. Remember: in Go, interfaces are implicit, small, and composable.

In the next article, we'll discuss packages and dependencies: how to organize code so the import graph is flat and dependencies are unidirectional.

What's your take on interface design? How small is too small? How do you decide when to create an interface vs using a concrete type? Share your experience in the comments!

Go (programming language) Interface (computing)

Opinions expressed by DZone contributors are their own.

Related

  • Contexts in Go: A Comprehensive Guide
  • Event-Driven Pipelines With Apache Pulsar and Go
  • Beyond SOLID: Embracing CUPID for Modern Software Craftsmanship
  • Beyond Conversation: Mastering Context with Claude Code Skills and Agents

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook