Struct Embedding in Go Instead of Inheritance
1. Overview
If you’re familiar with Java or C#, your first question when writing Go is usually: “Where’s extends?”. Go has no inheritance. Instead, Go has struct embedding: embedding one type into another struct to reuse fields and methods.
This article explains how embedding works, how it differs from inheritance, and common pitfalls. After reading, you’ll be able to use embedding to share behavior, write clean decorators, and avoid its characteristic errors.
2. Basic Struct Embedding
Embedding is declaring a field with only a type, no name:
package main
import "fmt"
type Address struct {
Street string
City string
}
func (a Address) FullAddress() string {
return a.Street + ", " + a.City
}
type Customer struct {
Name string
Address // embedded field
}
func main() {
c := Customer{
Name: "Lan",
Address: Address{Street: "12 Lê Lợi", City: "Huế"},
}
fmt.Println(c.City) // field promoted
fmt.Println(c.FullAddress()) // method promoted
fmt.Println(c.Address.City) // explicit access
}
Huế
12 Lê Lợi, Huế
Huế
Analysis:
Addresssits insideCustomerwith no field name. Go automatically names the fieldAddress, which is the type’s name.- The
Cityfield andFullAddress()method ofAddressare promoted toCustomer. So you can writec.Cityinstead ofc.Address.City. - When initializing with a composite literal, you still have to use the field name
Address:. There’s no syntax to directly fillCityintoCustomer.
At its core, Customer contains an Address. This is a has-a relationship, just with some syntactic convenience added by the compiler. Customer is not an Address. You can’t pass a Customer to a function that expects Address:
func printCity(a Address) { fmt.Println(a.City) }
// printCity(c) // compile error: cannot use c (type Customer) as Address
printCity(c.Address) // OK
3. Method “Overriding” via Shadowing
If the outer struct declares a method with the same name as a method of the embedded type, the method of the outer struct will shadow the inner one:
package main
import "fmt"
type Logger struct {
Prefix string
}
func (l Logger) Log(msg string) {
fmt.Println(l.Prefix + msg)
}
type OrderService struct {
Logger
}
func (s OrderService) Log(msg string) {
s.Logger.Log("[order] " + msg) // call the method of the embedded type
}
func main() {
s := OrderService{Logger: Logger{Prefix: "> "}}
s.Log("created #1001")
s.Logger.Log("raw message")
}
> [order] created #1001
> raw message
s.Log(...)calls the method ofOrderService, because it’s at a shallower depth.s.Logger.Log(...)can still call the original. This is equivalent tosuper.log(...)in Java.
It looks like overriding at first glance. But this is exactly where many people stumble, as the next section shows.
4. The Biggest Pitfall: No Virtual Dispatch
In Java, if a parent class calls this.channel() and a subclass overrides channel(), the subclass version runs. Go does not do this:
package main
import "fmt"
type Notifier struct{}
func (n Notifier) Channel() string { return "email" }
func (n Notifier) Send(msg string) string {
return "[" + n.Channel() + "] " + msg
}
type SMSNotifier struct {
Notifier
}
func (s SMSNotifier) Channel() string { return "sms" }
func main() {
s := SMSNotifier{}
fmt.Println(s.Channel())
fmt.Println(s.Send("Đơn hàng đã giao"))
}
sms
[email] Đơn hàng đã giao
s.Send(...) is actually s.Notifier.Send(...). Inside Send, the receiver n has type Notifier. It has no idea it’s sitting inside a SMSNotifier, so n.Channel() always returns "email".
This is the core difference. The embedded type has no backward reference to the struct containing it. The Java-style “template method” pattern doesn’t apply directly.
4.1. The Fix: Separate the Varying Part into an Interface
If you need behavior that can change, pass it in explicitly:
package main
import "fmt"
type Channel interface {
Name() string
}
type Email struct{}
func (Email) Name() string { return "email" }
type SMS struct{}
func (SMS) Name() string { return "sms" }
type Notifier struct {
Channel Channel
}
func (n Notifier) Send(msg string) string {
return "[" + n.Channel.Name() + "] " + msg
}
func main() {
fmt.Println(Notifier{Channel: SMS{}}.Send("Đơn hàng đã giao"))
fmt.Println(Notifier{Channel: Email{}}.Send("Hóa đơn tháng 10"))
}
[sms] Đơn hàng đã giao
[email] Hóa đơn tháng 10
Now the varying part (Channel) is an explicit dependency. This is more “Go” than trying to emulate overriding.
5. Embedding and Interfaces
Interfaces in Go are satisfied implicitly. Promoted methods are also counted in the method set of the outer struct. So embedding helps a struct satisfy an interface without rewriting methods:
package main
import "fmt"
type Describer interface {
FullAddress() string
}
type Address struct{ Street, City string }
func (a Address) FullAddress() string { return a.Street + ", " + a.City }
type Warehouse struct {
Code string
Address
}
func describe(d Describer) { fmt.Println(d.FullAddress()) }
func main() {
w := Warehouse{Code: "HN-01", Address: Address{"KCN Thăng Long", "Hà Nội"}}
describe(w) // Warehouse satisfies Describer thanks to the promoted method
}
KCN Thăng Long, Hà Nội
5.1. Note on Pointer Receivers
The method set rules for embedding are as follows:
Declaration in S |
Method set of S |
Method set of *S |
|---|---|---|
embed T |
method receiver T |
method receiver T and *T |
embed *T |
method receiver T and *T |
method receiver T and *T |
For example, if Counter has func (c *Counter) Inc() and you embed Counter (not a pointer) into Stats, then only *Stats satisfies an interface with Inc(). The value Stats does not. Compile errors like “Stats does not implement Incrementer (method Inc has pointer receiver)” often come from this.
6. Embedding an Interface in a Struct: Clean Decorators
You can embed an interface in a struct. The struct will automatically have all the methods of the interface, with each method delegated to the inner value. You only need to override the methods you want to change:
package payment
import "fmt"
type PaymentService interface {
Charge(orderID string, amount int) error
Refund(orderID string) error
}
type LoggingPayment struct {
PaymentService // delegate all methods
Logs []string
}
func (l *LoggingPayment) Charge(orderID string, amount int) error {
l.Logs = append(l.Logs, fmt.Sprintf("charge %s: %d", orderID, amount))
return l.PaymentService.Charge(orderID, amount)
}
LoggingPaymentonly overridesCharge.Refundis automatically delegated to the innerPaymentService.*LoggingPaymentsatisfiesPaymentService, so you can wrap it around any implementation without the caller knowing.
When an interface has ten methods and you only need to intercept one, this technique saves a lot of repetitive code.
Note: If the embedded interface field is
nil, the code still compiles fine. But calling a method that isn’t overridden (likeRefund) will cause a nil pointer dereference panic at runtime. Always initialize it through a constructor, likeNewLoggingPayment(inner PaymentService).
7. Testing Delegation Behavior
Go uses the standard testing package instead of JUnit. The test below checks two things: Charge is logged and still calls the underlying service, and Refund is delegated without logging.
package payment
import "testing"
type fakePayment struct {
charged []string
refunded []string
}
func (f *fakePayment) Charge(orderID string, amount int) error {
f.charged = append(f.charged, orderID)
return nil
}
func (f *fakePayment) Refund(orderID string) error {
f.refunded = append(f.refunded, orderID)
return nil
}
func TestLoggingPayment(t *testing.T) {
inner := &fakePayment{}
var svc PaymentService = &LoggingPayment{PaymentService: inner}
if err := svc.Charge("ORD-1", 250000); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err := svc.Refund("ORD-1"); err != nil {
t.Fatalf("unexpected error: %v", err)
}
lp := svc.(*LoggingPayment)
if len(lp.Logs) != 1 || lp.Logs[0] != "charge ORD-1: 250000" {
t.Errorf("logs = %v, want [charge ORD-1: 250000]", lp.Logs)
}
if len(inner.charged) != 1 || len(inner.refunded) != 1 {
t.Errorf("inner not called: charged=%v refunded=%v", inner.charged, inner.refunded)
}
}
Run the test:
go test ./...
ok example.com/shop/payment 0.002s
The line var svc PaymentService = &LoggingPayment{...} is also a compile-time check: if *LoggingPayment doesn’t satisfy the interface, the test won’t build. In production code, you can use the familiar idiom var _ PaymentService = (*LoggingPayment)(nil) for the same purpose.
8. Name Conflicts and Common Errors
8.1. Ambiguous Selector
When two embedded types at the same depth have the same field or method name, accessing through the outer struct causes a compile error:
type Billing struct{ Email string }
type Shipping struct{ Email string }
type Order struct {
Billing
Shipping
}
func main() {
o := Order{}
// fmt.Println(o.Email) // compile error: ambiguous selector o.Email
fmt.Println(o.Billing.Email) // OK
}
The struct Order is still valid. The error only appears when you access o.Email. The fix is to call explicitly, or declare Email directly on Order to shadow both.
8.2. Accidentally Exposing API: The sync.Mutex Example
type Inventory struct {
sync.Mutex
stock map[string]int
}
This code runs fine. But now Lock() and Unlock() are public methods of Inventory, because the embedded field name Mutex is exported. Code outside the package can lock your inventory. Instead, write:
type Inventory struct {
mu sync.Mutex
stock map[string]int
}
The general rule: embedding brings all methods of the embedded type into your API. If you don’t want to expose them, use a named field.
8.3. Nil Embedded Pointers
Embedding *Address instead of Address means the zero value of the containing struct has a nil pointer. Then c.City will panic. Only embed a pointer when you really need to share an instance or need the method set of *T, and remember to initialize it.
9. When to Use and Not Use Embedding
Use when:
- You want the outer struct to genuinely expose all the behavior of the embedded type, like embedding
http.Handlerorio.Reader. - You’re writing a decorator or wrapper that only changes a few methods of a large interface.
- You’re grouping common fields like
Audit{CreatedAt, UpdatedAt}into many entities.
Don’t use when:
- You’re trying to recreate a multi-level inheritance hierarchy with “parent class calling child method”. Use interfaces and explicit dependencies instead (section 4.1).
- The embedded type has methods you don’t want to expose (section 8.2).
- You only need one or two methods from that type. A named field with a few hand-written delegating methods will be clearer.
Note: Embedding does not create an is-a relationship. If you need polymorphism, define an interface and have multiple types satisfy it. Don’t rely on “a struct is its embedded type”.
10. Conclusion
Struct embedding is Go’s composition tool, with a bit of syntactic convenience: fields and methods of the embedded type are promoted to the outer struct. It helps you reuse code, satisfy interfaces, and write clean decorators.
However, embedding is not inheritance. There’s no virtual dispatch, the embedded type doesn’t know about the struct containing it, and every promoted method becomes part of your API. Remember three things: use interfaces for behavior that varies, fully initialize embedded interface fields or pointers, and switch to named fields when you don’t want to expose methods.