🎯 什麼情境該想到我
當你「一個東西有很多可選參數,而 New(a, b, nil, nil, true, nil) 已經沒人看得懂」的時候。
★ 在 Go 裡,這個模式有一個更好的變形叫 Functional Options——那是本頁最重要的內容。
⚙️ 怎麼用
意圖
把複雜物件的「建構過程」與它的「表示」分離,讓同樣的建構過程能產生不同的表示。
它解決兩個具體痛點:
- 伸縮建構子(telescoping constructor) ——
New(a)、New(a,b)、New(a,b,c)… 無限膨脹 - 參數意義不明 ——
New("x", 30, true, false, nil)的第三個true是什麼?
★ Go 的長相:Functional Options(Rob Pike 提出,Go 社群的標準解法)
type Server struct {
addr string
timeout time.Duration
maxConn int
tls *tls.Config
logger *slog.Logger
}
// ★ Option 是「一個會修改 Server 的函式」
type Option func(*Server)
func WithTimeout(d time.Duration) Option { return func(s *Server) { s.timeout = d } }
func WithMaxConn(n int) Option { return func(s *Server) { s.maxConn = n } }
func WithTLS(c *tls.Config) Option { return func(s *Server) { s.tls = c } }
func WithLogger(l *slog.Logger) Option { return func(s *Server) { s.logger = l } }
func NewServer(addr string, opts ...Option) *Server {
s := &Server{ // ★ 先放預設值
addr: addr,
timeout: 30 * time.Second,
maxConn: 100,
logger: slog.Default(),
}
for _, opt := range opts { // ★ 再依序套用覆寫
opt(s)
}
return s
}
// 使用:★ 每個參數都自我說明,順序無關,不需要的就不寫
srv := NewServer(":8080",
WithTimeout(5*time.Second),
WithMaxConn(1000),
)★ 為什麼 Go 選這個而不是經典 Builder:
| 經典 Builder | Functional Options | |
|---|---|---|
| 需要額外型別 | Builder struct + Build() | 只要一個 func 型別 |
忘記呼叫 Build() | ❌ 拿到半成品 | ✅ 不可能發生 |
| 必填 vs 選填 | 都在 builder 裡,分不出來 | ★ 必填當一般參數、選填當 option——型別簽章就說清楚了 |
| 預設值 | 要在 builder 初始化 | 在 New 裡集中一處 |
| 可否回報錯誤 | 在 Build() 回 | 見下方變體 |
| 向後相容 | 加方法 | ★ 加一個 WithX 不影響任何既有呼叫端 |
★ 最後一列是它真正的殺手鐧:
opts ...Option是可變參數,所以「新增選項」永遠不會破壞既有程式碼。
這對函式庫作者是巨大的價值——grpc.Dial、http.Server、AWS SDK v2 全部採用這個模式。
能回報錯誤的變體(選項本身可能無效時):
type Option func(*Server) error
func WithMaxConn(n int) Option {
return func(s *Server) error {
if n <= 0 { return fmt.Errorf("maxConn must be > 0, got %d", n) }
s.maxConn = n
return nil
}
}
func NewServer(addr string, opts ...Option) (*Server, error) {
s := &Server{ /* 預設值 */ }
for _, opt := range opts {
if err := opt(s); err != nil { return nil, err }
}
return s, nil
}TypeScript 的長相:物件字面量就夠了
type ServerOptions = {
timeout?: number;
maxConn?: number;
tls?: TlsConfig;
logger?: Logger;
};
const defaults = { timeout: 30_000, maxConn: 100 } satisfies Partial<ServerOptions>;
export function createServer(addr: string, opts: ServerOptions = {}) {
const cfg = { ...defaults, ...opts }; // ★ 展開即可,不需要任何模式
...
}
createServer(":8080", { timeout: 5_000, maxConn: 1000 });★ TypeScript 根本不需要 Builder 模式。
具名的可選屬性 + 物件展開 + 預設值,語言層面就解決了 GoF 要用一整個類別解決的問題。
加上satisfies可以在不放寬型別的前提下檢查預設值(見 工具-satisfies與型別標註)。這是「模式是語言缺陷的補丁」最清楚的例子之一。
什麼時候仍該用「真正的」Builder
經典 Builder 的核心價值是分步驟組裝與同一流程產生不同表示:
- 查詢建構器:
db.Select("a").From("t").Where(...).Limit(10).Build()——
★ 這裡的鏈式呼叫有語義順序,不是單純的參數設定 - 逐步解析/組裝(解析器邊讀邊建 AST)
- 不可變物件的累積建構
生態中的真實案例
| 案例 | 模式 |
|---|---|
grpc.Dial(target, grpc.WithTransportCredentials(...), ...) | ★ Functional Options |
AWS SDK for Go v2 的 func(*Options) | Functional Options |
slog.HandlerOptions | ★ 單純用 struct 也可以——見下方 |
strings.Builder | ⚠️ 名字叫 Builder 但不是這個模式(它是效能優化,避免字串拼接的重複配置) |
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
- ★ 參數少(≤ 3 個)就不要用。
NewServer(":8080", 5*time.Second)很清楚,硬套 options 只是噪音。 - ★ 更簡單的替代:直接傳一個 config struct。
優點:更簡單、零值即預設(要小心零值語義)、可以序列化成 YAML/JSON。type Config struct { Timeout time.Duration; MaxConn int } func NewServer(addr string, cfg Config) *Server { ... }
缺點:無法區分「沒設」和「設成零值」(要用指標或sql.Null*之類)、加欄位時若呼叫端用具名欄位仍相容但用位置初始化就壞。
→ 函式庫給外部用 → Functional Options;內部程式碼 → config struct 通常更務實。 - Options 的套用順序有意義。後面的會覆寫前面的。如果有選項互相衝突,要在
New的最後做一次驗證。 - 不要讓 Option 有副作用(開連線、讀檔)。它應該只是設定欄位;副作用留給
New或明確的Start()。 strings.Builder不是這個模式,別混淆。- 在 TS 裡幾乎永遠不需要這個模式(見上)。
🔗 相關工具
- 工廠方法模式 —— 建立單一物件但不需要複雜組裝時
- 抽象工廠模式 —— 建立一族物件
- 原型模式 —— 用複製既有物件取代重新建構
- 工具-satisfies與型別標註 —— TS 版本的預設值檢查
- 工具-tsconfig基準設定 —— 可選屬性的嚴格度設定
- 工具-介面設計原則 —— 參數設計
- 設計模式