Ask AI to "create a user hierarchy with admin and regular users" in Go and watch what happens. It'll often generate Java-style inheritance patterns using embedding -- Admin embeds User embeds BaseEntity. This looks familiar but breaks Go's design philosophy. Embedding is composition ("has-a"), not inheritance ("is-a"), and misusing it creates bugs AI won't warn you about.
Struct embedding basics
Go lets you embed one struct inside another. The embedded struct's fields and methods get "promoted" to the outer struct:
type Person struct {
Name string
Age int
}
func (p Person) Greet() string {
return fmt.Sprintf("Hi, I'm %s", p.Name)
}
type Employee struct {
Person // Embedded -- no field name
EmployeeID int
Department string
}
func main() {
emp := Employee{
Person: Person{Name: "Alice", Age: 30},
EmployeeID: 12345,
Department: "Engineering",
}
fmt.Println(emp.Name) // "Alice" -- promoted from Person
fmt.Println(emp.Greet()) // "Hi, I'm Alice" -- promoted method
}No field name on Person -- just the type. That's what makes it embedding rather than a regular field.
Embedding vs regular fields
| Approach | Syntax | Field access | Method access |
|---|---|---|---|
| Regular field | p Person | emp.p.Name | emp.p.Greet() |
| Embedding | Person | emp.Name | emp.Greet() |
Use embedding when you want the inner type's APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. to be part of the outer type's API. Use a regular field when you want to keep it private or just use it internally.
log.Logger, promoting every logging method onto your client type. If you don't want callers to see those methods, use a regular field instead:> // AI generates this -- exposes all of http.Client's methods
> type MyClient struct {
> http.Client
> }
>
> // Better -- only expose what you need
> type MyClient struct {
> client http.Client
> }
>
> func (c *MyClient) Get(url string) (*http.Response, error) {
> return c.client.Get(url)
> }
>The critical difference: embedding is not inheritance
This is the concept AI gets wrong most often. In Java, Dog extends Animal means a Dog IS an Animal -- you can pass a Dog anywhere an Animal is expected. In Go, embedding doesn't create that relationship:
type Animal struct {
Name string
}
func ProcessAnimal(a Animal) {
fmt.Println("Processing:", a.Name)
}
type Dog struct {
Animal
Breed string
}
func main() {
dog := Dog{Animal: Animal{Name: "Rex"}, Breed: "Shepherd"}
// This does NOT work
// ProcessAnimal(dog) // Compile error: cannot use Dog as Animal
// You must explicitly access the embedded struct
ProcessAnimal(dog.Animal) // Works
}Animal and then pass a Dog, expecting Go inheritance that doesn't exist. When you see this pattern, the fix is usually to define an interface instead:> type Named interface {
> GetName() string
> }
>
> func (a Animal) GetName() string { return a.Name }
> // Dog inherits GetName through embedding -- and satisfies Named
>
> func Process(n Named) {
> fmt.Println("Processing:", n.GetName())
> }
>Method promotion and overriding
Embedded methods are promoted, and the outer type can override them:
type Vehicle struct {
Make string
}
func (v Vehicle) Start() string {
return fmt.Sprintf("%s is starting", v.Make)
}
type Car struct {
Vehicle
Model string
}
func (c Car) Start() string {
return fmt.Sprintf("%s %s vroom!", c.Make, c.Model)
}
car := Car{
Vehicle: Vehicle{Make: "Toyota"},
Model: "Camry",
}
fmt.Println(car.Start()) // "Toyota Camry vroom!" -- outer wins
fmt.Println(car.Vehicle.Start()) // "Toyota is starting" -- still accessibleIf both the outer and embedded struct have a field with the same name, the outer one wins. Access the embedded version by qualifying it:
> type Employee struct {
> Person
> Name string // Shadows Person.Name
> }
> emp.Name // Employee's Name
> emp.Person.Name // Person's Name
>Interface embedding
Interfaces can embed other interfaces to compose contracts:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type ReadWriter interface {
Reader
Writer
}This is exactly how io.ReadWriter is defined in the standard libraryWhat is standard library?A collection of ready-made tools that come built into a language - no install required. Covers common tasks like reading files or making web requests.. Any type satisfying both Reader and Writer automatically satisfies ReadWriter.
Real-world embedding patterns
Thread-safe structs with sync.MutexWhat is mutex?A mutual exclusion lock that prevents concurrent access to shared data - only one thread or goroutine can hold it at a time.
The most common and idiomatic embedding pattern:
type SafeCounter struct {
sync.Mutex
count int
}
func (c *SafeCounter) Increment() {
c.Lock() // Promoted from sync.Mutex
defer c.Unlock()
c.count++
}
func (c *SafeCounter) Value() int {
c.Lock()
defer c.Unlock()
return c.count
}> // WRONG -- value receiver copies the mutex
> func (c SafeCounter) Value() int {
> c.Lock() // Locking a COPY of the mutex
> defer c.Unlock()
> return c.count
> }
>Always use pointer receivers on types that embed
sync.Mutex.Composable service dependencies
type Logger struct {
prefix string
}
func (l Logger) Log(msg string) {
fmt.Printf("[%s] %s\n", l.prefix, msg)
}
type Metrics struct {
counters map[string]int
}
func (m *Metrics) Increment(name string) {
m.counters[name]++
}
type Service struct {
Logger
*Metrics
Name string
}
func (s *Service) DoWork() {
s.Log("Starting work...") // From Logger
s.Increment("work_started") // From Metrics
}Evaluating AI's OOP translations
When AI translates OOP patterns into Go, use this table to evaluate:
| OOP pattern | AI's Go translation | Correct Go approach |
|---|---|---|
class Dog extends Animal | Embed Animal in Dog | Define interfaces for shared behavior |
abstract class Shape | Embed a BaseShape struct | Define a Shape interface |
interface ISerializable | Create Serializable interface | Correct, but drop the I prefix |
| Deep hierarchy (3+ levels) | Nested embedding | Flatten with interfaces + composition |
super.Method() | d.Animal.Method() | Correct, but question if needed |
| Multiple inheritance | Embed multiple structs | Usually correct, but watch for name collisions |
Composition vs inheritance: the mental model
| Aspect | Inheritance (Java/Python) | Composition (Go) |
|---|---|---|
| Relationship | "is-a" (Dog is an Animal) | "has-a" (Car has an Engine) |
| Flexibility | Rigid hierarchy | Mix and match freely |
| Multiple inheritance | Diamond problem | Embed multiple structs safely |
| Method resolution | Complex chain up hierarchy | Outer type wins, explicit access to inner |
| Testing | Mock entire class tree | Mock individual embedded types |
| Polymorphism | Through class hierarchy | Through interfaces (separate concept) |
Quick reference
| Pattern | Syntax | Use case |
|---|---|---|
| Struct embedding | Type (no field name) | Reuse fields and methods |
| Interface embedding | Interface inside interface | Compose contracts |
| Access promoted field | outer.Field | When no name collision |
| Access shadowed field | outer.EmbeddedType.Field | When names collide |
| Override method | Define same method on outer type | Customize embedded behavior |