Go achieves code reuse through composition rather than inheritance. Two mechanisms support this: interface embedding (combining interfaces into larger ones) and struct embedding (promoting a type’s fields and methods to the outer struct). Both make types interoperable without explicit declarations, and both appear throughout the standard library.
For interface definition fundamentals see Go interfaces definition. For struct composition basics see Go structs composition.
Interface Embedding
Embedding one interface inside another creates a composed interface that requires all methods of both:
// Three focused interfaces from io package
type Reader interface { Read(p []byte) (n int, err error) }
type Writer interface { Write(p []byte) (n int, err error) }
type Closer interface { Close() error }
// Composed — requires both Read and Write
type ReadWriter interface {
Reader
Writer
}
// Composed with three interfaces
type ReadWriteCloser interface {
Reader
Writer
Closer
}
Any type that implements Read, Write, and Close satisfies ReadWriteCloser automatically — without knowing that ReadWriteCloser exists. os.File satisfies all of these. So does net.Conn. So does tls.Conn.
This is why io.Copy(dst io.Writer, src io.Reader) works with files, network connections, HTTP bodies, compressed streams, and anything else — they all satisfy the minimal interface it requires.
Building Your Own Composed Interfaces
Define composed interfaces when a function genuinely needs multiple capabilities:
// A data store that both reads and persists
type UserStore interface {
GetUser(ctx context.Context, id string) (*User, error)
ListUsers(ctx context.Context, filter UserFilter) ([]User, error)
}
type UserWriter interface {
CreateUser(ctx context.Context, u *User) error
UpdateUser(ctx context.Context, u *User) error
DeleteUser(ctx context.Context, id string) error
}
// Combined for contexts that need full access
type UserRepository interface {
UserStore
UserWriter
}
// Admin service needs full access
type AdminService struct {
repo UserRepository
}
// Read-only report service needs only UserStore
type ReportService struct {
store UserStore // narrower dependency — easier to mock, clearer intent
}
ReportService only declares what it needs — a UserStore. The test mock only has to implement GetUser and ListUsers. The production PostgresUserRepository can implement all five methods and satisfy both interfaces.
Struct Embedding and Method Promotion
When you embed a struct (anonymous field), its methods and fields are promoted to the outer struct — accessible directly without qualifying the field name:
type Logger struct {
prefix string
}
func (l *Logger) Info(msg string) {
fmt.Printf("[INFO] %s %s\n", l.prefix, msg)
}
func (l *Logger) Error(msg string) {
fmt.Printf("[ERROR] %s %s\n", l.prefix, msg)
}
// UserService embeds Logger — inherits Info and Error
type UserService struct {
Logger // embedded — no field name
db *sql.DB
}
svc := &UserService{
Logger: Logger{prefix: "user-service"},
db: db,
}
svc.Info("starting") // calls svc.Logger.Info("starting")
svc.Error("db failed") // calls svc.Logger.Error("db failed")
The embedded Logger is still accessible by its type name when needed: svc.Logger.Info(...). But the promotion means you can call svc.Info directly — cleaner when the embedding relationship is “a UserService uses Logger” (not “is a Logger”).
Method Promotion and Interface Satisfaction
Promoted methods count toward interface satisfaction. If Logger satisfies some interface, and UserService embeds Logger, then UserService satisfies that interface too:
type Outputter interface {
Info(string)
Error(string)
}
var _ Outputter = (*Logger)(nil) // Logger satisfies Outputter
var _ Outputter = (*UserService)(nil) // UserService also satisfies Outputter (via embedding)
This lets UserService be passed anywhere an Outputter is expected, without explicitly implementing the interface methods.
The Wrapper Pattern: Embedding for Decoration
Embedding an interface in a struct creates a transparent wrapper — the struct forwards all interface method calls to the embedded value, and you override only the methods you care about:
// BaseLogger is an interface
type Logger interface {
Info(msg string, args ...any)
Error(msg string, args ...any)
Debug(msg string, args ...any)
}
// SampledLogger wraps Logger and samples debug logs (only logs 1 in N)
type SampledLogger struct {
Logger // embed the interface — all methods forwarded automatically
sampleRate int
count atomic.Int64
}
// Override only Debug — Info and Error are forwarded to the embedded Logger
func (s *SampledLogger) Debug(msg string, args ...any) {
if s.count.Add(1)%int64(s.sampleRate) == 0 {
s.Logger.Debug(msg, args...) // call through to the real logger
}
// otherwise drop the debug log
}
// Usage
realLogger := slog.Default()
sampledLogger := &SampledLogger{Logger: realLogger, sampleRate: 100}
sampledLogger.Info("request") // forwarded to realLogger.Info
sampledLogger.Debug("trace") // sampled — only every 100th call goes through
This works because embedding the Logger interface means *SampledLogger automatically has Info and Error methods (forwarded to s.Logger.Info and s.Logger.Error), even though SampledLogger only explicitly defines Debug.
The same pattern enables test doubles:
// Embed the interface; override only what the test needs to control
type testUserStore struct {
UserStore // embedded interface — panics if unimplemented methods are called
getUser func(ctx context.Context, id string) (*User, error)
}
func (t *testUserStore) GetUser(ctx context.Context, id string) (*User, error) {
return t.getUser(ctx, id)
}
Now testUserStore satisfies UserStore by embedding the interface, and only GetUser needs to be defined. Any other method call would panic — making test failures clear rather than silent.
Embedding for Mixin Behavior
Embedding is the Go equivalent of mixins — adding a set of behaviors to a type:
// Timestamps is a reusable "mixin" for audit fields
type Timestamps struct {
CreatedAt time.Time
UpdatedAt time.Time
}
func (t *Timestamps) Touch() {
t.UpdatedAt = time.Now()
}
func (t *Timestamps) SetCreated() {
now := time.Now()
t.CreatedAt = now
t.UpdatedAt = now
}
// All domain types get timestamp behavior for free
type User struct {
Timestamps
ID string
Email string
}
type Order struct {
Timestamps
ID string
Amount int
}
u := &User{Email: "[email protected]"}
u.SetCreated() // Timestamps.SetCreated()
order := &Order{Amount: 4999}
order.SetCreated()
order.Touch() // update UpdatedAt on each modification
When Composition Beats Explicit Delegation
Explicit delegation means naming the embedded field and writing forwarding methods:
// Explicit delegation — verbose, but clear ownership
type UserService struct {
logger *Logger // named field
}
func (s *UserService) Info(msg string) {
s.logger.Info(msg)
}
Embedding saves the forwarding methods when the relationship is genuinely “has and uses the capability” rather than “wraps and possibly overrides”:
// Embedding — promoted methods, no forwarding code
type UserService struct {
*Logger // anonymous — methods promoted
}
Use embedding when:
- You want all methods of the embedded type available on the outer type
- The outer type “is made of” the inner type (composition, not just delegation)
- You’re implementing the wrapper/decorator pattern with interface embedding
Use explicit delegation when:
- You want to control which methods are exposed
- The inner type is a dependency you’re injecting, not composing with
- The relationship is “uses” rather than “is implemented in terms of”
Summary
- Interface embedding composes interfaces:
ReadWriter=Reader+Writer— any type satisfying both satisfies the composed interface - Struct embedding promotes fields and methods:
svc.Info()callssvc.Logger.Info()automatically - Promoted methods satisfy interfaces: embed
Loggerand your outer type satisfies any interfaceLoggerdoes - Embedding an interface in a struct creates a forwarder — override only the methods you need, the rest are forwarded
- Test doubles: embed the interface, define only the methods under test — unimplemented calls panic clearly
- Use embedding for mixins (Timestamps, audit fields) that many types share; use explicit fields for injected dependencies
Resources
- Effective Go: Embedding
- Go specification: Struct types
- Go by Example: Struct Embedding
- Go interfaces definition
Comments