บทที่ 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