0.2 組合優先:小介面與明確依賴
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 擁有 CreatedAt 與 UpdatedAt。這適合重用資料欄位,但不代表 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。這種替換的目標是讓測試只覆蓋當前邊界的行為。