{"slug": "struct-embedding-in-go-composition-that-bites-when-you-reach-for-inheritance", "title": "Struct Embedding in Go: Composition That Bites When You Reach for Inheritance", "summary": "A developer explains that Go's struct embedding is composition, not inheritance, and warns that promoted methods can cause unintended interface satisfaction and method shadowing. The post details how embedded methods run against the embedded value, not the outer struct, and recommends using named fields to avoid exposing unintended methods.", "body_md": "You come to Go from a language with classes. You see struct\n\nembedding for the first time, and it reads like inheritance. A field\n\nwith no name, methods that \"carry over\" to the outer type, a base\n\nstruct that your type extends. So you write code the way you always\n\nhave, and most of it works. Then a method does something you did not\n\nask for, a type satisfies an interface you never meant to implement,\n\nor two embedded types fight over a name and the compiler shrugs until\n\nthe exact line that calls it.\n\nEmbedding is not inheritance. It is composition with a syntax that\n\npromotes methods and fields up one level. Once you hold that\n\ndistinction, the surprises stop being surprises. Here is where they\n\ncome from.\n\nWrite an embedded field by giving a type with no field name:\n\n```\ntype Engine struct {\n    Horsepower int\n}\n\nfunc (e Engine) Start() string {\n    return \"vroom\"\n}\n\ntype Car struct {\n    Engine // embedded\n    Brand  string\n}\n```\n\n`Car`\n\nnow has a `Start`\n\nmethod and a `Horsepower`\n\nfield, both\n\npromoted from `Engine`\n\n. You can write `car.Start()`\n\nand\n\n`car.Horsepower`\n\nas if they were declared on `Car`\n\n.\n\n```\ncar := Car{Engine: Engine{Horsepower: 300}, Brand: \"Fiat\"}\nfmt.Println(car.Start())      // vroom\nfmt.Println(car.Horsepower)   // 300\n```\n\nThis is where the inheritance illusion starts. `car.Start()`\n\nis\n\nsugar. The compiler rewrites it to `car.Engine.Start()`\n\n. The\n\nreceiver of `Start`\n\nis still an `Engine`\n\n, never a `Car`\n\n. There is no\n\nbase class, no `super`\n\n, no virtual dispatch. `Engine`\n\ndoes not know\n\n`Car`\n\nexists.\n\nThat last point is the one that bites. A promoted method runs against\n\nthe embedded value, not the outer struct.\n\nSay you want a stringer on the embedded type that reads outer fields.\n\nThis is the move that feels like overriding a base method.\n\n```\ntype Base struct {\n    Name string\n}\n\nfunc (b Base) Describe() string {\n    return \"base: \" + b.Name\n}\n\ntype User struct {\n    Base\n    Role string\n}\n```\n\nYou construct a `User`\n\n, set the role, and call `Describe`\n\n, expecting\n\nthe role to show up somehow.\n\n```\nu := User{Base: Base{Name: \"ana\"}, Role: \"admin\"}\nfmt.Println(u.Describe()) // base: ana\n```\n\nThere is no path from `Describe`\n\nto `Role`\n\n. `Describe`\n\nhas a `Base`\n\nreceiver. `Base`\n\nhas no idea what `Role`\n\nis. In a class hierarchy a\n\nmethod on the parent can be overridden by the child and dynamic\n\ndispatch picks the override. Embedding has no dispatch. If you want\n\n`User`\n\nto describe itself, you declare the method on `User`\n\n:\n\n```\nfunc (u User) Describe() string {\n    return \"user: \" + u.Name + \" (\" + u.Role + \")\"\n}\n```\n\nNow `User.Describe`\n\nshadows the promoted `Base.Describe`\n\n. Calling\n\n`u.Describe()`\n\nruns the `User`\n\nversion. Calling `u.Base.Describe()`\n\nstill runs the base one. Both exist. You chose which by the selector,\n\nnot by runtime type.\n\nThis is the surprise that ships to production. Embedding a type drags\n\nits whole method set into yours, and that method set can satisfy\n\ninterfaces you never intended to implement.\n\n```\ntype Closer interface {\n    Close() error\n}\n\ntype Conn struct{}\n\nfunc (c *Conn) Close() error {\n    fmt.Println(\"closing real connection\")\n    return nil\n}\n\ntype Service struct {\n    *Conn // embedded for the Query method, say\n}\n```\n\nYou embedded `*Conn`\n\nbecause you wanted its query helpers. But\n\n`*Conn`\n\nhas `Close`\n\n, so `*Service`\n\nnow satisfies `Closer`\n\ntoo. A\n\npool that ranges over `Closer`\n\nvalues and calls `Close`\n\nwill close\n\nyour service's underlying connection, possibly one shared across the\n\nprogram.\n\n```\nfunc closeAll(cs []Closer) {\n    for _, c := range cs {\n        c.Close()\n    }\n}\n\n// *Service slides into []Closer with no warning,\n// and closeAll shuts its connection.\n```\n\nNothing in the type declaration says \"I am a Closer.\" The compiler\n\ninfers it from the promoted method set. If you did not mean to expose\n\n`Close`\n\n, embedding was the wrong tool. Use a named field instead:\n\n```\ntype Service struct {\n    conn *Conn\n}\n\nfunc (s *Service) Query(q string) error {\n    return s.conn.query(q)\n}\n```\n\nNow `*Service`\n\nexposes only what you wrote. `Close`\n\nstays private to\n\nthe connection. The rule: embedding is a public promise. Everything\n\nthe embedded type exports, your type exports too.\n\nWhether you embed `Conn`\n\nor `*Conn`\n\ndecides which methods get\n\npromoted, because of how Go builds method sets.\n\nA method with a pointer receiver belongs to the method set of the\n\npointer type, not the value type. So if `Conn`\n\nhas `func (c *Conn)`\n\n, then:\n\nClose()\n\n`Conn`\n\n(value) into `Service`\n\npromotes `Close`\n\nonly when\nyou hold a `*Service`\n\n, because `Close`\n\nneeds an addressable\nreceiver.`*Conn`\n\n(pointer) into `Service`\n\npromotes `Close`\n\nonto\nboth `Service`\n\nand `*Service`\n\n.\n\n``` js\ntype Service struct {\n    Conn // value embed\n}\n\nvar s Service\nvar _ Closer = &s // works: &s is addressable, Close promoted\nvar _ Closer = s  // compile error: Service does not implement Closer\n```\n\nThe fix when you want the value type itself to satisfy the interface\n\nis to embed the pointer, or to keep all receivers as values. Mixing\n\nvalue and pointer receivers on the same type is where this gets hard\n\nto reason about. Pick one receiver style per type and the method-set\n\nrules stop fighting you.\n\nEmbed two types that both export the same method or field name, and\n\nthe outer struct compiles. The conflict is only an error at the\n\nselector that uses the ambiguous name.\n\n```\ntype Reader struct{}\n\nfunc (Reader) Read() string { return \"read\" }\n\ntype Writer struct{}\n\nfunc (Writer) Read() string { return \"also read\" }\n\ntype Pipe struct {\n    Reader\n    Writer\n}\n```\n\n`Pipe`\n\ndeclares fine. You can construct it, store it, pass it around.\n\nThe Go spec says a name promoted from two embedded types at the same\n\ndepth is not promoted at all. So `Pipe`\n\nhas no `Read`\n\nmethod.\n\n``` js\nvar p Pipe\np.Read() // compile error: ambiguous selector p.Read\n```\n\nThe collision sleeps until a caller writes `p.Read()`\n\n. If no caller\n\never does, the ambiguity stays invisible. Then someone adds a call,\n\nor assigns `Pipe`\n\nto an interface that needs `Read`\n\n, and the build\n\nbreaks far from where the embedding lives.\n\nDepth matters too. A name at a shallower embed level wins over a\n\ndeeper one. So this resolves with no error:\n\n```\ntype WriterHolder struct {\n    Writer // Read sits at depth 2 from Pipe\n}\n\ntype Pipe struct {\n    Reader       // depth 1\n    WriterHolder // depth 2 path to Writer.Read\n}\n```\n\nThe shallow `Reader.Read`\n\nis promoted; the deeper one is shadowed.\n\nThe asymmetry between \"same depth is an error\" and \"different depth\n\nsilently picks the shallow one\" is exactly the kind of rule that\n\nreads fine and behaves surprisingly.\n\nResolve a real collision by promoting explicitly:\n\n```\nfunc (p Pipe) Read() string {\n    return p.Reader.Read()\n}\n```\n\nNone of this means avoid embedding. It means use it for what it is:\n\ncomposition where you genuinely want the embedded type's surface to\n\nbecome part of yours.\n\nEmbedding fits when:\n\n`http.ResponseWriter`\n\nto add logging while\nforwarding everything else.`sync.Mutex`\n\nso `s.Lock()`\n\nreads cleanly. Watch the value-copy trap\nthere: embed it and use pointer receivers.\n\n```\ntype loggingWriter struct {\n    http.ResponseWriter // promote the whole interface\n    status int\n}\n\nfunc (w *loggingWriter) WriteHeader(code int) {\n    w.status = code\n    w.ResponseWriter.WriteHeader(code) // forward\n}\n```\n\nThat is embedding doing its job. You wanted the full\n\n`ResponseWriter`\n\nsurface, and you overrode one method by shadowing.\n\nReach for a named field instead when you want the inner type's data\n\nwithout its public method set, when you do not want to promise the\n\ninner type's interfaces, or when \"has-a\" describes the relationship\n\nbetter than \"is-a-surface-of.\" Most domain types fall here. A\n\n`User`\n\nhas an `Address`\n\n; it does not want every `Address`\n\nmethod\n\nshowing up on `User`\n\n.\n\nBefore you embed, ask one question: do I want every exported method\n\nand field of this type to become part of my type's public surface,\n\nand to satisfy whatever interfaces that surface satisfies? If yes,\n\nembed. If you only want the data, or only some behavior, give it a\n\nfield name and forward what you need by hand. The extra lines are\n\ncheaper than an accidental `Closer`\n\n.\n\nEmbedding is one of those Go features that looks like a port of a\n\nclass concept and turns out to be its own thing with its own rules.\n\n*The Complete Guide to Go Programming* walks through method sets,\n\npromotion, and interface satisfaction the way the spec defines them,\n\nso the surprises in this post become the behavior you expect.\n\n*Hexagonal Architecture in Go* shows where composition over\n\ninheritance pays off when you are wiring ports and adapters.", "url": "https://wpnews.pro/news/struct-embedding-in-go-composition-that-bites-when-you-reach-for-inheritance", "canonical_source": "https://dev.to/gabrielanhaia/struct-embedding-in-go-composition-that-bites-when-you-reach-for-inheritance-29le", "published_at": "2026-06-13 21:54:11+00:00", "updated_at": "2026-06-13 22:20:40.464554+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["Go"], "alternates": {"html": "https://wpnews.pro/news/struct-embedding-in-go-composition-that-bites-when-you-reach-for-inheritance", "markdown": "https://wpnews.pro/news/struct-embedding-in-go-composition-that-bites-when-you-reach-for-inheritance.md", "text": "https://wpnews.pro/news/struct-embedding-in-go-composition-that-bites-when-you-reach-for-inheritance.txt", "jsonld": "https://wpnews.pro/news/struct-embedding-in-go-composition-that-bites-when-you-reach-for-inheritance.jsonld"}}