Composition Over Inheritance
Go has no classes and no inheritance. Instead of building deep type hierarchies, Go uses struct embedding and interface composition to share behavior. This design choice eliminates entire categories of problems — no diamond inheritance, no fragile base class issues, no confusion about which method gets called. Composition produces flatter, more explicit code.
No Classes, No Inheritance
In languages like Java or Python, you might write:
Animal (base class)
-> Dog (subclass)
-> GuideDog (subclass)
In Go, this hierarchy does not exist. There is no extends keyword. Types are not related by a parent-child tree. Instead, you build up behavior by combining small pieces.
Struct Embedding
Embedding places one struct inside another, promoting the inner struct's fields and methods to the outer struct.
type Address struct {
Street string
City string
State string
}
func (a Address) FullAddress() string {
return a.Street + ", " + a.City + ", " + a.State
}
type Employee struct {
Name string
Address
}
func main() {
e := Employee{
Name: "Alice",
Address: Address{
Street: "123 Main St",
City: "Portland",
State: "OR",
},
}
// Address methods are promoted to Employee
fmt.Println(e.FullAddress())
// Fields are also promoted
fmt.Println(e.City)
}
123 Main St, Portland, OR
Portland
The Employee type does not inherit from Address. It contains an Address and the compiler promotes its fields and methods for convenience.
Promotion Rules
When a struct embeds another type, the embedded type's exported fields and methods are accessible directly on the outer type. The key rules are:
type Base struct {
ID int
}
func (b Base) Describe() string {
return fmt.Sprintf("ID: %d", b.ID)
}
type Extended struct {
Base
Label string
}
func main() {
e := Extended{Base: Base{ID: 42}, Label: "test"}
// Promoted method
fmt.Println(e.Describe())
// Promoted field
fmt.Println(e.ID)
// You can still access the embedded struct directly
fmt.Println(e.Base.ID)
}
ID: 42
42
42
If the outer struct defines a method with the same name as the embedded type, the outer method takes precedence.
type Extended struct {
Base
Label string
}
func (e Extended) Describe() string {
return fmt.Sprintf("Extended: %s (%s)", e.Label, e.Base.Describe())
}
func main() {
e := Extended{Base: Base{ID: 42}, Label: "test"}
fmt.Println(e.Describe())
}
Extended: test (ID: 42)
If two embedded types have a method with the same name, and the outer struct does not define its own, the compiler reports an ambiguity error at the call site.
Interface Embedding
Interfaces can embed other interfaces to compose larger contracts from smaller ones.
type Reader interface {
Read(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
type ReadCloser interface {
Reader
Closer
}
This is how the standard library builds up io.ReadWriter, io.ReadCloser, and io.ReadWriteCloser from single-method interfaces.
You can also embed interfaces in structs. This is useful for partial implementation or test doubles.
type MockDB struct {
io.ReadCloser // embed the interface
records []string
}
Any MockDB value must have its ReadCloser field set (or the methods will panic on nil). This technique is more commonly used in testing to satisfy an interface while only implementing the methods you actually call.
Why Composition Wins
Flatter Hierarchies
Inheritance creates deep trees. Composition creates flat structures. When you read a Go struct, you see exactly what it contains — no need to trace up through parent classes to understand its behavior.
// Everything this type can do is visible right here
type OrderService struct {
repo OrderRepository
mailer EmailSender
logger *slog.Logger
}
No Diamond Problem
In languages with multiple inheritance, the diamond problem creates ambiguity about which parent's method to call. Go does not have this problem because there is no inheritance. Embedding is explicit, and ambiguities are caught at compile time.
Easier to Understand
With inheritance, calling a method might execute code defined three levels up in the hierarchy. With composition, the code path is direct. If OrderService calls s.repo.Save(order), you know exactly where Save lives.
Flexible Combination
You can embed any number of types and combine any set of interfaces. There are no restrictions about single vs multiple inheritance.
type Server struct {
*http.ServeMux
*slog.Logger
config Config
db *sql.DB
}
Designing with Small Interfaces & Struct Embedding
The Go design pattern combines small interfaces with struct embedding to build flexible, testable systems.
// Small, focused interfaces
type UserFinder interface {
FindUser(id int) (User, error)
}
type UserSaver interface {
SaveUser(user User) error
}
type UserDeleter interface {
DeleteUser(id int) error
}
// Compose when you need multiple capabilities
type UserStore interface {
UserFinder
UserSaver
UserDeleter
}
// A concrete implementation satisfies all three
type PostgresUserStore struct {
db *sql.DB
}
func (s *PostgresUserStore) FindUser(id int) (User, error) {
var u User
err := s.db.QueryRow("SELECT id, name FROM users WHERE id = $1", id).
Scan(&u.ID, &u.Name)
return u, err
}
func (s *PostgresUserStore) SaveUser(user User) error {
_, err := s.db.Exec("INSERT INTO users (id, name) VALUES ($1, $2)",
user.ID, user.Name)
return err
}
func (s *PostgresUserStore) DeleteUser(id int) error {
_, err := s.db.Exec("DELETE FROM users WHERE id = $1", id)
return err
}
Functions that only need to find users accept UserFinder. Functions that only need to save accept UserSaver. Only functions that need everything accept UserStore.
// This function only needs to find users — its dependency is minimal
func GetUserProfile(finder UserFinder, id int) (Profile, error) {
user, err := finder.FindUser(id)
if err != nil {
return Profile{}, err
}
return Profile{Name: user.Name}, nil
}
This makes testing straightforward. You implement only the interface you need.
type mockFinder struct {
user User
err error
}
func (m *mockFinder) FindUser(id int) (User, error) {
return m.user, m.err
}
func TestGetUserProfile(t *testing.T) {
finder := &mockFinder{user: User{ID: 1, Name: "Alice"}}
profile, err := GetUserProfile(finder, 1)
if err != nil {
t.Fatal(err)
}
if profile.Name != "Alice" {
t.Errorf("got %q, want %q", profile.Name, "Alice")
}
}
Common Pitfalls
- Embedding for code reuse alone. Embedding promotes all exported methods, not just the ones you want. If you only need one method from the embedded type, use a named field and call the method explicitly.
- Confusing embedding with inheritance. An
Employeethat embedsAddressis not a subtype ofAddress. You cannot pass anEmployeeto a function expecting anAddress. - Accidentally exposing methods. Embedding a type promotes all of its exported methods. If the embedded type has methods you do not want on the outer type, use a named field instead.
- Nil embedded interfaces. Embedding an interface in a struct means calling an unset method will panic. Only use this pattern deliberately, typically in tests.
- Deep embedding chains. Embedding a struct that itself embeds another struct creates promotion chains that are hard to follow. Keep embedding shallow.
Key Takeaways
- Go has no classes and no inheritance — use composition instead.
- Struct embedding promotes fields and methods from the inner type to the outer type.
- The outer type's own methods take precedence over promoted methods.
- Interface embedding composes larger interfaces from smaller ones.
- Small interfaces (1-2 methods) are the building blocks of flexible Go designs.
- Composition produces flatter, more explicit, and more testable code than inheritance hierarchies.
- Accept small interfaces, embed structs for reuse, and keep your type relationships shallow.