Embedding
Composition แทน Inheritance
หัวข้อที่มีชื่อว่า “Composition แทน Inheritance”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 fieldfmt.Println(e.Describe()) // promoted methodPromotion ทำงานอย่างไร
หัวข้อที่มีชื่อว่า “Promotion ทำงานอย่างไร”เมื่อคุณเข้าถึง 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.NamePromotion ถูก 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
คุณสามารถ 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()
Interface embedding
หัวข้อที่มีชื่อว่า “Interface embedding”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
Embedding ไม่ใช่ Inheritance
หัวข้อที่มีชื่อว่า “Embedding ไม่ใช่ Inheritance”ความแตกต่างสำคัญ:
| 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 methods | Embedded type ไม่รู้จัก outer type |
เพราะไม่มี virtual dispatch, embedding ไม่อนุญาต polymorphism บน outer type ในการได้ polymorphism คุณยังต้องการ interface
Embedding *T vs T
หัวข้อที่มีชื่อว่า “Embedding *T vs T”การ 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:
engpanic: 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 นั้น
Embed interface เพื่อทำ partial mock
หัวข้อที่มีชื่อว่า “Embed interface เพื่อทำ partial mock”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-xpanic: runtime error: invalid memory address or nil pointer dereferencemockStore ตอบสนอง Store ทันทีที่มัน embed มัน — compiler พอใจเพราะ method set ของ embedded interface ครอบคลุมทุกอย่างที่ Store ต้องการ — แต่ embedded field เองเป็น nil จนกว่าคุณจะ assign implementation จริงเข้าไป การเรียก method ใดที่คุณไม่ได้ override จะเข้าถึง field ที่ nil นั้นและ panic pattern นี้ตั้งใจและมีประโยชน์สำหรับ stub method เดียวเป๊ะ ๆ แต่ทุกการเรียกที่ไม่ได้ override คือกับระเบิดที่ยังไม่ถูกเก็บ ไม่ใช่ค่า default ที่ปลอดภัย
JSON flattening ผ่าน embedding
หัวข้อที่มีชื่อว่า “JSON flattening ผ่าน embedding”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`
Method set เมื่อ embed T vs *T
หัวข้อที่มีชื่อว่า “Method set เมื่อ embed T vs *T”ไม่ว่าจะ 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 ด้วย
Lock/Unlock ที่รั่วจาก embedded mutex
หัวข้อที่มีชื่อว่า “Lock/Unlock ที่รั่วจาก embedded mutex”การ 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() abovefunc (e Employee) String() string { return fmt.Sprintf("%s (Dept: %s)", e.Name, e.Department)}
// Interface embeddingtype 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 embedding | reuse fields และ methods โดยไม่ต้อง inheritance | field shadowing ถ้า outer type มี field ชื่อเดียวกับ embedded type |
| interface embedding | compose 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 methodsRead/Write(และอื่น ๆ ) ของทั้งสองให้เรียกผ่านReadWriterตัวเดียวได้เลย โดยไม่ต้อง duplicate code
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”เมื่อโค้ดที่ generate มา embed type ให้เช็กสิ่งเหล่านี้ก่อนเชื่อมัน:
- embedded field เป็น pointer (
*T) หรือเปล่า? ถ้าใช่ มันรับประกันว่าไม่ nil ก่อนที่ promoted method ใด ๆ จะถูกเรียกหรือเปล่า? struct ที่ zero value มี embedded*Tfield จะมี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 ภายในของมัน