1. Why “glue” interfaces at all
When you’re just starting out programming, you want everything to be “in one place”: one type—and it can do everything. But as the program grows, that turns into a combine harvester that brews coffee and grows bugs. That’s why Go usually moves from small pieces of behavior to larger ones, and sometimes it’s convenient to assemble those pieces together—not via inheritance, but via contract composition.
Imagine an interface as a contract: “if you promise methods A and B, you can be used here.” But there are situations where we need the contract “A and B together.” That’s where interface composition appears: we build a large contract from small ones without rewriting the method list by hand every time.
Minimal mental model of an interface
To understand interface composition, we don’t need to dive into the details of how interfaces are laid out in memory, what nil traps exist, and why interface != nil sometimes “is not what it seems.” Today, a very simple working model is enough: an interface is a type defined by a set of method signatures, and any type with those methods “fits.”
The main benefit of an interface for the reader of the code: it describes behavior, not data. An interface has no fields; it doesn’t store “state.” It says: “I can do this.” And if your code contains a variable of an interface type, it almost always means “I don’t care what’s inside, as long as it can perform the required methods.”
2. Interface composition
Embedding interfaces: a “sum” of methods
An interface in Go can include (embed) another interface, and that means: “the new interface contains all methods from the embedded interface plus my own methods.” In other words, it’s literally a “sum of methods”: instead of copying signatures, we include a ready-made chunk of the contract.
This is official, idiomatic language syntax: inside interface { ... } you can list not only methods, but also the names of other interfaces—assembling a contract from parts.
To keep it as grounded as possible, here’s a small example of “how it’s written” (no deep analysis—just the syntax fact):
package main
type Reader interface {
Read() string
}
type Writer interface {
Write(s string)
}
type ReadWriter interface {
Reader
Writer
}
func main() {}
Here ReadWriter requires methods Read() and Write(string). The important point is that this is not “inheritance magic” and not “a class got extended”: it’s just a compact way to write a contract.
Equivalent form without embedding
Embedding interfaces can look like a “superpower of the language,” but in practice it’s more about syntactic convenience: you could write the same thing by hand—there would just be more characters. It’s useful to say this out loud so your brain doesn’t draw extra magic where there isn’t any (brains love doing that—especially on Monday morning).
Let’s compare “with embedding” and “without embedding” on the same example: the meaning is the same; only the form differs.
package main
type ReadWriter interface {
Read() string
Write(s string)
}
func main() {}
So why do we need embedding at all? Because in a real project, small interfaces get reused: the same Reader might be needed in ten places. And if you copy Read() string by hand into ten interfaces, one day you’ll want to “tweak the contract a bit”—and you’ll start a fun quest: “find 10 places and don’t mess up.” Embedding saves not only lines, but also the number of reasons to introduce bugs.
Mini example: TaskStore in a learning task tracker
To see the point of interface composition in live code, imagine our learning project as a small task tracker: we have tasks (Task), and some part of the program can add them, while another part can show the list. Today we’re not building a layered architecture and not discussing “how to design interfaces properly” in depth; we only care about the fact of composing contracts.
Let’s make two small interfaces and “assemble” a third:
package main
type Task struct {
Title string
Done bool
}
type TaskAdder interface {
Add(t Task) error
}
type TaskLister interface {
List() []Task
}
type TaskStore interface {
TaskAdder
TaskLister
}
func main() {}
The idea is simple: TaskAdder is the contract “I can add a task,” TaskLister is the contract “I can list tasks,” and TaskStore is “I can do both.”
In a real application this helps separate responsibilities: one function can accept only TaskLister (it doesn’t need to add), and another only TaskAdder. And where you need everything at once, you take TaskStore.
A small diagram to make it sit in your head like “LEGO,” not like “abstract abstraction”:
flowchart TD A["TaskAdder: Add(Task) error"] --> C[TaskStore] B["TaskLister: List() []Task"] --> C[TaskStore] C --> D[Code that needs both adding and reading]
Limitation: you can’t “glue” together contradictory methods
Interface composition may sound like “let’s combine everything with everything,” but the language has common sense: if you try to assemble a contract that contradicts itself, the compiler will stop you. This isn’t being picky; it’s protection. Otherwise you could create an interface that nobody can satisfy, or the satisfaction would be ambiguous.
The most common cause is the same method name with different signatures. For a beginner it looks like “why can’t I, I just merged two interfaces,” but at the contract level it means “I require method Do to accept both int and string at the same time”—which is impossible.
package main
type A interface {
Do(x int) int
}
type B interface {
Do(x string) string
}
// type Bad interface { A; B } // not allowed: name Do conflicts with a different signature
func main() {}
The compiler is doing you a favor in these cases: it won’t let you write a contract that can’t be implemented in a reasonable way.
4. Typical mistakes
Mistake #1: treating interface embedding as “inheritance.”
When you see type X interface { A; B }, your hand may reach for the familiar OOP picture: like X “inherited” A and B. But there is no object here, no fields, no base class. It’s just a record: “X requires all methods from A and B.” If you keep the word “contract” in mind, there’s less confusion.
Mistake #2: copying methods by hand instead of composing—and then suffering during changes.
Beginners often think “it’s only two methods, I’ll just rewrite them,” then a third appears, a fourth… and a month later one interface ReadWriter suddenly differs from another ReadWriter2 by one parameter. Embedding isn’t “for smart people”; it’s for people who don’t want to force themselves into manual refactoring for no reason.
Mistake #3: trying to “glue” interfaces with conflicting signatures and being surprised by a compiler error.
If two interfaces require a method with the same name but different parameters/results, that’s not a “Go weirdness,” it’s logical incompatibility of requirements. A simple habit helps: when you merge contracts, say out loud “I require methods…” and check whether any requirements are mutually exclusive.
Mistake #4: making a huge interface “just in case.”
Interface composition is convenient, but you can turn it into a monster: one interface embeds five others, those embed five more, and in the end you have a 30-method contract that’s hard to implement and even harder to understand. Even if the syntax allows it, readability usually benefits from small contracts and careful assembly only where you truly need “both this and that.”
GO TO FULL VERSION