บทที่ 11 · Part 4 — APIs and Boundaries
Construction and Compatibility
เลือก zero value, constructor, config struct, functional options และ DI ให้เหมาะกับ lifecycle
constructor ที่เริ่มจาก dependency 2 ตัวอาจโตเป็น 10 option ใน 1 ปี บางทีมแก้ด้วย global config, บางทีมใช้ functional options ทุก type และบางทีมเพิ่ม DI container จน business code ดึง dependency จาก container เอง ปัญหาจริงคือการประกาศ required state, default และ lifecycle ให้ชัดพร้อมรักษา API
จบบทนี้คุณจะ
- เลือก zero value, constructor, config struct หรือ functional options ตาม contract
- ประกอบ dependency แบบ explicit และรู้ว่าเมื่อไร
samber/doคุ้มค่า - evolve public API โดยไม่ทำลาย caller โดยไม่ตั้งใจ
เลือกรูปแบบ Construction จาก Invariant
ใช้ zero value เมื่อมันพร้อมใช้และไม่สร้าง invalid state เช่น bytes.Buffer ใช้ constructor เมื่อ type
ต้องมี dependency หรือ validation ก่อนทำงาน:
type Service struct {
store BatchStore
clock Clock
}
func NewService(store BatchStore, clock Clock) (*Service, error) {
if store == nil {
return nil, errors.New("batch store is required")
}
if clock == nil {
return nil, errors.New("clock is required")
}
return &Service{store: store, clock: clock}, nil
}
required dependency ควรเป็น parameter ปกติเพื่อให้ call site เห็นทันที ส่วน optional configuration ที่มีจำนวนคงที่และมักตั้งพร้อมกันเหมาะกับ config struct:
type WorkerConfig struct {
Concurrency int
QueueSize int
}
func NewWorker(store BatchStore, cfg WorkerConfig) (*Worker, error) {
if cfg.Concurrency <= 0 || cfg.QueueSize < cfg.Concurrency {
return nil, errors.New("invalid worker capacity")
}
return &Worker{store: store, cfg: cfg}, nil
}
functional options เหมาะกับ public API ที่มี optional settings จำนวนมากและต้องเพิ่ม option โดยไม่ เปลี่ยน signature แต่มีต้นทุน: option อาจขัดกัน, validation กระจาย, discoverability ต่ำกว่า field และ closure อาจถูก reuse อย่างไม่คาดคิด คอร์สนี้จึงไม่ใช้มันเป็น default ทุก constructor
Default ต้องมองเห็นและ Validate ครั้งเดียว
กำหนด default ใน construction boundary แล้ว validate ก่อนเปิด listener/worker อย่าให้ค่า 0
แปลว่า default ใน 3 packageแต่หมายถึง unlimited ในอีก package ถ้า zero มีความหมายถูกต้องให้ใช้มัน;
ถ้าไม่ ให้ normalize เป็น explicit config และ log effective configuration ที่ไม่รวม secret ตอน startup
หลีกเลี่ยง init() สำหรับ network connection หรือ config loading เพราะคืน error ไม่ได้และ test ควบคุม
ลำดับยาก application root ควรสร้าง resource ตามลำดับและปิดย้อนลำดับ ดังจะขยายใน
Service Lifecycle and Observability
Dependency Injection แบบที่เล็กที่สุด
เริ่มด้วย manual constructor injection ใน main เพราะ compiler แสดง graph และ debugging ตรงไปตรงมา:
db := openDB(cfg.Database)
store := mysql.NewBatchStore(db)
service := settlement.NewService(store, time.Now)
handler := httpapi.NewHandler(service)
เมื่อ graph โต, ต้องมี lazy construction, scopes, health checks หรือ coordinated shutdown คอร์สนี้
แนะนำ samber/do/v2 เป็นตัวเลือก production ที่ควรรู้ เพราะใช้
generics, มี lifecycle/scopes และไม่ต้อง code generation แต่ container ต้องอยู่ composition root
ห้ามส่ง *do.Injector เข้า service เพราะจะกลายเป็น service locator และซ่อน dependency กลับไปเหมือนเดิม
ทางเลือกอื่นมี uber-go/fx เมื่อทีมต้องการ application framework/lifecycle ที่ mature และยอมรับ
reflection, uber-go/dig สำหรับ container ต่ำกว่า Fx ส่วน Google Wire ยังพบใน codebase เดิมแต่
repository ถูก archive แล้ว จึงไม่ใช่ default ใหม่ของคอร์สปี 2026
Compatibility เป็นส่วนหนึ่งของ Design
Go 1 compatibility ช่วย source code ที่ถูกต้องให้เดินหน้าข้าม release แต่ package ของเราเองต้องรักษา
contract เช่นกัน การเพิ่ม method ใน exported interface ทำลาย implementation ของ consumer การเปลี่ยน
error wrapping อาจทำให้ errors.Is เริ่ม match หรือหยุด match และการเปลี่ยน nil slice เป็น empty
อาจกระทบ JSON
ก่อนเปลี่ยน public API ให้เพิ่ม behavior ใหม่โดยไม่ทำลายของเดิมเมื่อทำได้, deprecate พร้อม migration, ใช้ semantic import version เมื่อมี v2 ที่ incompatible และทดสอบ client example ไม่ใช่เพียง unit test ภายใน package
Production Toolbox
Default: manual DI + constructor parameters สำหรับ required dependencies + config struct สำหรับ
cohesive options ใช้ functional options เมื่อ compatibility และ optionality คุ้มความซับซ้อน ใช้
samber/do/v2 เมื่อ dependency graph/lifecycle โตจน manual wiring เป็นจุดเสี่ยง ไม่ใช้ container
เป็นข้ออ้างให้ service หา dependency เอง
Checklist การประกอบ Application
- zero value ใช้ได้จริงหรือ constructor ป้องกัน invalid state
- required dependency มองเห็นใน signature
- default ถูก normalize และ validate ก่อนเริ่มรับงาน
- ไม่มี network/config side effect ใน
init() - DI container อยู่เฉพาะ composition root
- option ใหม่รักษา source และ behavior compatibility หรือประกาศ breaking version
- cleanup ownership ถูกบันทึกตั้งแต่ตอนสร้าง resource
อ่านเพิ่ม: Keeping Your Modules Compatible, Go 1 compatibility, Uber Fx และ samber/do documentation