ข้ามไปยังเนื้อหา

Embedding

Go ไม่มี extends, ไม่มี base class, และไม่มี virtual dispatch แต่มี embedding: คุณวาง type หนึ่งไว้ใน type อื่นโดยไม่มีชื่อ field และ outer type จะได้รับ fields และ methods ทั้งหมดของ inner type ราวกับว่าเป็นของตัวเอง

type Named struct {
Name string
}
func (n Named) Describe() string { return "Name: " + n.Name }
type Employee struct {
Named // embedded — no field name
Department string
}

ตอนนี้ Employee มี field Name และ method Describe() ที่ promote จาก Named:

e := Employee{Named: Named{Name: "Alice"}, Department: "Engineering"}
fmt.Println(e.Name) // promoted field
fmt.Println(e.Describe()) // promoted method

เมื่อคุณเข้าถึง e.Name, Go มองหา field หรือ method ชื่อ Name ใน Employee โดยตรงก่อน ถ้าไม่พบ compiler จะค้นหาตาม depth: field หรือ method ที่อยู่ตื้นกว่าจะชนะ ถ้าสอง candidate อยู่ที่ depth เดียวกัน จะเป็น ambiguous และการใช้ selector นั้นเป็น compile error:

type A struct{ Name string }
type B struct{ Name string }
type E struct {
A
B
}
e := E{A{"a"}, B{"b"}}
e.Name // compile error: ambiguous selector e.Name

Promotion ถูก resolve ที่ compile time และ promoted methods เข้าร่วม method set ของ outer type — นี่คือเหตุผลที่ Employee satisfy Describer ด้านล่าง รูปแบบยาว e.Named.Name ใช้ได้เสมอ แม้ในกรณีที่รูปแบบสั้นจะ ambiguous

flowchart TD
  E["Employee (depth 0)"]
  N["Named (depth 1, embedded)"]
  E -->|embeds| N
  E -.promoted.-> Name["e.Name"]
  E -.promoted.-> Desc["e.Describe()"]
  N --> NameField["Name field"]
  N --> DescMethod["Describe() method"]
  NameField -.-> Name
  DescMethod -.-> Desc
field หรือ method ที่ depth ตื้นกว่าชนะ; สอง candidate ที่ depth เดียวกันจะ ambiguous

คุณสามารถ override promoted method ได้โดยประกาศ method ที่มีชื่อเดียวกันบน outer type:

func (e Employee) Describe() string {
return fmt.Sprintf("%s in %s", e.Name, e.Department)
}

ตอนนี้ e.Describe() เรียก method ของ outer แต่ inner method ยังเข้าถึงได้ผ่าน e.Named.Describe()

Interfaces ยัง embed interfaces อื่นได้:

type Stringer interface {
String() string
}
type Describer interface {
Stringer // embeds Stringer
Describe() string
}

type ใดที่ตอบสนอง Describer ก็ตอบสนอง Stringer โดยอัตโนมัติ standard library ใช้สิ่งนี้อย่างกว้างขวาง: io.ReadWriter embeds io.Reader และ io.Writer

ความแตกต่างสำคัญ:

Inheritance (Java/C++)Embedding (Go)
Virtual dispatch: การเรียก method ผ่าน base type จะ dispatch ไปยัง override ของ derivedไม่มี virtual dispatch: การเรียก inner method เรียก method นั้น เสมอ ไม่ว่า outer type จะมีอะไร
instanceof / type hierarchyไม่มี hierarchy — types ไม่มีความสัมพันธ์
Base class อ้างถึง derived class ได้ผ่าน virtual methodsEmbedded type ไม่รู้จัก outer type

เพราะไม่มี virtual dispatch, embedding ไม่อนุญาต polymorphism บน outer type ในการได้ polymorphism คุณยังต้องการ interface

การ embed pointer แทน value เปลี่ยนสองอย่าง: embedded type จะถูกแชร์หรือ copy และมันเป็น nil ได้หรือไม่ zero value ของมันเป็น nil จนกว่าจะมีอะไรมา allocate มัน — และการเรียก promoted method ผ่าน pointer นั้น dereference มัน:

type Named struct{ Name string }
func (n *Named) Hello() string { return "hi " + n.Name }
type Employee struct {
*Named // embedded pointer — zero value is nil
Dept string
}
func main() {
e := Employee{Dept: "eng"} // Named is nil, not allocated
fmt.Println(e.Dept)
fmt.Println(e.Hello()) // panics: promoted method dereferences a nil embedded pointer
}

รันบน Go 1.26.5:

eng
panic: runtime error: invalid memory address or nil pointer dereference

