Hello! Welcome to the final lesson of our course on Go interfaces.
In our previous lessons, we journeyed from the fundamental "why" of Go's interface design—its solution to tight coupling—to the practical "how" of defining interfaces and the mechanics of implicit satisfaction. We established that Go's unique approach allows an interface to be defined by the consumer of a type, focusing on behavior rather than identity.
Today, we'll build on that foundation to explore the idiomatic design patterns that emerge from this philosophy. These are the conventions and principles that guide experienced Go developers in writing clean, maintainable, and decoupled code.
Specifically, this lesson covers two central tenets of idiomatic Go interface design:
- The preference for small, single-method interfaces.
- The powerful principle of "accept interfaces, return structs."
By the end of this lesson, you will have a deep conceptual understanding of these patterns and the design rationale behind them, completing your journey into the purpose and use of interfaces in Go.
1. The Problem with "Fat" Interfaces
Developers coming from some object-oriented languages are often accustomed to defining large interfaces on the "producer" side, listing every possible action a type can perform. In Go, this is considered an anti-pattern known as "interface pollution."
Let's examine why this is problematic.
Mistakes to avoid when coming from an Object-Oriented language
To understand the idiomatic approach, we first need to see what to avoid. This article, 'Mistakes to avoid when coming from an Object-Oriented language,' clearly illustrates the problem with large, producer-defined interfaces.
Please read the section titled 'But interfaces aren’t easy!'. Focus on the MessageStore interface example. Notice how large it is and how the PrintMessages consumer only needs a fraction of its methods, yet is coupled to the entire definition.
As the article points out, the PrintMessages function is now tightly coupled to a large, complex contract it doesn't fully need. This forces any user of the function to depend on methods like Sort(), Clear(), and Add(), even if they are irrelevant. This creates unnecessary dependencies and makes the system harder to test and maintain.
2. The Solution: The Interface Segregation Principle
The solution to interface pollution is rooted in a well-established software engineering concept: the Interface Segregation Principle (ISP). It's one of the five SOLID principles and states that "clients should not be forced to depend on methods they do not use."
Go's design makes applying this principle natural. Because interfaces are satisfied implicitly and defined by the consumer, you are encouraged to define an interface that asks for only the behavior you need.
The following article provides a masterful, step-by-step demonstration of applying this principle to refine a function's dependency from a large, concrete type to the smallest possible interface.
Dave Cheney's article 'SOLID Go Design' offers one of the clearest explanations of applying the Interface Segregation Principle in Go. It's a perfect illustration of the thought process behind idiomatic interface design.
Read the section titled 'Interface Segregation Principle'. Follow the author's reasoning as he refactors the Save function's signature. Notice how the dependency is progressively refined from a concrete *os.File to io.ReadWriteCloser, then to io.WriteCloser, and finally to the beautifully simple io.Writer. This process is the very essence of designing with small interfaces.
This progressive refinement is key. By asking for an io.Writer instead of an *os.File, the Save function becomes:
- More general: It can now write to a file, a network connection, an in-memory buffer, or anything else that satisfies the
io.Writerinterface. - More honest: Its signature clearly communicates its only requirement: a destination to write to.
- More decoupled: It is no longer tied to the filesystem or the
ospackage.
This leads directly to the Go idiom of preferring small, often single-method, interfaces. As Rob Pike, one of Go's creators, stated, "The bigger the interface, the weaker the abstraction." A small interface like io.Reader is a powerful abstraction because many types can provide that simple behavior. A large, bespoke interface is weak because very few types can satisfy its long list of demands.
3. The Power of Small Interfaces: Composition
This preference for small interfaces isn't just about minimalism; it's about enabling composition. By breaking down behaviors into their smallest components, we can combine them in new and powerful ways.
This GopherCon talk by Tomas Senart explores the philosophy behind this, linking small interfaces to composability and extensibility.
GopherCon 2015: Embrace the Interface - Tomas Senart
In 'Embrace the Interface,' Tomas Senart argues that small interfaces are the key to building composable systems in Go. This talk provides the conceptual 'why' behind the power of this pattern.
Please watch these two key segments: 07:07 - 09:44: The speaker introduces the Interface Segregation Principle and makes the case for small, precise, single-method interfaces as the most expressive tool of abstraction in Go. 18:37 - 21:52: Here, he demonstrates the power of this approach. Watch how multiple, independent behaviors (authorization, load balancing, logging, instrumentation) are composed onto a client. This is all made possible because they operate on a very small, simple interface. Focus on how these orthogonal concerns are layered together.
The decorator pattern shown in the video is a prime example. Because the core functionality only depends on a minimal interface, you can "wrap" it with additional behaviors (like logging or metrics) without ever modifying the original code. This adheres to the Open/Closed Principle—"open for extension, but closed for modification"—and is a hallmark of robust software design.
4. The Idiom: "Accept Interfaces, Return Structs"
The principles we've discussed culminate in a famous Go aphorism: "Accept interfaces, return structs."
You've already seen the first part in action: the Save function was refactored to accept the io.Writer interface. The second part—"return structs"—is about giving the caller maximum flexibility.
This video provides a concise and practical demonstration of both sides of this idiom.
Interfaces in Go - Discovering Behavior
The video 'Interfaces in Go - Discovering Behavior' by Matthew Sanabria offers a clear, code-driven explanation of this core Go idiom.
Watch these two short clips: 06:36 - 07:44 ('Accept interfaces'): Notice how the sendNotification function is changed to accept a slice of Notifier interfaces. This decouples the function from concrete DiscordClient or EmailClient types. 13:00 - 13:57 ('Return structs'): This explains the other half of the idiom. The key idea is that a constructor or factory function should return a concrete type (a struct), not an interface. This avoids constraining the type too early.
Let's break down the reasoning:
- Accept Interfaces: As we've extensively covered, this decouples your function from specific implementations. It depends only on the behavior it needs, making it more reusable and easier to test.
- Return Structs: When a function creates a new value (e.g., a constructor like
NewDiscordClient), returning the concrete struct gives the caller the complete object with all its fields and methods. The caller then has the choice to use the full object or to immediately assign it to an interface variable that suits their specific needs. If you return an interface, you force a narrow view of the object onto the caller, taking away their flexibility.
5. Embracing the Go Philosophy
These idiomatic patterns are the natural outcome of Go's core design philosophy, which we explored in our first lesson. Traditional OO often focuses on identity (a Manager is a type of Employee). Go, with its implicit interfaces, focuses on behavior (this value behaves like an io.Writer).
This final reading selection masterfully summarizes how this philosophical distinction leads directly to the patterns we've learned today.
Mistakes to avoid when coming from an Object-Oriented language
To bring everything together, let's revisit the core difference between Go's philosophy and traditional OO. This section of the Thoughtworks article explains how Go's focus on behavior naturally leads to the small, consumer-defined interfaces we've been discussing.
Read the section 'Embracing the Go philosophy'. Pay close attention to the contrast it draws between identity and behavior. This is the central point that underpins all of Go's interface design patterns.
Conclusion
In this final lesson, we've synthesized everything we've learned to understand the idiomatic application of Go's interfaces. We've seen that the most effective and maintainable Go code follows patterns that stem directly from the language's core design.
Key Takeaways:
- Prefer Small Interfaces: Avoid "fat" interfaces. Define interfaces with the minimum number of methods required by the consumer, guided by the Interface Segregation Principle. Single-method interfaces are common and powerful.
- Small Interfaces Enable Composition: By defining minimal behavioral contracts, you can build complex systems by composing simple, decoupled components.
- Accept Interfaces, Return Structs: This is the quintessential Go idiom for function signatures. It maximizes decoupling and flexibility by having functions depend on abstract behaviors while providing callers with concrete, fully-featured types.
- Behavior over Identity: All these patterns are a consequence of Go's focus on what a type can do, not what it is.
This concludes our course. We have journeyed from the problem of software coupling to Go's philosophical answer in implicit interfaces, explored the mechanics of their implementation, and finally, uncovered the idiomatic patterns that bring it all to life. You now possess a solid conceptual framework for understanding, designing, and using interfaces effectively in Go.