Why Go has no inheritance — rebuilding OOP with embedding and interfaces
Contents9 sections
Go has no classes and no inheritance, yet it’s hard to call it non-object-oriented. The official FAQ answers “Is Go an object-oriented language?” with “Yes and no.” This post explains why by following what Go put where inheritance used to be.
Where Go keeps the four pillars of OOP
I’d memorized the four pillars for interviews, and the mapping fell apart when I moved to Go. Encapsulation, abstraction, inheritance, polymorphism: none of the mechanisms class-based languages use for them exists in Go as-is. Every goal is still met, just by other means.
| Pillar | Class-based languages | Go |
|---|---|---|
| Encapsulation | class + private / protected |
package boundaries + identifier case. The unit is the package |
| Abstraction | abstract classes, interfaces | interfaces. There are no abstract classes |
| Inheritance | extends, type hierarchies |
none. Embedding is not inheritance |
| Polymorphism | overriding + vtables | interfaces only. There is no other path |
The first row deserves a closer look. In Go, the unit of encapsulation is the package. Inside a package, every type can see every other type’s lowercase fields. If you arrive thinking one type equals one class, that looks leaky. In practice the package boundary is the real wall, and everything inside it counts as one unit of implementation.
There’s one more mechanism that doesn’t get much attention: the internal/ directory. A package under it can only be imported from within the tree rooted at internal’s parent. Even exported names stay unusable outside the project, which makes it a layer of encapsulation one step wider than the package.
myapp/├── internal/│ └── auth/ ← importable only from under myapp/│ └── token.go exported names still can't be used by other modules├── pkg/│ └── client/ ← importable by anyone└── main.goMethods aren’t tied to classes either. You can attach methods to types that aren’t structs, including a type built on a basic type, like type Celsius float64. Go never starts from the idea that an object is an instance of a class.
The official FAQ answers “Is Go an object-oriented language?” with “Yes and no.” It has types and methods and lets you write in an object-oriented style, but there is no type hierarchy. When someone asks me, I say that only class-based inheritance is missing and the other goals are met by different tools, then walk through the tools in the table above.
Not removed, but split in two
For a long time I read “why no inheritance?” as “what did Go leave out?”, and that makes for a weak answer. More precisely, Go separated two features that inheritance used to ship as one.
Class inheritance does two different jobs at once. It hands down the parent’s implementation (code reuse), and it lets a child stand in wherever the parent is expected (polymorphism). Because the two are tied together, wanting only the implementation drags the type relationship along with it.
Some problems went away along with inheritance.
| Problem | What goes wrong |
|---|---|
| Tight coupling | Inheriting implementation binds parent and child tightly together |
| Fragile base class | Changing the parent can break every child, without knowing where the children are |
| Deep hierarchies | To find where a method came from, you have to climb the whole hierarchy |
| The diamond problem | With multiple inheritance, inheriting the same ancestor along two paths makes it unclear which one wins |
The FAQ gives one more reason, and it’s the one that stuck with me. In object-oriented languages, too much time goes into declaring and debating the relationships between types up front. Go chose to let those relationships hold automatically, without declarations. The implicit interface satisfaction from part 1 is exactly that mechanism. With no declared relationships, there’s no type hierarchy to maintain.
There is a cost: more delegation code. Using a parent method as-is, which inheritance gives you for free, has to be written out by hand. Embedding’s promotion cuts down that repetition, so it’s worth pinning down exactly what embedding is.
What embedding really is: anonymous fields and promotion
Write a type without a field name and the type’s name becomes the field name, and you can reach the inner fields and methods while skipping a level. That skipping is called promotion.
Embedded (anonymous field)
type Dog struct { Animal Breed string}- Reach it directly as
d.Name - Promoted methods join
Dog’s method set
Named field
type Dog struct { Animal Animal Breed string}- Reach it only as
d.Animal.Name Dog’s method set is unchanged
The memory layout is identical. The only difference is promotion, and promotion does two things. It shortens the name, and it puts the inner methods into the outer type’s method set. Thanks to the second, Dog satisfies every interface Animal satisfied. The abstract-class substitute later in this post depends on exactly that.
type Animal struct{ Name string }func (a Animal) Sound() string { return "..." }func (a Animal) Speak() string { return a.Name + ": " + a.Sound() }
type Dog struct { Animal // anonymous field: same layout as an Animal Animal field Breed string}func (d Dog) Sound() string { return "Woof" } // looks like an override…
d := Dog{Animal: Animal{Name: "Bori"}, Breed: "Jindo"}fmt.Println(d.Name) // Bori: promoted, short for d.Animal.Namefmt.Println(d.Sound()) // Woof: Dog's own (the shallower one wins)fmt.Println(d.Animal.Sound()) // ...: the inner method, reached directlyYou can also reach the inner value by its type name, as in d.Animal.Sound(). It’s the explicit way to call an inner method the outer type has shadowed, roughly where a super call would sit in other languages.
When names collide
There are two rules. The shallower depth wins: a name on the outer type shadows the same name inside. And two or more at the same depth are ambiguous. A common misconception is that declaring an ambiguous type is an error. It isn’t. The compile error comes only when you actually use the name.
type A struct{ Name string }type B struct{ Name string }
type C struct{ A; B; Name string } // C.Name is shallower, so it wins (shadowing)type D struct{ A; B } // this declaration compiles
var d D_ = d.Name // compile error here: ambiguous selector d.Name_ = d.A.Name // being explicit fixes itNo overriding: there’s no way back out
The line that matters most in the code above is the one it didn’t print. What does d.Speak() return?
🧪 Try it
What does d.Speak() print?
Dog redefined Sound(), so anyone used to inheritance expects Bori: Woof. I ran it.
d := Dog{Animal: Animal{Name: "Bori"}, Breed: "Jindo"}fmt.Println(d.Speak())Bori: ...Speak() is a method on Animal, and inside it the a in a.Sound() is just an Animal. It has no way of knowing a Dog is wrapped around it.
The FAQ nails this down in one sentence: the only way to get dynamically dispatched methods is through an interface, and methods on a struct or any other concrete type are always resolved statically. a.Sound() inside Animal.Speak() is fixed to Animal.Sound at compile time for the sole reason that a has type Animal. There’s never a moment at run time to ask whether it’s really a Dog. Promotion only shortens names. It doesn’t route calls back out.
So how do you design it? Flip the direction
Wanting the parent to call a method the child redefined is the template method pattern. Go can’t build that structure, so you flip it into the strategy pattern: pull the varying part out into an interface and inject it from outside.
type Sounder interface{ Sound() string }
type Animal struct { Name string S Sounder // injected from outside}func (a Animal) Speak() string { return a.Name + ": " + a.S.Sound() }
type DogSound struct{}func (DogSound) Sound() string { return "Woof" }
a := Animal{Name: "Bori", S: DogSound{}}fmt.Println(a.Speak()) // Bori: Woof, now it works as intendedFilling the gap left by abstract classes
An abstract class puts two things in one type: some behavior I implement, and some I force subclasses to provide. Go has nothing like it, so you split the two. Shared implementation comes in through embedding, and the required contract is declared as an interface.
// ① Shared implementation: embed it and get it promotedtype BaseJob struct{ ID string }func (b BaseJob) Key() string { return "job:" + b.ID }
// ② The required contract: declare it as an interfacetype Runner interface { Key() string Run(context.Context) error}
type SyncJob struct{ BaseJob } // shared part is promotedfunc (SyncJob) Run(ctx context.Context) error { return nil } // contract implemented here
var _ Runner = SyncJob{} // forget Run and this line fails to compile (see part 1)SyncJob satisfies Runner because Key() was promoted into SyncJob’s method set. This is promotion’s second job from earlier, put to work.
Embedding pitfalls in practice
The convenience comes with a clear price. If you’re writing a library in particular, know these before you embed.
| Pitfall | What happens |
|---|---|
| Internals leak into your public API | Every exported method of the embedded type becomes an exported method of yours. Removing one later breaks compatibility |
Embedding sync.Mutex |
Lock and Unlock become public API, so anyone can lock your value. A named field, mu sync.Mutex, is safer |
| Pointer-receiver methods | If you embed a type by value and its methods have pointer receivers, only *Outer satisfies the interface (method sets in part 1) |
| Name collisions | The shallower one wins; at equal depth you get an ambiguous error the moment you use it |
The first row is the one that bites most often. One convenience grows your entire API surface. So in practice, picking the methods you need and delegating them explicitly is more often the better choice. It costs a few more lines, but you decide what’s public.
// Bad: dozens of exported Client methods all become LeakyStore's APItype LeakyStore struct{ *clientv3.Client }
// Good: delegate only what you need. You decide the public surfacetype Store struct{ c *clientv3.Client }
func (s *Store) Get(ctx context.Context, key string) (*clientv3.GetResponse, error) { return s.c.Get(ctx, key)}
// Keep the lock in a named field so Lock/Unlock don't leak outtype Cache struct { mu sync.RWMutex // not embedded m map[string]int}Embedding interfaces: composition and test doubles
You can embed interfaces as well as structs, and that use is much safer. It’s how the standard library composes small interfaces. Since Go 1.14 you can also compose interfaces that share a method; before that, a duplicate method was a compile error.
type ReadWriter interface{ io.Reader; io.Writer } // compose small pieces
// Test double: embed the big interface, implement only what the test usestype fakeStore struct{ VMStore } // leave the embedded value nilfunc (f fakeStore) Get(id string) (VM, error) { return VM{ID: id}, nil }
// Calling a method you didn't implement is a call on a nil interface, so it panics.// That's not a bug; it says this test should never reach that method.Generics vs interfaces: the third axis
Generics arrived in 1.18 and added another option. At first I took them as a replacement for interfaces; using them showed me the two answer different questions. One question settles the choice: what changes from type to type?
Behavior differs → interface
- Each type has to act differently
- Polymorphism, mocking, swapping implementations
- Example: a
Notifierthat sends via Slack, email, or SMS
Only the type differs → generics
- The logic doesn’t change by a single character
- Containers,
Map,Filter - Runtime type assertions disappear and types are checked at compile time
// Generics: only the type differs, the logic is identicalfunc Map[T, U any](in []T, f func(T) U) []U { out := make([]U, 0, len(in)) for _, v := range in { out = append(out, f(v)) } return out}
doubled := Map([]int{1, 2}, func(i int) int { return i * 2 }) // [2 4]names := Map(users, func(u User) string { return u.Name })
// Interface: each type does something differenttype Notifier interface{ Notify(msg string) error } // Slack/email/SMS…Overused, though, generics make signatures hard to read fast. Introducing them once you’ve actually copy-pasted the same code two or three times worked well for me.
A word on performance. Go’s generics don’t copy the code for every type like C++ templates, and they don’t erase types and box values like Java. They sit in between: types with the same GC shape share one copy of the code, and the differences are passed in a dictionary. All pointer types share a single shape, while value types usually get code of their own. That’s why value-type generics run at essentially the speed of concrete types. When I benchmarked the three approaches, value-type generics came out nearly identical to concrete types while interfaces were 2.4x slower, and this is the reason.
Advice to reach for generics first whenever you need abstraction always puzzled me. It only clicked once I changed the question to “will I ever swap this out?” If you’ll swap implementations, use an interface; if only the type differs, use generics. If you’re writing an interface with nothing to mock, generics or a plain concrete type is probably enough.
What design patterns look like in Go
Carry a pattern over by name and it usually comes out awkward. The shapes change in Go, and the causes are everything covered so far: no inheritance means inheritance-based patterns disappear, and first-class functions mean more closure-based forms.
| Pattern | What it looks like in Go |
|---|---|
| Strategy | Interface injection. By far the most common, since template method isn’t possible and designs converge here |
| Decorator | http.Handler middleware: a closure that takes a handler and returns a wrapped one |
| Factory | A NewXxx() constructor function. A separate factory type is rarely needed |
| Builder | The functional options pattern, built from closures and variadic arguments |
| Singleton | sync.Once. Since 1.21, sync.OnceValue is shorter |
| Observer | Channels. Less need to build your own subscribe/unsubscribe machinery |
// Functional options: closures take the builder's placetype Option func(*Server)
func WithTimeout(d time.Duration) Option { return func(s *Server) { s.timeout = d } }func WithRetries(n int) Option { return func(s *Server) { s.retries = n } }
func NewServer(opts ...Option) *Server { s := &Server{timeout: 5 * time.Second} // defaults for _, o := range opts { o(s) // each option applies itself } return s}
srv := NewServer(WithTimeout(3*time.Second), WithRetries(2))
// Singleton: since 1.21, this one line does itvar conn = sync.OnceValue(func() *DB { return mustConnect() })db := conn() // call it as often as you like; it initializes onceWhen I explain a pattern, I try to start with why it ended up in that shape rather than with its name. For the strategy pattern, “template method doesn’t work, so I flipped the direction” tells you more than the name does.
Summary
- Embedding and delegation take reuse, interfaces take polymorphism, and interface injection takes extension.
- An embedded field has the same memory layout as a named one. Promotion shortens names and widens the method set, but it never routes calls back out.
- Methods on concrete types are always fixed at compile time, so when you need a template method, flip it into a strategy.
- In a library, delegate only the methods you need instead of embedding, and keep
sync.Mutexin a named field. - Interfaces when you’ll swap implementations, generics when only the type differs.
Four parts in, Go’s design has started to read as one idea. Types don’t declare their relationships to each other in advance. That single principle shows up as implicit interface satisfaction and again as the absence of type hierarchies. With no declared relationships there’s no hierarchy to manage, and when you need a relationship, you make one with an interface. Most of what first felt inconvenient follows from that.
References
- Go FAQ: Is Go an object-oriented language? · Why is there no type inheritance? · How do I get dynamic dispatch of methods?
- Effective Go: Embedding
- The Go spec: Struct types (embedded fields and promotion rules)
- Go 1.4 release notes: Internal packages
- Go 1.14 release notes (embedding interfaces with overlapping methods)
- Generics implementation: GC shape stenciling with dictionaries