In the Go language, composition support is implemented through the embedding mechanism — the ability to include one structure inside another without an explicit field name. This approach was intended to allow modeling inheritance "in its own way", while maintaining the simplicity of the language and avoiding many complications associated with multiple inheritance.
Developers often want to extend the behavior or interface of a certain base structure without complicating the architecture. In Go, this is often achieved using embedding, which allows accessing methods and fields of the embedded type directly, as if they are defined in the parent structure. However, embedding has its peculiarities, and a misunderstanding of the mechanism can lead to unexpected errors, such as name conflicts, double embedding, and incorrect method inheritance.
Proper and economical use of embedding allows for a cleaner composition. It should be noted that methods and fields are "lifted" only one level up, and that embedding implements a "has-a" rather than an "is-a" relationship. One should avoid name conflicts and understand how the method receiver works.
Example code:
package main import "fmt" type Engine struct { Power int } func (e Engine) Start() { fmt.Println("Engine started with power", e.Power) } type Car struct { Engine // embedding, not a field Engine as engine Engine Brand string } func main() { c := Car{Engine: Engine{Power: 200}, Brand: "Toyota"} c.Start() // directly accessible fmt.Println(c.Power) // field is also lifted }
Key features:
Can embedding implement multiple inheritance?
No, embedding does not implement inheritance as in OOP; it is composition. You can embed several other structures into one, but upon method conflicts, it results in an assembly conflict, not merging.
What happens if the fields of the embedded structures have the same names?
There will be a compilation error upon direct access: the compiler does not know which field to refer to. The path must be explicitly specified through the name of the embedded structure.
Example code:
type A struct {X int} type B struct {X int} type C struct { A B } func main() { c := C{} // c.X = 1 // error: ambiguous selector c.A.X = 1 // only this way }
Are methods with value/pointer receivers lifted equally during embedding?
No. Methods with pointer receivers are lifted only if the structure also uses a pointer (c := &Car{}). If the struct is by value, methods with pointer receivers will not be "lifted".
A project with deeply nested structures where embedding is used to imitate multi-level inheritance. A newcomer does not understand from which structure a field or method comes, complicating the entire project and making it "magical".
Pros:
Cons:
Embedding is used to implement an interface; for example, the Logger interface is implemented through embedding a type with a Println method, explicitly documented and covered by tests.
Pros:
Cons: