🎯 什麼情境該想到我

當你「一個東西有很多可選參數,而 New(a, b, nil, nil, true, nil) 已經沒人看得懂」的時候。
★ 在 Go 裡,這個模式有一個更好的變形叫 Functional Options——那是本頁最重要的內容。

⚙️ 怎麼用

意圖

把複雜物件的「建構過程」與它的「表示」分離,讓同樣的建構過程能產生不同的表示。

它解決兩個具體痛點:

  1. 伸縮建構子(telescoping constructor) —— New(a)、New(a,b)、New(a,b,c)… 無限膨脹
  2. 參數意義不明 —— 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:

經典 BuilderFunctional 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。
    type Config struct { Timeout time.Duration; MaxConn int }
    func NewServer(addr string, cfg Config) *Server { ... }
    優點:更簡單、零值即預設(要小心零值語義)、可以序列化成 YAML/JSON。
    缺點:無法區分「沒設」和「設成零值」(要用指標或 sql.Null* 之類)、加欄位時若呼叫端用具名欄位仍相容但用位置初始化就壞。
    → 函式庫給外部用 → Functional Options;內部程式碼 → config struct 通常更務實。
  • Options 的套用順序有意義。後面的會覆寫前面的。如果有選項互相衝突,要在 New 的最後做一次驗證。
  • 不要讓 Option 有副作用(開連線、讀檔)。它應該只是設定欄位;副作用留給 New 或明確的 Start()。
  • strings.Builder 不是這個模式,別混淆。
  • 在 TS 裡幾乎永遠不需要這個模式(見上)。

🔗 相關工具