การ embed Named แบบ value แทนจะเลี่ยงกรณี nil ได้ทั้งหมด แลกกับการ copy Named struct ทั้งก้อนเข้าไปใน Employee ทุกตัว การ embed *Named เลี่ยง copy และให้ outer values หลายตัวแชร์ Named ตัวเดียวกันได้ แต่ทุกการเรียก promoted method ห่างจาก panic นี้แค่ nil check เดียว จนกว่าจะมีอะไรมา assign pointer นั้น

Struct embedding ใช้กับ interface types ได้เหมือนกัน ไม่ใช่แค่ concrete types การ embed interface field ให้ outer type ได้ methods ทั้งหมดของ interface นั้นมาฟรี — รวมถึงตัวที่คุณไม่เคย implement เอง ซึ่ง forward ตรงไปยังสิ่งที่ field นั้นเก็บอยู่ นี่เป็นเทคนิคที่พบบ่อยสำหรับ partial test double: override เฉพาะ method ที่ test สนใจ แล้ว inherit ที่เหลือ

type Store interface {
Get(key string) string
Put(key, val string)
}
// mockStore embeds the interface to get a partial implementation for free —
// only override the method the test cares about.
type mockStore struct {
Store // nil — any unoverridden method panics if called
}
func (m *mockStore) Get(key string) string { return "stub-" + key }
func main() {
var s Store = &mockStore{}
fmt.Println(s.Get("x")) // stub-x — overridden, fine
s.Put("x", "y") // panics: Put is not overridden, falls through to the nil embedded Store
}
stub-x
panic: runtime error: invalid memory address or nil pointer dereference

mockStore ตอบสนอง Store ทันทีที่มัน embed มัน — compiler พอใจเพราะ method set ของ embedded interface ครอบคลุมทุกอย่างที่ Store ต้องการ — แต่ embedded field เองเป็น nil จนกว่าคุณจะ assign implementation จริงเข้าไป การเรียก method ใดที่คุณไม่ได้ override จะเข้าถึง field ที่ nil นั้นและ panic pattern นี้ตั้งใจและมีประโยชน์สำหรับ stub method เดียวเป๊ะ ๆ แต่ทุกการเรียกที่ไม่ได้ override คือกับระเบิดที่ยังไม่ถูกเก็บ ไม่ใช่ค่า default ที่ปลอดภัย

encoding/json promote embedded struct fields เข้าไปใน JSON object ของ parent เมื่อ embedded field ไม่มี tag ของตัวเอง — บ่อยครั้งนี่คือสิ่งที่ API ต้องการพอดี และเกิดขึ้นโดยไม่ตั้งใจได้ง่ายเมื่อไม่ใช่:

type Address struct {
City string `json:"city"`
}
type Person struct {
Address // embedded, no json tag — fields flatten into the parent object
Name string `json:"name"`
}
func main() {
p := Person{Address: Address{City: "Bangkok"}, Name: "Ada"}
b, _ := json.Marshal(p)
fmt.Println(string(b))
}
{"city":"Bangkok","name":"Ada"}

