Go 組合的核心原則是用小型型別與小介面拼出行為。程式需要什麼能力,就依賴那個能力;型別擁有哪些資料,就把資料明確放在 struct 裡。這種設計讓高併發服務更容易拆解責任,也更容易在選型成立時維持邊界清楚。

為什麼這章在第零章

當你的工作負載本來就適合 Go 時,真正需要確認的就不只是語法,而是 Go 如何幫你把服務邊界維持清楚。組合優先讓 main()、constructor、handler、worker 與 repository 的責任能被直接看見,這是 Go 在服務型專案中很重要的可維護性來源。

組合先描述擁有什麼

struct 的核心責任是把一組資料與依賴放在同一個明確邊界內。Go 不用 class inheritance 表達「某個型別繼承另一個型別」,而是用欄位組合出需要的結構。

1type Logger interface {
2 Info(message string)
3}
4
5type Server struct {
6 addr   string
7 logger Logger
8}

Server 擁有一個地址,也依賴一個 logger。這些資訊都在欄位上直接呈現,讀者不需要追蹤隱式容器或父類別初始化順序。

當依賴變多時,struct 仍然應該只保留這個型別真正需要的依賴。把整個 application container 塞進 struct,通常會讓依賴邊界變模糊。

小介面先描述需要什麼

interface 的核心責任是描述呼叫端需要的行為。Go 的介面通常很小,常見的好介面只有一到三個方法。

1type UserFinder interface {
2 FindUser(ctx context.Context, id string) (User, error)
3}
4
5type UserHandler struct {
6 finder UserFinder
7}

UserHandler 不需要知道資料來自資料庫、快取或遠端 API。它只需要「可以用 id 找使用者」這個能力,因此介面只放 FindUser

介面應由替換、測試或隔離需求推動。只有一個具體型別,而且沒有測試替身或替換需求時,先直接依賴具體型別通常更簡單。過度抽象的代價是把原本簡單的依賴藏成難追蹤的間接關係,反而比直接依賴更難閱讀。

依賴由外層組裝

Go 應用組裝依賴的核心策略是讓外層建立具體型別,內層只接收自己需要的依賴。常見位置是 main() 或專門的 constructor。

 1func NewUserHandler(finder UserFinder) UserHandler {
 2 return UserHandler{finder: finder}
 3}
 4
 5func main() {
 6 db := NewDatabase("postgres://localhost/app")
 7 repository := NewUserRepository(db)
 8 handler := NewUserHandler(repository)
 9
10 http.HandleFunc("/users/", handler.ServeHTTP)
11}

這段程式讓資料流與依賴關係保持可見:repository 依賴 db,handler 依賴 repository 的查詢能力。Go 的組合方式偏好把這些關係寫出來,而不是藏在 framework magic 裡。

行為可以用 embedding 重用

embedding 的核心用途是把一個型別的欄位或方法提升到外層型別。它是組合工具;若需要表達明確擁有關係,named field 會比繼承式心智模型更清楚。

 1type AuditFields struct {
 2 CreatedAt time.Time
 3 UpdatedAt time.Time
 4}
 5
 6type User struct {
 7 ID    string
 8 Email string
 9 AuditFields
10}

User 透過 embedding 擁有 CreatedAtUpdatedAt。這適合重用資料欄位,但不代表 User 在概念上「繼承」了 AuditFields 的完整行為。

embedding 應該用在語意自然的地方。若提升方法會讓外層型別出現不該公開的能力,明確寫欄位名稱通常更安全。

組合讓測試替換更自然

組合的測試價值是可以替換依賴,而不需要啟動整個系統。只要 production code 依賴小介面,測試就能提供 fake。

 1type fakeUserFinder struct {
 2    user User
 3    err  error
 4}
 5
 6func (f fakeUserFinder) FindUser(ctx context.Context, id string) (User, error) {
 7 if f.err != nil {
 8     return User{}, f.err
 9 }
10 return f.user, nil
11}

測試 handler 時,可以把 fakeUserFinder 傳進去,專注檢查 HTTP response。這種替換的目標是讓測試只覆蓋當前邊界的行為。