Embedding And Composition For Code Reuse: Using Go’s Embedding Feature For Cleaner, More Modular Code
Overview
This chapter delves into Go’s powerful embedding and composition mechanisms, which allow developers to build cleaner, modular, and reusable code without traditional inheritance. Instead of subclassing, Go enables embedding types within structs and interfaces, promoting fields and methods automatically. You will explore how embedding works, how method promotion simplifies code, and how to handle collisions and method overriding effectively. This chapter also covers how embedding can be used to satisfy interfaces, making your code more versatile and maintainable through practical examples.
Embedding And Composition For Code Reuse: Using Go's Embedding Feature For Cleaner, More Modular Code
Go does not have inheritance
It does, however, offer other tools that allow the assembling of multiple components into smaller, more manageable ones.
Embedding Is NOT Inheritance - It's Different
When coming from object-oriented languages, it’s tempting to think of embedding as “inheritance lite.” This mental model will lead you astray.
Inheritance models an is-a relationship and creates a lineage (a subclass is-a superclass).
Embedding models behavior sharing - the outer type gains the embedded type’s methods and fields, but there is no parent-child relationship.
Key differences:
- No polymorphism: An
Adminthat embedsUseris NOT aUsertype - No method dispatch up the chain: Methods on the embedded type don’t know about the outer type
- Composition, not lineage: Think “has-a” with automatic delegation, not “is-a”
This distinction matters most when methods call other methods, as we’ll see in the Receiver Chain section.
No Inheritance
When discussing the struct type we saw that a struct contains n numbers of members, of various types.
package main
import "fmt"
// section: user
type User struct {
Name string
Age int
}
// section: user
// section: admin
type Admin struct {
User User
Permissions map[string]bool
}
// section: admin
// section: main
func main() {
a := Admin{
User: User{
Name: "Homer",
Age: 40,
},
Permissions: map[string]bool{},
}
fmt.Println(a.User.Name)
}
// section: mainThis is an example of composition. We are creating a new type, User, out of two other types, string and int.
Composing Complex Types
In Go, we are not limited to just using “primitive” types when composing a new type. We can use “complex” types as well.
package main
import "fmt"
// section: user
type User struct {
Name string
Age int
}
// section: user
// section: admin
type Admin struct {
User User
Permissions map[string]bool
}
// section: admin
// section: main
func main() {
a := Admin{
User: User{
Name: "Homer",
Age: 40,
},
Permissions: map[string]bool{},
}
fmt.Println(a.User.Name)
}
// section: main
func main() {
a := Admin{
User: User{
Name: "Homer",
Age: 40,
},
Permissions: map[string]bool{},
}
fmt.Println(a.User.Name)
}
package main
import "fmt"
// section: user
type User struct {
Name string
Age int
}
// section: user
// section: admin
type Admin struct {
User User
Permissions map[string]bool
}
// section: admin
// section: main
func main() {
a := Admin{
User: User{
Name: "Homer",
Age: 40,
},
Permissions: map[string]bool{},
}
fmt.Println(a.User.Name)
}
// section: mainEmbedding
In Go we can embed a type by just declaring its type, with no name.
package main
import "fmt"
type User struct {
Name string
Age int
}
// section: admin
type Admin struct {
User
Permissions map[string]bool
}
// section: admin
// section: main
func main() {
a := Admin{
User: User{
Name: "Homer",
Age: 40,
},
Permissions: map[string]bool{},
}
fmt.Println(a.Name)
}
// section: main
func main() {
a := Admin{
User: User{
Name: "Homer",
Age: 40,
},
Permissions: map[string]bool{},
}
fmt.Println(a.Name)
}
package main
import "fmt"
type User struct {
Name string
Age int
}
// section: admin
type Admin struct {
User
Permissions map[string]bool
}
// section: admin
// section: main
func main() {
a := Admin{
User: User{
Name: "Homer",
Age: 40,
},
Permissions: map[string]bool{},
}
fmt.Println(a.Name)
}
// section: mainNotice that when creating a new value of type Admin we still had to specify the key User.
Method Promotion
When we embed a type inside of another, both the inner type’s methods and fields get promoted and can be accessed as if they were part of the outer type.
package main
import "fmt"
type User struct {
Name string
Age int
}
// section: user
func (u User) Greet() {
fmt.Printf("Hello, %s\n", u.Name)
}
// section: user
type Admin struct {
User
Permissions map[string]bool
}
// section: main
func main() {
a := Admin{
User: User{
Name: "Homer",
},
}
// Promoted Method from User type
a.Greet() // Hello, Homer
// Promoted fields from User Type
fmt.Println(a.Name, a.Age)
}
// section: main
func main() {
a := Admin{
User: User{
Name: "Homer",
},
}
// Promoted Method from User type
a.Greet() // Hello, Homer
// Promoted fields from User Type
fmt.Println(a.Name, a.Age)
}
package main
import "fmt"
type User struct {
Name string
Age int
}
// section: user
func (u User) Greet() {
fmt.Printf("Hello, %s\n", u.Name)
}
// section: user
type Admin struct {
User
Permissions map[string]bool
}
// section: main
func main() {
a := Admin{
User: User{
Name: "Homer",
},
}
// Promoted Method from User type
a.Greet() // Hello, Homer
// Promoted fields from User Type
fmt.Println(a.Name, a.Age)
}
// section: mainMethod Collision
When there is a method collision (the inner type and the outer type both implement the same method) the outer type always wins.
package main
import "fmt"
type User struct {
Name string
Age int
}
// section: user
func (u User) Greet() {
fmt.Printf("Hello, %s\n", u.Name)
}
// section: user
type Admin struct {
User
Permissions map[string]bool
}
// section: admin
func (a Admin) Greet() {
fmt.Printf("Admin: %s\n", a.Name)
}
// section: admin
// section: main
func main() {
a := Admin{
User: User{
Name: "Homer",
},
}
a.Greet() // Admin: Homer
}
// section: mainpackage main
import "fmt"
type User struct {
Name string
Age int
}
// section: user
func (u User) Greet() {
fmt.Printf("Hello, %s\n", u.Name)
}
// section: user
type Admin struct {
User
Permissions map[string]bool
}
// section: admin
func (a Admin) Greet() {
fmt.Printf("Admin: %s\n", a.Name)
}
// section: admin
// section: main
func main() {
a := Admin{
User: User{
Name: "Homer",
},
}
a.Greet() // Admin: Homer
}
// section: mainpackage main
import "fmt"
type User struct {
Name string
Age int
}
// section: user
func (u User) Greet() {
fmt.Printf("Hello, %s\n", u.Name)
}
// section: user
type Admin struct {
User
Permissions map[string]bool
}
// section: admin
func (a Admin) Greet() {
fmt.Printf("Admin: %s\n", a.Name)
}
// section: admin
// section: main
func main() {
a := Admin{
User: User{
Name: "Homer",
},
}
a.Greet() // Admin: Homer
}
// section: mainAmbiguous Fields
If two inner types define the same field name, neither is promoted.
Given the following code…
package main
import (
"fmt"
)
type User struct {
ID int
Name string
}
type Guest struct {
ID int
Alias string
}
type Admin struct {
User
Guest
}
func main() {
a := Admin{
User: User{
ID: 1,
Name: "Cory",
},
Guest: Guest{
ID: -1,
Alias: "guest",
},
}
fmt.Println(a.ID)
}package main
import (
"fmt"
)
type User struct {
ID int
Name string
}
type Guest struct {
ID int
Alias string
}
type Admin struct {
User
Guest
}
func main() {
a := Admin{
User: User{
ID: 1,
Name: "Cory",
},
Guest: Guest{
ID: -1,
Alias: "guest",
},
}
fmt.Println(a.ID)
}…neither field would get promoted, and you would receive the following error:
ambiguous selector a.ID
Ambiguous Methods
If two inner types provide the same method, neither is promoted.
Given the following code…
package main
import (
"fmt"
)
type User struct {
Name string
}
func (u User) Greet() {
fmt.Println("hello: ", u.Name)
}
type Guest struct{}
func (g Guest) Greet() {
fmt.Println("hello guest")
}
type Admin struct {
User
Guest
}
func main() {
a := Admin{
User: User{Name: "Cory"},
}
a.Greet()
}package main
import (
"fmt"
)
type User struct {
Name string
}
func (u User) Greet() {
fmt.Println("hello: ", u.Name)
}
type Guest struct{}
func (g Guest) Greet() {
fmt.Println("hello guest")
}
type Admin struct {
User
Guest
}
func main() {
a := Admin{
User: User{Name: "Cory"},
}
a.Greet()
}…neither method would get promoted, and you would receive the following error:
./ambiguous/main.go:30:3: ambiguous selector a.Greet
The Shallowest Depth Rule
The Go specification uses the term “anonymous fields” (also called embedded types) and defines a precise rule for method/field promotion:
A field or method
fof an embedded field is promoted ifx.fis a legal selector that denotes that field or methodf.
The shallowest depth wins:
- Depth 0: Fields/methods defined directly on the outer type
- Depth 1: Fields/methods from directly embedded types
- Depth 2+: Fields/methods from types embedded within embedded types
When the same name exists at different depths, the shallowest one is promoted. When the same name exists at the same depth from multiple embeddings, it’s ambiguous and both are evicted from the method set.
// This example demonstrates the "shallowest depth" rule from the Go spec.
// When a field or method exists at multiple embedding depths, the
// shallowest one wins.
type Inner struct {
Value string
}
func (i Inner) Describe() string {
return "Inner: " + i.Value
}
type Middle struct {
Inner // depth 2 from Outer
Value string
}
func (m Middle) Describe() string {
return "Middle: " + m.Value
}
type Outer struct {
Middle // Middle.Describe is at depth 1
Inner // Inner.Describe is also at depth 1 - AMBIGUOUS!
}
package main
import "fmt"
// section: types
// This example demonstrates the "shallowest depth" rule from the Go spec.
// When a field or method exists at multiple embedding depths, the
// shallowest one wins.
type Inner struct {
Value string
}
func (i Inner) Describe() string {
return "Inner: " + i.Value
}
type Middle struct {
Inner // depth 2 from Outer
Value string
}
func (m Middle) Describe() string {
return "Middle: " + m.Value
}
type Outer struct {
Middle // Middle.Describe is at depth 1
Inner // Inner.Describe is also at depth 1 - AMBIGUOUS!
}
// section: types
// section: fixed
// To fix ambiguity, define the method at depth 0 (on Outer itself)
type OuterFixed struct {
Middle
Inner
}
func (o OuterFixed) Describe() string {
return fmt.Sprintf("Outer combining: %s and %s",
o.Middle.Describe(), o.Inner.Describe())
}
// section: fixed
// section: main
func main() {
// Shallowest depth wins - Middle.Value shadows Inner.Value
m := Middle{
Inner: Inner{Value: "inner-value"},
Value: "middle-value",
}
fmt.Println("Middle.Value:", m.Value) // "middle-value" (depth 0)
fmt.Println("Middle.Inner.Value:", m.Inner.Value) // "inner-value" (explicit)
// When depths are equal, both get evicted (ambiguous)
// Uncommenting this would cause: "ambiguous selector o.Describe"
// o := Outer{}
// o.Describe()
// Fixed version: depth 0 wins over depth 1
of := OuterFixed{
Middle: Middle{Value: "mid"},
Inner: Inner{Value: "in"},
}
fmt.Println("OuterFixed.Describe():", of.Describe())
}
// section: mainTo resolve ambiguity, define the method at depth 0:
// To fix ambiguity, define the method at depth 0 (on Outer itself)
type OuterFixed struct {
Middle
Inner
}
func (o OuterFixed) Describe() string {
return fmt.Sprintf("Outer combining: %s and %s",
o.Middle.Describe(), o.Inner.Describe())
}
package main
import "fmt"
// section: types
// This example demonstrates the "shallowest depth" rule from the Go spec.
// When a field or method exists at multiple embedding depths, the
// shallowest one wins.
type Inner struct {
Value string
}
func (i Inner) Describe() string {
return "Inner: " + i.Value
}
type Middle struct {
Inner // depth 2 from Outer
Value string
}
func (m Middle) Describe() string {
return "Middle: " + m.Value
}
type Outer struct {
Middle // Middle.Describe is at depth 1
Inner // Inner.Describe is also at depth 1 - AMBIGUOUS!
}
// section: types
// section: fixed
// To fix ambiguity, define the method at depth 0 (on Outer itself)
type OuterFixed struct {
Middle
Inner
}
func (o OuterFixed) Describe() string {
return fmt.Sprintf("Outer combining: %s and %s",
o.Middle.Describe(), o.Inner.Describe())
}
// section: fixed
// section: main
func main() {
// Shallowest depth wins - Middle.Value shadows Inner.Value
m := Middle{
Inner: Inner{Value: "inner-value"},
Value: "middle-value",
}
fmt.Println("Middle.Value:", m.Value) // "middle-value" (depth 0)
fmt.Println("Middle.Inner.Value:", m.Inner.Value) // "inner-value" (explicit)
// When depths are equal, both get evicted (ambiguous)
// Uncommenting this would cause: "ambiguous selector o.Describe"
// o := Outer{}
// o.Describe()
// Fixed version: depth 0 wins over depth 1
of := OuterFixed{
Middle: Middle{Value: "mid"},
Inner: Inner{Value: "in"},
}
fmt.Println("OuterFixed.Describe():", of.Describe())
}
// section: mainImportant: When embedding causes method ambiguity, the struct no longer satisfies interfaces that require those methods - they’re completely evicted, not just hidden.
Method Overriding
When embedding another type we can override their methods with new methods, while still being able to call the original method.
package main
import "fmt"
type User struct {
Name string
Age int
}
// section: user
func (u User) String() string {
return u.Name
}
// section: user
// section: admin
type Admin struct {
User
Permissions map[string]bool
}
func (a Admin) String() string {
return fmt.Sprintf("Admin: %s", a.User.String())
}
// section: admin
func main() {
a := Admin{
User: User{
Name: "Homer",
},
}
// section: print
fmt.Println(a.String()) // Admin: Homer
// section: print
}
type Admin struct {
User
Permissions map[string]bool
}
func (a Admin) String() string {
return fmt.Sprintf("Admin: %s", a.User.String())
}
package main
import "fmt"
type User struct {
Name string
Age int
}
// section: user
func (u User) String() string {
return u.Name
}
// section: user
// section: admin
type Admin struct {
User
Permissions map[string]bool
}
func (a Admin) String() string {
return fmt.Sprintf("Admin: %s", a.User.String())
}
// section: admin
func main() {
a := Admin{
User: User{
Name: "Homer",
},
}
// section: print
fmt.Println(a.String()) // Admin: Homer
// section: print
}package main
import "fmt"
type User struct {
Name string
Age int
}
// section: user
func (u User) String() string {
return u.Name
}
// section: user
// section: admin
type Admin struct {
User
Permissions map[string]bool
}
func (a Admin) String() string {
return fmt.Sprintf("Admin: %s", a.User.String())
}
// section: admin
func main() {
a := Admin{
User: User{
Name: "Homer",
},
}
// section: print
fmt.Println(a.String()) // Admin: Homer
// section: print
}Fixing Ambiguous Methods
To fix it, you would need to defined the Greet method on the
Admin type as follows:
package main
import (
"fmt"
)
type User struct {
Name string
}
func (u User) Greet() {
fmt.Println("hello: ", u.Name)
}
type Guest struct{}
func (g Guest) Greet() {
fmt.Println("hello guest")
}
type Admin struct {
User
Guest
}
func (a Admin) Greet() {
if a.User.Name != "" {
a.User.Greet()
return
}
a.Guest.Greet()
}
func main() {
a := Admin{
User: User{Name: "Cory"},
}
a.Greet()
}package main
import (
"fmt"
)
type User struct {
Name string
}
func (u User) Greet() {
fmt.Println("hello: ", u.Name)
}
type Guest struct{}
func (g Guest) Greet() {
fmt.Println("hello guest")
}
type Admin struct {
User
Guest
}
func (a Admin) Greet() {
if a.User.Name != "" {
a.User.Greet()
return
}
a.Guest.Greet()
}
func main() {
a := Admin{
User: User{Name: "Cory"},
}
a.Greet()
}Overriding Via Type Conversion
You can also do method overriding via Type Conversion.
Type conversion happens when we assign the value of one data type to another:
You have already seen type conversions, such as when we go from one numeric type to another:
func main() {
var i uint64 = 10
var j int = 20
// We must type convert i from a uint64 to an int in order to add i and j
k := (int(i) + j)
fmt.Println(k)
}
package main
import (
"fmt"
)
// section: code
func main() {
var i uint64 = 10
var j int = 20
// We must type convert i from a uint64 to an int in order to add i and j
k := (int(i) + j)
fmt.Println(k)
}
// section: codeYou can also create a type just for method overriding, and via type conversion change the behavior in your program:
type prettyString string
func (p prettyString) String() string {
return fmt.Sprintf("I'm a pretty string: %q", string(p))
}
func main() {
s := "Hi"
fmt.Println(s)
fmt.Println(prettyString(s))
}
package main
import "fmt"
// section: code
type prettyString string
func (p prettyString) String() string {
return fmt.Sprintf("I'm a pretty string: %q", string(p))
}
func main() {
s := "Hi"
fmt.Println(s)
fmt.Println(prettyString(s))
}
// section: code
/*
// section: output
Hi
I'm a pretty string: "Hi"
// section: output
*/Output:
package main
import "fmt"
// section: code
type prettyString string
func (p prettyString) String() string {
return fmt.Sprintf("I'm a pretty string: %q", string(p))
}
func main() {
s := "Hi"
fmt.Println(s)
fmt.Println(prettyString(s))
}
// section: code
/*
// section: output
Hi
I'm a pretty string: "Hi"
// section: output
*/More Overriding
In this example, we declare an authUser and use it to override the String method to provide sensitive information for only when a user is authenticated.
package main
import (
"fmt"
"log"
"net/http"
)
type User struct {
ID int
First string
Last string
Email string
}
func (u User) String() string {
return fmt.Sprintf(`{"id":%d","first":"%s","last":"%s"}`, u.ID, u.First, u.Last)
}
type authUser User
func (u authUser) String() string {
return fmt.Sprintf(`{"id":%d","first":"%s","last":"%s","email":"%s"}`, u.ID, u.First, u.Last, u.Email)
}
func handler(w http.ResponseWriter, r *http.Request) {
u := User{
ID: 1,
First: "Cory",
Last: "LaNou",
Email: "cory@gopherguides.com",
}
w.Header().Set("Content-Type", "application/text")
data := u.String()
username, password, ok := r.BasicAuth()
if ok && username == "admin" && password == "password" {
// Type convert user to an authUser to send all protected information to be serialized in the response.
data = authUser(u).String()
}
w.Write([]byte(data))
}
func main() {
http.HandleFunc("/", handler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
/*
$ curl localhost:8080
{"id":1","first":"Cory","last":"LaNou"}
$ curl --user admin:password localhost:8080
{"id":1","first":"Cory","last":"LaNou","email":"cory@gopherguides.com"}
*/package main
import (
"fmt"
"log"
"net/http"
)
type User struct {
ID int
First string
Last string
Email string
}
func (u User) String() string {
return fmt.Sprintf(`{"id":%d","first":"%s","last":"%s"}`, u.ID, u.First, u.Last)
}
type authUser User
func (u authUser) String() string {
return fmt.Sprintf(`{"id":%d","first":"%s","last":"%s","email":"%s"}`, u.ID, u.First, u.Last, u.Email)
}
func handler(w http.ResponseWriter, r *http.Request) {
u := User{
ID: 1,
First: "Cory",
Last: "LaNou",
Email: "cory@gopherguides.com",
}
w.Header().Set("Content-Type", "application/text")
data := u.String()
username, password, ok := r.BasicAuth()
if ok && username == "admin" && password == "password" {
// Type convert user to an authUser to send all protected information to be serialized in the response.
data = authUser(u).String()
}
w.Write([]byte(data))
}
func main() {
http.HandleFunc("/", handler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
/*
// section: command-anon
$ curl localhost:8080
{"id":1","first":"Cory","last":"LaNou"}
// section: command-anon
// section: command-auth
$ curl --user admin:password localhost:8080
{"id":1","first":"Cory","last":"LaNou","email":"cory@gopherguides.com"}
// section: command-auth
*/The Receiver Chain Gotcha
This is the most common source of confusion for developers coming from OOP languages.
When a method on an embedded type calls another method, that call stays at the embedded type’s level. The outer type’s “override” is never called.
// Employee has two methods that call each other
type Employee struct {
Name string
}
func (e Employee) Greet() {
fmt.Printf("Hello, I'm ")
e.Introduce() // calls Employee.Introduce, NOT Manager.Introduce
}
func (e Employee) Introduce() {
fmt.Println(e.Name)
}
package main
import "fmt"
// section: embedded
// Employee has two methods that call each other
type Employee struct {
Name string
}
func (e Employee) Greet() {
fmt.Printf("Hello, I'm ")
e.Introduce() // calls Employee.Introduce, NOT Manager.Introduce
}
func (e Employee) Introduce() {
fmt.Println(e.Name)
}
// section: embedded
// section: outer
// Manager embeds Employee and tries to "override" Introduce
type Manager struct {
Employee
Department string
}
// This method is NEVER called from Greet()!
func (m Manager) Introduce() {
fmt.Printf("%s, Manager of %s\n", m.Name, m.Department)
}
// section: outer
// section: main
func main() {
m := Manager{
Employee: Employee{Name: "Alice"},
Department: "Engineering",
}
// Calling Introduce directly works as expected
fmt.Print("Direct call: ")
m.Introduce() // Output: Alice, Manager of Engineering
// But calling Greet uses Employee.Introduce, not Manager.Introduce!
fmt.Print("Via Greet: ")
m.Greet() // Output: Hello, I'm Alice (NOT "Alice, Manager of Engineering")
}
// section: main
// Manager embeds Employee and tries to "override" Introduce
type Manager struct {
Employee
Department string
}
// This method is NEVER called from Greet()!
func (m Manager) Introduce() {
fmt.Printf("%s, Manager of %s\n", m.Name, m.Department)
}
package main
import "fmt"
// section: embedded
// Employee has two methods that call each other
type Employee struct {
Name string
}
func (e Employee) Greet() {
fmt.Printf("Hello, I'm ")
e.Introduce() // calls Employee.Introduce, NOT Manager.Introduce
}
func (e Employee) Introduce() {
fmt.Println(e.Name)
}
// section: embedded
// section: outer
// Manager embeds Employee and tries to "override" Introduce
type Manager struct {
Employee
Department string
}
// This method is NEVER called from Greet()!
func (m Manager) Introduce() {
fmt.Printf("%s, Manager of %s\n", m.Name, m.Department)
}
// section: outer
// section: main
func main() {
m := Manager{
Employee: Employee{Name: "Alice"},
Department: "Engineering",
}
// Calling Introduce directly works as expected
fmt.Print("Direct call: ")
m.Introduce() // Output: Alice, Manager of Engineering
// But calling Greet uses Employee.Introduce, not Manager.Introduce!
fmt.Print("Via Greet: ")
m.Greet() // Output: Hello, I'm Alice (NOT "Alice, Manager of Engineering")
}
// section: main
func main() {
m := Manager{
Employee: Employee{Name: "Alice"},
Department: "Engineering",
}
// Calling Introduce directly works as expected
fmt.Print("Direct call: ")
m.Introduce() // Output: Alice, Manager of Engineering
// But calling Greet uses Employee.Introduce, not Manager.Introduce!
fmt.Print("Via Greet: ")
m.Greet() // Output: Hello, I'm Alice (NOT "Alice, Manager of Engineering")
}
package main
import "fmt"
// section: embedded
// Employee has two methods that call each other
type Employee struct {
Name string
}
func (e Employee) Greet() {
fmt.Printf("Hello, I'm ")
e.Introduce() // calls Employee.Introduce, NOT Manager.Introduce
}
func (e Employee) Introduce() {
fmt.Println(e.Name)
}
// section: embedded
// section: outer
// Manager embeds Employee and tries to "override" Introduce
type Manager struct {
Employee
Department string
}
// This method is NEVER called from Greet()!
func (m Manager) Introduce() {
fmt.Printf("%s, Manager of %s\n", m.Name, m.Department)
}
// section: outer
// section: main
func main() {
m := Manager{
Employee: Employee{Name: "Alice"},
Department: "Engineering",
}
// Calling Introduce directly works as expected
fmt.Print("Direct call: ")
m.Introduce() // Output: Alice, Manager of Engineering
// But calling Greet uses Employee.Introduce, not Manager.Introduce!
fmt.Print("Via Greet: ")
m.Greet() // Output: Hello, I'm Alice (NOT "Alice, Manager of Engineering")
}
// section: mainWhy this happens: When Greet() executes, its receiver is Employee, not Manager. The method Greet() has no knowledge of Manager - it was defined on Employee and always operates in that context.
The rule: Once you “go down” the embedding chain via a promoted method, you can never go back up. Methods stay at their original depth.
This breaks the classical “override parent method” mental model. In traditional OOP:
class Manager extends Employee
def introduce() # override
"Manager: #{name}"
end
end
m.greet() # calls Manager.introduce() via dynamic dispatch
In Go, there is no dynamic dispatch up the embedding chain. If you need this behavior, use interfaces and explicit delegation instead.
Embedding Unexported Types
Even though a type is unexported, if you embed it promotion still occurs:
package models
import "fmt"
// user is unexported
type user struct {
Name string
Age int
}
func (u user) Greet() {
fmt.Printf("Hello, %s\n", u.Name)
}
type Admin struct {
user // embedding the unexported type
Permissions map[string]bool
}
func DefaultAdmin() Admin {
return Admin{
user: user{
Name: "Homer",
},
}
}package models
import "fmt"
// user is unexported
type user struct {
Name string
Age int
}
func (u user) Greet() {
fmt.Printf("Hello, %s\n", u.Name)
}
type Admin struct {
user // embedding the unexported type
Permissions map[string]bool
}
func DefaultAdmin() Admin {
return Admin{
user: user{
Name: "Homer",
},
}
}Here is the code that you can run the private promoted method from:
package main
import (
"fmt"
"training/models"
)
func main() {
a := models.DefaultAdmin()
a.Greet() // Call the `Greet` method that was promoted from the unexported user type
fmt.Println(a.Name) // All exported/promoted fields are also accessible.
// a.user.Greet() // this will not compile as `user` is not an exported field
}
// section: code
package main
import (
"fmt"
"training/models"
)
func main() {
a := models.DefaultAdmin()
a.Greet() // Call the `Greet` method that was promoted from the unexported user type
fmt.Println(a.Name) // All exported/promoted fields are also accessible.
// a.user.Greet() // this will not compile as `user` is not an exported field
}
// section: code
/*
// section: output
Hello, Homer
// section: output
*/Output:
// section: code
package main
import (
"fmt"
"training/models"
)
func main() {
a := models.DefaultAdmin()
a.Greet() // Call the `Greet` method that was promoted from the unexported user type
fmt.Println(a.Name) // All exported/promoted fields are also accessible.
// a.user.Greet() // this will not compile as `user` is not an exported field
}
// section: code
/*
// section: output
Hello, Homer
// section: output
*/Embedding Pointers
You can also embed pointers within structs:
package main
import "fmt"
type User struct {
name string
}
func (u *User) SetName(name string) {
u.name = name
}
func (u *User) String() string {
return u.name
}
// section: pointer
type Admin struct {
*User
Perms map[string]bool
}
// section: pointer
// section: main
func main() {
a := &Admin{}
fmt.Println(a.String())
}
// section: mainHowever, if that pointer is nil and you try to call a promoted method, you could open yourself up to panics:
package main
import "fmt"
type User struct {
name string
}
func (u *User) SetName(name string) {
u.name = name
}
func (u *User) String() string {
return u.name
}
// section: pointer
type Admin struct {
*User
Perms map[string]bool
}
// section: pointer
// section: main
func main() {
a := &Admin{}
fmt.Println(a.String())
}
// section: mainpanic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0xffffffff addr=0x0 pc=0xd799b]
Embedding Exercise (15 Mins)
package main
import (
"errors"
"fmt"
)
// #TODO: Create a struct type called `Email`
// Add the following fields:
// - DisplayName: string
// - UserName: string
// - Domain: string
// #TODO: Create a method on the Email type called `Address` that takes no arguments
// It should return a string in the format:
// `DisplayName <UserName@Domain>`
// You can use `fmt.Sprintf` to create the return string
// #TODO: Create a struct type called Invite
// Add the following fields:
// - ID: int
// - Email (embedded type Email)
func main() {
// #TODO: initialize a variable called `invite` of Type Invite with the following values:
// - ID: 1
// - Email
// - Display Name: "John Smith"
// - UserName: "jsmith"
// - Domain: "yahoo.com"
if err := SendEmail(invite); err != nil {
fmt.Printf("error sending email: %s", err)
}
}
func SendEmail(i Invite) error {
address := i.Address()
if address == "" {
return errors.New("no email address provided")
}
fmt.Printf("sent email to: %s", address)
return nil
}package main
import (
"errors"
"fmt"
)
// #TODO: Create a struct type called `Email`
// Add the following fields:
// - DisplayName: string
// - UserName: string
// - Domain: string
// #TODO: Create a method on the Email type called `Address` that takes no arguments
// It should return a string in the format:
// `DisplayName <UserName@Domain>`
// You can use `fmt.Sprintf` to create the return string
// #TODO: Create a struct type called Invite
// Add the following fields:
// - ID: int
// - Email (embedded type Email)
func main() {
// #TODO: initialize a variable called `invite` of Type Invite with the following values:
// - ID: 1
// - Email
// - Display Name: "John Smith"
// - UserName: "jsmith"
// - Domain: "yahoo.com"
if err := SendEmail(invite); err != nil {
fmt.Printf("error sending email: %s", err)
}
}
func SendEmail(i Invite) error {
address := i.Address()
if address == "" {
return errors.New("no email address provided")
}
fmt.Printf("sent email to: %s", address)
return nil
}Embedding Solution
package main
import (
"errors"
"fmt"
)
// #TODO: Create a struct type called `Email`
// Add the following fields:
// - DisplayName: string
// - UserName: string
// - Domain: string
type Email struct {
DisplayName string
UserName string
Domain string
}
// #TODO: Create a method on the Email type called `Address` that takes no arguments
// It should return a string in the format:
// `DisplayName <UserName@Domain>`
// You can use `fmt.Sprintf` to create the return string
func (e Email) Address() string {
return fmt.Sprintf("%s <%s@%s>", e.DisplayName, e.UserName, e.Domain)
}
// #TODO: Create a struct type called Invite
// Add the following fields:
// - ID: int
// - Email (embedded type Email)
type Invite struct {
ID int
Email
}
func main() {
// #TODO: initialize a variable called `invite` of Type Invite with the following values:
// - ID: 1
// - Email
// - Display Name: "John Smith"
// - UserName: "jsmith"
// - Domain: "yahoo.com"
invite := Invite{
ID: 1,
Email: Email{
DisplayName: "John Smith",
UserName: "jsmith",
Domain: "yahoo.com",
},
}
if err := SendEmail(invite); err != nil {
fmt.Printf("error sending email: %s", err)
}
}
func SendEmail(i Invite) error {
address := i.Address()
if address == "" {
return errors.New("no email address provided")
}
fmt.Printf("sent email to: %s", address)
return nil
}package main
import (
"errors"
"fmt"
)
// #TODO: Create a struct type called `Email`
// Add the following fields:
// - DisplayName: string
// - UserName: string
// - Domain: string
type Email struct {
DisplayName string
UserName string
Domain string
}
// #TODO: Create a method on the Email type called `Address` that takes no arguments
// It should return a string in the format:
// `DisplayName <UserName@Domain>`
// You can use `fmt.Sprintf` to create the return string
func (e Email) Address() string {
return fmt.Sprintf("%s <%s@%s>", e.DisplayName, e.UserName, e.Domain)
}
// #TODO: Create a struct type called Invite
// Add the following fields:
// - ID: int
// - Email (embedded type Email)
type Invite struct {
ID int
Email
}
func main() {
// #TODO: initialize a variable called `invite` of Type Invite with the following values:
// - ID: 1
// - Email
// - Display Name: "John Smith"
// - UserName: "jsmith"
// - Domain: "yahoo.com"
invite := Invite{
ID: 1,
Email: Email{
DisplayName: "John Smith",
UserName: "jsmith",
Domain: "yahoo.com",
},
}
if err := SendEmail(invite); err != nil {
fmt.Printf("error sending email: %s", err)
}
}
func SendEmail(i Invite) error {
address := i.Address()
if address == "" {
return errors.New("no email address provided")
}
fmt.Printf("sent email to: %s", address)
return nil
}Embedding != Interface
Embedding User into Admin makes Admin appear to behave like User, but it does not mean that an Admin can be used where the User type is requested.
package main
import "fmt"
type User struct {
Name string
Age int
}
type Admin struct {
User
Permissions map[string]bool
}
// section: user
func greetUser(u User) {
fmt.Println(u.Name)
}
// section: user
func main() {
a := Admin{
User: User{
Name: "Homer",
},
}
// section: greet
greetUser(a)
// section: greet
}package main
import "fmt"
type User struct {
Name string
Age int
}
type Admin struct {
User
Permissions map[string]bool
}
// section: user
func greetUser(u User) {
fmt.Println(u.Name)
}
// section: user
func main() {
a := Admin{
User: User{
Name: "Homer",
},
}
// section: greet
greetUser(a)
// section: greet
}admin-bad/main.go:25: cannot use a (type Admin) as type User in argument to greetUser
Embedding To Satisfy An Interface
Because of method promotion when embedding, the outer type can satisfy any interfaces that the inner type satisfies.
package main
import "fmt"
type User struct {
Name string
Age int
}
// section: user
func (u User) Greet() {
fmt.Printf("Hello, %s\n", u.Name)
}
// section: user
type Admin struct {
User
Permissions map[string]bool
}
// section: greeter
type greeter interface {
Greet()
}
// section: greeter
// section: func
func greet(g greeter) {
g.Greet()
}
// section: func
func main() {
a := Admin{
User: User{
Name: "Homer",
},
}
// section: greet
greet(a)
// section: greet
}package main
import "fmt"
type User struct {
Name string
Age int
}
// section: user
func (u User) Greet() {
fmt.Printf("Hello, %s\n", u.Name)
}
// section: user
type Admin struct {
User
Permissions map[string]bool
}
// section: greeter
type greeter interface {
Greet()
}
// section: greeter
// section: func
func greet(g greeter) {
g.Greet()
}
// section: func
func main() {
a := Admin{
User: User{
Name: "Homer",
},
}
// section: greet
greet(a)
// section: greet
}package main
import "fmt"
type User struct {
Name string
Age int
}
// section: user
func (u User) Greet() {
fmt.Printf("Hello, %s\n", u.Name)
}
// section: user
type Admin struct {
User
Permissions map[string]bool
}
// section: greeter
type greeter interface {
Greet()
}
// section: greeter
// section: func
func greet(g greeter) {
g.Greet()
}
// section: func
func main() {
a := Admin{
User: User{
Name: "Homer",
},
}
// section: greet
greet(a)
// section: greet
}package main
import "fmt"
type User struct {
Name string
Age int
}
// section: user
func (u User) Greet() {
fmt.Printf("Hello, %s\n", u.Name)
}
// section: user
type Admin struct {
User
Permissions map[string]bool
}
// section: greeter
type greeter interface {
Greet()
}
// section: greeter
// section: func
func greet(g greeter) {
g.Greet()
}
// section: func
func main() {
a := Admin{
User: User{
Name: "Homer",
},
}
// section: greet
greet(a)
// section: greet
}Embedding Interfaces
Embedding doesn’t end with just concrete types, we can also embedded interfaces, making some very powerful types.
type ReadWriter interface {
Reader
Writer
}
From the io package in the standard library creates a new interface, ReadWriter, by composing, and embedding, two other interfaces, Reader and Writer.
Embedding Interfaces Within Structs
Interface types can also be embedded into structs as well, and similarly promote their methods onto the struct type. This can be useful for slightly modifying the behavior of an interface or adding methods to a smaller interface to satisfy a wider one.
Io.ReadCloser
Occasionally you’ll encounter the io.ReadCloser interface when working with the standard library:
type ReadCloser interface {
Reader
Closer
}
In tests, you’ll typically have a reader built from strings.NewReader:
r := strings.NewReader("foo")
This is easy to create, but it doesn’t have a Close() method. How can we add one?
Adding A Close Method
By embedding the io.Reader interface, we can decorate it with a Close()
method that does nothing:
package main
import (
"fmt"
"io"
"os"
"strings"
)
// section: nop-closer
type Closer struct {
io.Reader
}
func (c Closer) Close() error {
return nil
}
// section: nop-closer
// section: nop-implemented
func main() {
c := Closer{
Reader: strings.NewReader("Hello, World!"),
}
if err := read(c); err != nil {
fmt.Println(err)
}
}
func read(r io.ReadCloser) error {
if _, err := io.Copy(os.Stdout, r); err != nil {
return err
}
return r.Close()
}
// section: nop-implementedIn fact, this is the definition of io.NopCloser, which is more common
to use in practice than implementing it yourself as we’ve done here.
And this is putting the embedded reader to use:
func main() {
c := Closer{
Reader: strings.NewReader("Hello, World!"),
}
if err := read(c); err != nil {
fmt.Println(err)
}
}
func read(r io.ReadCloser) error {
if _, err := io.Copy(os.Stdout, r); err != nil {
return err
}
return r.Close()
}
package main
import (
"fmt"
"io"
"os"
"strings"
)
// section: nop-closer
type Closer struct {
io.Reader
}
func (c Closer) Close() error {
return nil
}
// section: nop-closer
// section: nop-implemented
func main() {
c := Closer{
Reader: strings.NewReader("Hello, World!"),
}
if err := read(c); err != nil {
fmt.Println(err)
}
}
func read(r io.ReadCloser) error {
if _, err := io.Copy(os.Stdout, r); err != nil {
return err
}
return r.Close()
}
// section: nop-implementedOr we could simply use a io.NopCloser:
func main() {
r := strings.NewReader("Hello, World!")
if err := read(io.NopCloser(r)); err != nil {
fmt.Println(err)
}
}
func read(r io.ReadCloser) error {
if _, err := io.Copy(os.Stdout, r); err != nil {
return err
}
return r.Close()
}
package main
import (
"fmt"
"io"
"os"
"strings"
)
// section: nop
func main() {
r := strings.NewReader("Hello, World!")
if err := read(io.NopCloser(r)); err != nil {
fmt.Println(err)
}
}
func read(r io.ReadCloser) error {
if _, err := io.Copy(os.Stdout, r); err != nil {
return err
}
return r.Close()
}
// section: nopOverriding Type For Interface
You can also use type conversions to override a method that satisfies an interface. This is often done to change the behavior of that interface.
In this example, if the user is authorized, we send back protected information(email). If the user is not authorized, the email is not serialized out.
We are able to change this behavior by creating our own type that overrides the MarshalJSON method that satisfies the json.Marshaler interface.
package main
import (
"encoding/json"
"log"
"net/http"
)
type User struct {
ID int `json:"id"`
First string `json:"first,omitempty"`
Last string `json:"last,omitempty"`
Email string `json:"-"` // ingore this field
}
type authUser User
func (a authUser) MarshalJSON() ([]byte, error) {
// declare a private type to write our data to
type usr struct {
ID int `json:"id"`
First string `json:"first,omitempty"`
Last string `json:"last,omitempty"`
Email string `json:"email,omitempty"`
}
// Populate our new instance with just the data we need
us := usr{
ID: a.ID,
First: a.First,
Last: a.Last,
Email: a.Email,
}
return json.Marshal(us)
}
func handler(w http.ResponseWriter, r *http.Request) {
u := User{
ID: 1,
First: "Cory",
Last: "LaNou",
Email: "cory@gopherguides.com",
}
w.Header().Set("Content-Type", "application/json")
username, password, ok := r.BasicAuth()
if ok && username == "admin" && password == "password" {
// Type convert user to an authUser to send all protected information to be serialized in the response.
json.NewEncoder(w).Encode(authUser(u))
return
}
json.NewEncoder(w).Encode(u)
}
func main() {
http.HandleFunc("/", handler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
/*
$ curl localhost:8080
{"id":1,"first":"Cory","last":"LaNou"}
$ curl --user admin:password localhost:8080
{"id":1,"first":"Cory","last":"LaNou","email":"cory@gopherguides.com"}
*/package main
import (
"encoding/json"
"log"
"net/http"
)
type User struct {
ID int `json:"id"`
First string `json:"first,omitempty"`
Last string `json:"last,omitempty"`
Email string `json:"-"` // ingore this field
}
type authUser User
func (a authUser) MarshalJSON() ([]byte, error) {
// declare a private type to write our data to
type usr struct {
ID int `json:"id"`
First string `json:"first,omitempty"`
Last string `json:"last,omitempty"`
Email string `json:"email,omitempty"`
}
// Populate our new instance with just the data we need
us := usr{
ID: a.ID,
First: a.First,
Last: a.Last,
Email: a.Email,
}
return json.Marshal(us)
}
func handler(w http.ResponseWriter, r *http.Request) {
u := User{
ID: 1,
First: "Cory",
Last: "LaNou",
Email: "cory@gopherguides.com",
}
w.Header().Set("Content-Type", "application/json")
username, password, ok := r.BasicAuth()
if ok && username == "admin" && password == "password" {
// Type convert user to an authUser to send all protected information to be serialized in the response.
json.NewEncoder(w).Encode(authUser(u))
return
}
json.NewEncoder(w).Encode(u)
}
func main() {
http.HandleFunc("/", handler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
/*
// section: command-anon
$ curl localhost:8080
{"id":1,"first":"Cory","last":"LaNou"}
// section: command-anon
// section: command-auth
$ curl --user admin:password localhost:8080
{"id":1,"first":"Cory","last":"LaNou","email":"cory@gopherguides.com"}
// section: command-auth
*/View Model Pattern: Hiding Sensitive Fields
A common use of embedding is the view model pattern for API responses. You embed a database model but shadow sensitive fields to prevent them from being serialized.
Important: Using json:"-" on an embedded field does NOT work as you might expect. The JSON marshaler will still dig into the embedded struct and serialize its fields. You must shadow the fields explicitly.
// User represents a database model with sensitive fields
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Password string `json:"password"` // Sensitive!
APIKey string `json:"api_key"` // Sensitive!
}
package main
import (
"encoding/json"
"fmt"
"log"
)
// section: model
// User represents a database model with sensitive fields
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Password string `json:"password"` // Sensitive!
APIKey string `json:"api_key"` // Sensitive!
}
// section: model
// section: viewmodel
// PublicUser is a view model that hides sensitive fields from API responses.
// Note: json:"-" on an embedded field does NOT work - the marshaler
// still digs into the embedded struct's fields.
type PublicUser struct {
User
Password string `json:"-"` // Hide password
APIKey string `json:"api_key,omitempty"` // Hide if empty
}
// NewPublicUser creates a safe-to-serialize view of the user
func NewPublicUser(u User) PublicUser {
return PublicUser{
User: u,
Password: "", // Shadows User.Password, hidden by json:"-"
APIKey: "", // Shadows User.APIKey, omitted when empty
}
}
// section: viewmodel
// section: main
func main() {
// Simulate a user from the database
dbUser := User{
ID: 1,
Name: "Alice",
Email: "alice@example.com",
Password: "super-secret-password",
APIKey: "sk-1234567890abcdef",
}
// BAD: Directly marshaling exposes sensitive data
fmt.Println("Direct marshal (UNSAFE):")
bad, _ := json.MarshalIndent(dbUser, "", " ")
fmt.Println(string(bad))
// GOOD: Use view model to hide sensitive fields
fmt.Println("\nView model marshal (SAFE):")
publicUser := NewPublicUser(dbUser)
good, err := json.MarshalIndent(publicUser, "", " ")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(good))
}
// section: main
/* Output:
Direct marshal (UNSAFE):
{
"id": 1,
"name": "Alice",
"email": "alice@example.com",
"password": "super-secret-password",
"api_key": "sk-1234567890abcdef"
}
View model marshal (SAFE):
{
"id": 1,
"name": "Alice",
"email": "alice@example.com"
}
*/
// PublicUser is a view model that hides sensitive fields from API responses.
// Note: json:"-" on an embedded field does NOT work - the marshaler
// still digs into the embedded struct's fields.
type PublicUser struct {
User
Password string `json:"-"` // Hide password
APIKey string `json:"api_key,omitempty"` // Hide if empty
}
// NewPublicUser creates a safe-to-serialize view of the user
func NewPublicUser(u User) PublicUser {
return PublicUser{
User: u,
Password: "", // Shadows User.Password, hidden by json:"-"
APIKey: "", // Shadows User.APIKey, omitted when empty
}
}
package main
import (
"encoding/json"
"fmt"
"log"
)
// section: model
// User represents a database model with sensitive fields
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Password string `json:"password"` // Sensitive!
APIKey string `json:"api_key"` // Sensitive!
}
// section: model
// section: viewmodel
// PublicUser is a view model that hides sensitive fields from API responses.
// Note: json:"-" on an embedded field does NOT work - the marshaler
// still digs into the embedded struct's fields.
type PublicUser struct {
User
Password string `json:"-"` // Hide password
APIKey string `json:"api_key,omitempty"` // Hide if empty
}
// NewPublicUser creates a safe-to-serialize view of the user
func NewPublicUser(u User) PublicUser {
return PublicUser{
User: u,
Password: "", // Shadows User.Password, hidden by json:"-"
APIKey: "", // Shadows User.APIKey, omitted when empty
}
}
// section: viewmodel
// section: main
func main() {
// Simulate a user from the database
dbUser := User{
ID: 1,
Name: "Alice",
Email: "alice@example.com",
Password: "super-secret-password",
APIKey: "sk-1234567890abcdef",
}
// BAD: Directly marshaling exposes sensitive data
fmt.Println("Direct marshal (UNSAFE):")
bad, _ := json.MarshalIndent(dbUser, "", " ")
fmt.Println(string(bad))
// GOOD: Use view model to hide sensitive fields
fmt.Println("\nView model marshal (SAFE):")
publicUser := NewPublicUser(dbUser)
good, err := json.MarshalIndent(publicUser, "", " ")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(good))
}
// section: main
/* Output:
Direct marshal (UNSAFE):
{
"id": 1,
"name": "Alice",
"email": "alice@example.com",
"password": "super-secret-password",
"api_key": "sk-1234567890abcdef"
}
View model marshal (SAFE):
{
"id": 1,
"name": "Alice",
"email": "alice@example.com"
}
*/The PublicUser type:
- Embeds
Userto get all the safe fields (ID, Name, Email) - Shadows
Passwordwithjson:"-"to hide it completely - Shadows
APIKeywithomitemptyso it’s omitted when empty
This pattern is especially useful when:
- Your database models have fields that should never leave the server
- You need different JSON representations for different API endpoints
- You want to add computed fields to responses without modifying the model
Note: Go 1.27 (expected August 2026) ships a new encoding/json/v2 package. Among the changes, field name matching becomes case-sensitive when decoding, and nil slices and maps marshal as [] and {} instead of null, which matters for API responses like these. This chapter teaches the original encoding/json package, which remains the default.
Embedding Vs. Fields
There should be a design decision when composing structures in regards to using embedding vs. field.
Embedding
- Promotes all fields and methods to newly composed type
- New type can automatically satisfy any interface the embedded type satisfies
- Requires method overriding to protect any methods you wish to guard (if any)
A common example of embedding is with the use if the io interfaces, such as io.Reader, io.Writer, etc.
Field
- No promotion of any fields or methods
- Type can be protected by not exporting it
- Can create your own methods and call underlying types fields or methods as needed
A common example of using a field to compose is the use of sync.Mutex. This allows for the new type to have a built in locking mechanism without exposing it outside the new type.
Dependency Injection
Dependency Injection is a design pattern that is used to decouple the external logic of your implementation. In go, if you desire to do dependency injection, you must first make sure your code is not tightly coupled.
If we look at the following example, the Service declares a Logger as a field, but uses the actual type from the log package, thus creating what we call a tightly coupled design. We can now only EVER use a log.Logger and nothing else. As a result, in our tests, we have to also create a real logger even though that may not be desirable.
package main
import (
"log"
"os"
)
type Service struct {
Name string
Logger *log.Logger
}
func NewService(n string, l *log.Logger) *Service {
return &Service{
Name: n,
Logger: l,
}
}
func (s *Service) Init() {
s.Logger.Printf("initializing : %s\n", s.Name)
}
func (s *Service) Stop() {
s.Logger.Printf("shutting down: %s\n", s.Name)
}
func main() {
l := log.New(os.Stdout, "[service] ", log.Lshortfile)
s := NewService("example", l)
s.Init()
s.Stop()
}package main
import (
"log"
"os"
)
type Service struct {
Name string
Logger *log.Logger
}
func NewService(n string, l *log.Logger) *Service {
return &Service{
Name: n,
Logger: l,
}
}
func (s *Service) Init() {
s.Logger.Printf("initializing : %s\n", s.Name)
}
func (s *Service) Stop() {
s.Logger.Printf("shutting down: %s\n", s.Name)
}
func main() {
l := log.New(os.Stdout, "[service] ", log.Lshortfile)
s := NewService("example", l)
s.Init()
s.Stop()
}Simple Decoupling
By introducing an interface that only defines the methods we intend to use, we can still use a log.Logger, but we can now create our own implementations (or even stubs) for testing:
package main
import (
"log"
"os"
)
type Logger interface {
Printf(format string, v ...any)
Println(v ...any)
}
type Service struct {
Name string
Logger
}
func NewService(n string, l Logger) *Service {
return &Service{
Name: n,
Logger: l,
}
}
func (s *Service) Init() {
s.Logger.Printf("initializing : %s\n", s.Name)
}
func (s *Service) Stop() {
s.Logger.Printf("shutting down: %s\n", s.Name)
}
func main() {
l := log.New(os.Stdout, "[service] ", log.Lshortfile)
s := NewService("example", l)
s.Init()
s.Stop()
}package main
import (
"log"
"os"
)
type Logger interface {
Printf(format string, v ...any)
Println(v ...any)
}
type Service struct {
Name string
Logger
}
func NewService(n string, l Logger) *Service {
return &Service{
Name: n,
Logger: l,
}
}
func (s *Service) Init() {
s.Logger.Printf("initializing : %s\n", s.Name)
}
func (s *Service) Stop() {
s.Logger.Printf("shutting down: %s\n", s.Name)
}
func main() {
l := log.New(os.Stdout, "[service] ", log.Lshortfile)
s := NewService("example", l)
s.Init()
s.Stop()
}Injecting A Dependency
Now that we have decoupled a real logger, we can create a No-op logger that we could use in testing, or perhaps we want to silence the logger based on a configuration file provided. The following is now possible due to the decoupling of the logger:
package main
type Logger interface {
Printf(format string, v ...any)
Println(v ...any)
}
type Service struct {
Name string
Logger
}
type nopLogger struct{}
func (n nopLogger) Printf(format string, v ...any) {}
func (n nopLogger) Println(v ...any) {}
func NewService(n string, l Logger) *Service {
return &Service{
Name: n,
Logger: l,
}
}
func (s *Service) Init() {
s.Logger.Printf("initializing : %s\n", s.Name)
}
func (s *Service) Stop() {
s.Logger.Printf("shutting down: %s\n", s.Name)
}
func main() {
//l := log.New(os.Stdout, "[service] ", log.Lshortfile)
s := NewService("example", nopLogger{})
s.Init()
s.Stop()
}package main
type Logger interface {
Printf(format string, v ...any)
Println(v ...any)
}
type Service struct {
Name string
Logger
}
type nopLogger struct{}
func (n nopLogger) Printf(format string, v ...any) {}
func (n nopLogger) Println(v ...any) {}
func NewService(n string, l Logger) *Service {
return &Service{
Name: n,
Logger: l,
}
}
func (s *Service) Init() {
s.Logger.Printf("initializing : %s\n", s.Name)
}
func (s *Service) Stop() {
s.Logger.Printf("shutting down: %s\n", s.Name)
}
func main() {
//l := log.New(os.Stdout, "[service] ", log.Lshortfile)
s := NewService("example", nopLogger{})
s.Init()
s.Stop()
}Complex Example
So far, we have looked at a very rudimentary example of using dependency injection. Let’s look at a more complex scenario that might look closer to what a production level construct might represent.
package main
import (
"log"
"os"
"sync"
"time"
)
type Logger interface {
Printf(format string, v ...any)
Println(v ...any)
}
type Service struct {
Name string
Logger
stop chan struct{}
complete chan struct{}
mu sync.Mutex
}
func NewService(n string, l Logger) *Service {
stop := make(chan struct{})
complete := make(chan struct{})
return &Service{
Name: n,
Logger: l,
stop: stop,
complete: complete,
mu: sync.Mutex{},
}
}
func (s *Service) Init() {
s.Logger.Printf("initializing : %s\n", s.Name)
}
func (s *Service) Run() {
go func() {
for {
select {
case <-s.stop:
s.Logger.Println("ending process")
s.mu.Lock()
defer s.mu.Unlock()
close(s.complete)
return
default:
time.Sleep(time.Second)
s.Logger.Println("doing some work...")
}
}
}()
}
func (s *Service) Stop() {
s.mu.Lock()
close(s.stop)
s.mu.Unlock()
s.Logger.Printf("shutting down: %s\n", s.Name)
<-s.complete
s.Logger.Printf("shutdown successful")
}
func main() {
l := log.New(os.Stdout, "[service] ", log.Lshortfile)
s := NewService("example", l)
s.Init()
s.Run()
// let the program fun for a while
time.Sleep(10 * time.Second)
s.Stop()
}package main
import (
"log"
"os"
"sync"
"time"
)
type Logger interface {
Printf(format string, v ...any)
Println(v ...any)
}
type Service struct {
Name string
Logger
stop chan struct{}
complete chan struct{}
mu sync.Mutex
}
func NewService(n string, l Logger) *Service {
stop := make(chan struct{})
complete := make(chan struct{})
return &Service{
Name: n,
Logger: l,
stop: stop,
complete: complete,
mu: sync.Mutex{},
}
}
func (s *Service) Init() {
s.Logger.Printf("initializing : %s\n", s.Name)
}
func (s *Service) Run() {
go func() {
for {
select {
case <-s.stop:
s.Logger.Println("ending process")
s.mu.Lock()
defer s.mu.Unlock()
close(s.complete)
return
default:
time.Sleep(time.Second)
s.Logger.Println("doing some work...")
}
}
}()
}
func (s *Service) Stop() {
s.mu.Lock()
close(s.stop)
s.mu.Unlock()
s.Logger.Printf("shutting down: %s\n", s.Name)
<-s.complete
s.Logger.Printf("shutdown successful")
}
func main() {
l := log.New(os.Stdout, "[service] ", log.Lshortfile)
s := NewService("example", l)
s.Init()
s.Run()
// let the program fun for a while
time.Sleep(10 * time.Second)
s.Stop()
}Interface Exercise (15 Mins)
package main
import (
"errors"
"fmt"
)
// #TODO: Declare an interface called `Addressable` that defines the `Address` method
// from the Email
type Invite struct {
ID int
Email
}
type Email struct {
DisplayName string
UserName string
Domain string
}
func (e Email) Address() string {
return fmt.Sprintf("%s <%s@%s>", e.DisplayName, e.UserName, e.Domain)
}
// #TODO: Declare a type called `spamTrack` derived from the `Invite` type
// #TODO: create a method `Address` to override the Invite address method
// - Retrieve the address from the original Invite
// - add in `+spam` identifier in the email to change all emails from `name@domain.com` to `name+spam@domain.com`
// hint: you can use the following command: strings.ReplaceAll(e, "@", "+spam@")
// - return the new `spam tracker` email instead of the original email
func main() {
invite := Invite{
ID: 1,
Email: Email{
DisplayName: "John Smith",
UserName: "jsmith",
Domain: "yahoo.com",
},
}
if err := SendEmail(invite); err != nil {
fmt.Printf("error sending email: %s", err)
}
// #TODO: repeat the above command to send an email, but this time override the invite with the new `spamTrack` type
// to insert a spam tracking email
}
// #TODO: Change the signature of the `SendEmail` function to take
// an `Addressable` interface, instead of the concrete type `Invite`
func SendEmail(i Invite) error {
address := i.Address()
if address == "" {
return errors.New("no email address provided")
}
fmt.Printf("sent email to: %s\n", i.Address())
return nil
}package main
import (
"errors"
"fmt"
)
// #TODO: Declare an interface called `Addressable` that defines the `Address` method
// from the Email
type Invite struct {
ID int
Email
}
type Email struct {
DisplayName string
UserName string
Domain string
}
func (e Email) Address() string {
return fmt.Sprintf("%s <%s@%s>", e.DisplayName, e.UserName, e.Domain)
}
// #TODO: Declare a type called `spamTrack` derived from the `Invite` type
// #TODO: create a method `Address` to override the Invite address method
// - Retrieve the address from the original Invite
// - add in `+spam` identifier in the email to change all emails from `name@domain.com` to `name+spam@domain.com`
// hint: you can use the following command: strings.ReplaceAll(e, "@", "+spam@")
// - return the new `spam tracker` email instead of the original email
func main() {
invite := Invite{
ID: 1,
Email: Email{
DisplayName: "John Smith",
UserName: "jsmith",
Domain: "yahoo.com",
},
}
if err := SendEmail(invite); err != nil {
fmt.Printf("error sending email: %s", err)
}
// #TODO: repeat the above command to send an email, but this time override the invite with the new `spamTrack` type
// to insert a spam tracking email
}
// #TODO: Change the signature of the `SendEmail` function to take
// an `Addressable` interface, instead of the concrete type `Invite`
func SendEmail(i Invite) error {
address := i.Address()
if address == "" {
return errors.New("no email address provided")
}
fmt.Printf("sent email to: %s\n", i.Address())
return nil
}Interface Solution
package main
import (
"errors"
"fmt"
"strings"
)
// #TODO: Declare an interface called `Addressable` that defines the `Address` method
// from the Email
type Addressable interface {
Address() string
}
type Invite struct {
ID int
Email
}
type Email struct {
DisplayName string
UserName string
Domain string
}
func (e Email) Address() string {
return fmt.Sprintf("%s <%s@%s>", e.DisplayName, e.UserName, e.Domain)
}
// #TODO: Declare a type called `spamTrack` derived from the `Invite` type
type spamTrack Invite
// #TODO: create a method `Address` to override the Invite address method
// - Retrieve the address from the original Invite
// - add in `+spam` identifier in the email to change all emails from `name@domain.com` to `name+spam@domain.com`
// hint: you can use the following command: strings.ReplaceAll(e, "@", "+spam@")
// - return the new `spam tracker` email instead of the original email
func (s spamTrack) Address() string {
e := Invite(s).Address()
e = strings.ReplaceAll(e, "@", "+spam@")
return e
}
func main() {
invite := Invite{
ID: 1,
Email: Email{
DisplayName: "John Smith",
UserName: "jsmith",
Domain: "yahoo.com",
},
}
if err := SendEmail(invite); err != nil {
fmt.Printf("error sending email: %s", err)
}
// #TODO: repeat the above command to send an email, but this time override the invite with the new `spamTrack` type
// to insert a spam tracking email
if err := SendEmail(spamTrack(invite)); err != nil {
fmt.Printf("error sending email: %s", err)
}
}
// Change the signature of the `SendEmail` function to take
// an `Addressable` interface, instead of the concrete type `Invite`
func SendEmail(a Addressable) error {
// Do some work here
address := a.Address()
if address == "" {
return errors.New("no email address provided")
}
fmt.Printf("sent email to: %s\n", a.Address())
return nil
}package main
import (
"errors"
"fmt"
"strings"
)
// #TODO: Declare an interface called `Addressable` that defines the `Address` method
// from the Email
type Addressable interface {
Address() string
}
type Invite struct {
ID int
Email
}
type Email struct {
DisplayName string
UserName string
Domain string
}
func (e Email) Address() string {
return fmt.Sprintf("%s <%s@%s>", e.DisplayName, e.UserName, e.Domain)
}
// #TODO: Declare a type called `spamTrack` derived from the `Invite` type
type spamTrack Invite
// #TODO: create a method `Address` to override the Invite address method
// - Retrieve the address from the original Invite
// - add in `+spam` identifier in the email to change all emails from `name@domain.com` to `name+spam@domain.com`
// hint: you can use the following command: strings.ReplaceAll(e, "@", "+spam@")
// - return the new `spam tracker` email instead of the original email
func (s spamTrack) Address() string {
e := Invite(s).Address()
e = strings.ReplaceAll(e, "@", "+spam@")
return e
}
func main() {
invite := Invite{
ID: 1,
Email: Email{
DisplayName: "John Smith",
UserName: "jsmith",
Domain: "yahoo.com",
},
}
if err := SendEmail(invite); err != nil {
fmt.Printf("error sending email: %s", err)
}
// #TODO: repeat the above command to send an email, but this time override the invite with the new `spamTrack` type
// to insert a spam tracking email
if err := SendEmail(spamTrack(invite)); err != nil {
fmt.Printf("error sending email: %s", err)
}
}
// Change the signature of the `SendEmail` function to take
// an `Addressable` interface, instead of the concrete type `Invite`
func SendEmail(a Addressable) error {
// Do some work here
address := a.Address()
if address == "" {
return errors.New("no email address provided")
}
fmt.Printf("sent email to: %s\n", a.Address())
return nil
}Stretch Exercise
The following code will output password as clear text in the XML. To avoid this, create a custom type for the password and satisfy the TextMarshaler interface to change the behavior to always encode a password as ********
package main
import (
"encoding/xml"
"fmt"
"log"
)
// TODO: create a custom type called `password` that is derived from the `string` type
// TODO: satisfy the `TextMarshaller` interface on the `password` type to always
// return a constant value of ********
type User struct {
ID int
Name string
Password string // TODO: change the type to your new type of `password`
}
func main() {
u := User{
ID: 1,
Name: "Jill Rogers",
Password: "ksd*23k21!",
}
b, err := xml.Marshal(u)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(b))
}package main
import (
"encoding/xml"
"fmt"
"log"
)
// TODO: create a custom type called `password` that is derived from the `string` type
// TODO: satisfy the `TextMarshaller` interface on the `password` type to always
// return a constant value of ********
type User struct {
ID int
Name string
Password string // TODO: change the type to your new type of `password`
}
func main() {
u := User{
ID: 1,
Name: "Jill Rogers",
Password: "ksd*23k21!",
}
b, err := xml.Marshal(u)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(b))
}Current Output:
<User><ID>1</ID><Name>Jill Rogers</Name><Password>ksd*23k21!</Password></User>
For more information on how the TextMarshaler is defined, see the TextMarshaler documentation
Stretch Solution
package main
import (
"encoding/xml"
"fmt"
"log"
)
// TODO: create a custom type called `password` that is derived from the `string` type
type password string
// TODO: satisfy the `TextMarshaller` interface on the `password` type to always
// return a constant value of ********
func (p password) MarshalText() ([]byte, error) {
return []byte("********"), nil
}
type User struct {
ID int
Name string
Password password // TODO: change the type to your new type of `password`
}
func main() {
u := User{
ID: 1,
Name: "Jill Rogers",
Password: "ksd*23k21!",
}
b, err := xml.Marshal(u)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(b))
}package main
import (
"encoding/xml"
"fmt"
"log"
)
// TODO: create a custom type called `password` that is derived from the `string` type
type password string
// TODO: satisfy the `TextMarshaller` interface on the `password` type to always
// return a constant value of ********
func (p password) MarshalText() ([]byte, error) {
return []byte("********"), nil
}
type User struct {
ID int
Name string
Password password // TODO: change the type to your new type of `password`
}
func main() {
u := User{
ID: 1,
Name: "Jill Rogers",
Password: "ksd*23k21!",
}
b, err := xml.Marshal(u)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(b))
}Output:
<User><ID>1</ID><Name>Jill Rogers</Name><Password>********</Password></User>
Additional Resources
Talks
- Sean Kelly - “Embedding, It Sure Is Weird” (GopherUK 2017)
- Video: https://www.youtube.com/watch?v=-LzYjMzfGDQ
- Code: https://github.com/savaki/embedding-talk
- Excellent deep dive into embedding gotchas, especially the receiver chain behavior
Go Specification
- Struct types - Official spec on embedding (called “anonymous fields”)
- Selectors - The depth rule for field/method promotion