fields ของ Address ปรากฏตรง ๆ ใน JSON object ของ Person โดยไม่มี "address": {...} ซ้อนเลย — กฎ field-promotion เดียวกับที่ให้คุณเขียน p.City ใน Go ก็ทำให้ JSON แบนไปด้วย ใส่ tag ให้ embedded field (Address \json:“address”`) เพื่อได้ nested object แทน ซึ่งจะเปลี่ยนมันจาก field แบบ *embedded* เป็นแบบ *named* ด้วย ทำให้ p.Citycompile ไม่ผ่านอีกต่อไปและกลายเป็นp.Address.City`

ไม่ว่าจะ embed แบบ value หรือ pointer promoted methods เข้าร่วม method set ของ outer type ตามกฎ value/pointer ปกติ — แต่ methods ไหนบ้างขึ้นกับ receiver ของ embedded type และผลลัพธ์อาจน่าแปลกใจ การ embed Named แบบ value เข้าไปใน Employee เมื่อ method เดียวของ Named มี pointer receiver:

type Named struct{ Name string }
func (n *Named) Hello() string { return "hi " + n.Name }
type Sayer interface{ Hello() string }
type Employee struct {
Named // embedded by value
Dept string
}
func main() {
var _ Sayer = Employee{} // Employee's method set has only value-receiver promoted methods
}
./main.go:15:16: cannot use Employee{} (value of struct type Employee) as Sayer value in variable declaration: Employee does not implement Sayer (method Hello has pointer receiver)

Employee (value) ไม่ตอบสนอง Sayer เพราะ Hello ต้องการ *Named และ Named field ที่ embed แบบ value จะ promote แค่ pointer-receiver methods ของมันไปยัง *Employee เท่านั้น ไม่ใช่ไปยัง Employee var _ Sayer = &Employee{} compile ผ่านสบาย ๆ — กฎเดียวกับที่ควบคุม methods ของ type เอง (method set ของ T เทียบกับ *T) ใช้ได้แบบ recursive ผ่าน embedding ด้วย

การ embed sync.Mutex เป็นทางลัดยอดนิยมเพื่อได้ lock/unlock โดยไม่ต้องตั้งชื่อ field — แต่ embedding promote Lock และ Unlock เข้าไปใน method set แบบ exported ของ outer type ทำให้ทุก caller เข้าถึง internal lock ได้โดยตรง:

// Bad: embedding sync.Mutex promotes Lock/Unlock into Cache's exported API —
// any caller can lock/unlock the internal mutex directly.
type Cache struct {
sync.Mutex
data map[string]string
}
func main() {
c := &Cache{data: map[string]string{}}
c.Lock() // Lock is part of Cache's public method set — leaked
c.data["a"] = "1"
c.Unlock()
}

โค้ดนี้ compile และรันได้โดยไม่ race แต่มันคือ design bug: ไม่มีอะไรหยุด caller ภายนอก Cache จากการเรียก c.Lock() แล้วไม่ unlock เลย หรือ lock คร่อมโค้ดที่เรียก method ของ Cache ที่ lock ภายในอีกที — deadlock ทันที ใช้ named, unexported field แทน — mu sync.Mutex — เพื่อให้เฉพาะ methods ของ Cache เองเท่านั้นที่เข้าถึง lock ได้ และวินัยการ lock อยู่ภายใน type ที่เป็นเจ้าของมันทั้งหมด

package main
import (
"encoding/json"
"fmt"
)
type Named struct {
Name string
}
func (n Named) String() string {
return "Named(" + n.Name + ")"
}
func (n Named) Describe() string {
// Describe calls String on itself — always Named's String, never an outer override
return "Name: " + n.String()
}
type Employee struct {
Named
Department string
}
// Employee's own String — NOT used by Named.Describe() above
func (e Employee) String() string {
return fmt.Sprintf("%s (Dept: %s)", e.Name, e.Department)
}
// Interface embedding
type Stringer interface {
String() string
}
type Describer interface {
Stringer
Describe() string
}
func print(d Describer) {
fmt.Println(d.String()) // Employee's own String, reached through the interface
fmt.Println(d.Describe()) // promoted Describe still calls Named's String, not Employee's
}
// Store is a minimal dependency; mockStore embeds it to get a partial fake for free.
type Store interface {
Get(key string) string
Put(key, val string)
}
type mockStore struct {
Store // only Get is overridden below; calling Put would panic on this nil embedded Store
}
func (m *mockStore) Get(key string) string { return "stub-" + key }
// Address is embedded without a json tag, so its fields flatten into Person's JSON object.
type Address struct {
City string `json:"city"`
}
type Person struct {
Address
Name string `json:"name"`
}
func main() {
e := Employee{
Named: Named{Name: "Alice"},
Department: "Engineering",
}
// promoted field and method
fmt.Println(e.Name) // Alice
fmt.Println(e.Describe()) // Name: Named(Alice) — Named's own String, not Employee's override
print(e)
var s Store = &mockStore{}
fmt.Println(s.Get("x")) // stub-x — overridden, safe to call
p := Person{Address: Address{City: "Bangkok"}, Name: "Ada"}
b, _ := json.Marshal(p)
fmt.Println(string(b)) // {"city":"Bangkok","name":"Ada"} — embedded fields flatten into the parent object
}
สิ่งที่ได้ประโยชน์ต้นทุน
struct embeddingreuse fields และ methods โดยไม่ต้อง inheritancefield shadowing ถ้า outer type มี field ชื่อเดียวกับ embedded type
interface embeddingcompose interface ขนาดใหญ่จาก interface เล็กยิ่ง interface ใหญ่ implement ยิ่งยาก
method promotionเรียก method ของ embedded type โดยตรงอาจสับสนว่า method มาจาก type ใดเมื่อมี embedding หลายชั้น
  • embedding = inheritance — embedded type ไม่รู้จัก outer type; ไม่มี virtual dispatch; ไม่มี IS-A relationship
  • embedding สร้าง subtype relationship — Person ที่ embed Address ไม่ใช่ subtype ของ Address — เป็นแค่ composition
  • promoted method ทำงานบน outer type — promoted method เรียก receiver ของ embedded type เสมอ ไม่ใช่ outer type

💡 ตัวอย่างจากของจริง

io.ReadWriter embed io.Reader และ io.Writer — type ที่ implement ทั้งสองอัตโนมัติ satisfy io.ReadWriter โดยไม่ต้องประกาศเพิ่ม

bufio.ReadWriter embed *bufio.Reader และ *bufio.Writer โดยตรง — promote methods Read/Write (และอื่น ๆ ) ของทั้งสองให้เรียกผ่าน ReadWriter ตัวเดียวได้เลย โดยไม่ต้อง duplicate code

เมื่อโค้ดที่ generate มา embed type ให้เช็กสิ่งเหล่านี้ก่อนเชื่อมัน:

  • embedded field เป็น pointer (*T) หรือเปล่า? ถ้าใช่ มันรับประกันว่าไม่ nil ก่อนที่ promoted method ใด ๆ จะถูกเรียกหรือเปล่า? struct ที่ zero value มี embedded *T field จะมี T เป็น nil จนกว่าจะมีอะไรมา assign
  • type ของ embedded field มี sync.Mutex, sync.RWMutex, หรือคล้ายกันหรือเปล่า? ถ้าไม่ใช่ named, unexported field (mu sync.Mutex) Lock/Unlock จะรั่วเข้าไปใน exported API ของ outer type
  • interface ถูก embed เพื่อสร้าง partial mock หรือ decorator หรือเปล่า? ทุก method ที่ไม่ได้ override ชัดเจนจะ forward ไปยัง embedded field ซึ่ง panic ถ้า field นั้นไม่เคยถูก assign
  • embedded struct field ขาด JSON tag ทั้งที่ตั้งใจอยากได้ nested "address": {...} object หรือเปล่า? ถ้าไม่มี tag fields ของมันจะแบนเข้าไปใน parent object แทน
  • เมื่อ embed แบบ value, method ที่เกี่ยวข้องเดียวของ embedded type มี pointer receiver หรือเปล่า? ถ้าใช่ outer type รูปแบบ value อาจไม่ตอบสนอง interface ที่รูปแบบ pointer ตอบสนองได้ — เช็กว่าโค้ดใช้แบบไหนจริง ๆ

🐛 หา bug ในโค้ดนี้

type Cache struct {
sync.Mutex
data map[string]int
}
func NewCache() *Cache {
return &Cache{data: make(map[string]int)}
}
func (c *Cache) Inc(key string) {
c.Lock()
defer c.Unlock()
c.data[key]++
}
func main() {
c := NewCache()
c.Lock() // caller reaches into Cache's internal lock directly
c.Inc("requests")
c.Unlock()
}
เฉลย

Cache embed sync.Mutex ตรง ๆ ซึ่ง promote Lock และ Unlock เข้าไปใน exported method set ของ Cache เอง — caller ใดก็ได้ ไม่ใช่แค่ methods ของ Cache สามารถเรียก c.Lock() ได้ ใน main caller lock mutex แล้วเรียก c.Inc ซึ่งพยายาม Lock อีกครั้งภายใน — sync.Mutex ไม่ reentrant ดังนั้น goroutine จึง block ตัวเองตลอดไป: fatal error: all goroutines are asleep - deadlock! ทางแก้คือ named, unexported field — mu sync.Mutex — เพื่อให้เฉพาะ methods ของ Cache เองเท่านั้นที่เข้าถึง lock ได้ และ caller ไม่มีทางไปยุ่งกับวินัยการ lock ภายในของมัน

Employee embeds Named คุณเข้าถึง field Name ที่ promote มาได้อย่างไร?
เกิดอะไรขึ้น?
type Named struct{ Name string }
func (n *Named) Hello() string { return "hi " + n.Name }

type Employee struct {
    *Named
    Dept string
}

e := Employee{Dept: "eng"}
fmt.Println(e.Hello())
ปัญหาของการ embed sync.Mutex ตรง ๆ แบบนี้คืออะไร?
type Cache struct {
    sync.Mutex
    data map[string]string
}
โค้ดนี้ compile ผ่านไหม?
type Named struct{ Name string }
func (n *Named) Hello() string { return "hi " + n.Name }
type Sayer interface{ Hello() string }

type Employee struct {
    Named // embedded by value
    Dept  string
}

var _ Sayer = Employee{}
Interface io.ReadWriter embeds io.Reader และ io.Writer type ที่ตอบสนอง io.ReadWriter ตอบสนอง... โดยอัตโนมัติ
struct Person embed Address (ไม่มี json tag) แล้วทั้งคู่ถูก marshal ด้วย encoding/json JSON หน้าตาเป็นอย่างไร?