1. はじめに
Go 言語は 1.0 の頃から関数を 第一級オブジェクト(first-class
citizen)
として扱うことができ、さらにクロージャもサポートしています。
そのため、Option パターンは特定のバージョン機能に依存せず、Go の基本機能だけで実現できます。
2014 年、Go コミュニティで有名な Dave Cheney 氏がブログ
「Functional Options for Friendly APIs」
にて初めて体系的に提案し、その後 Go
コミュニティの慣用的な書き方として広まりました。
2. 背景
従来の Go のコンストラクタでは次のような課題があります:
- Go には デフォルト引数 や 関数オーバーロード が存在しない
- 引数が 3〜4 個を超えると、呼び出し側の可読性が急激に低下する
- 新しい引数を追加する際に、新しいコンストラクタを追加せざるを得ず、API
が肥大化する - デフォルト値を優雅に扱えない
- 課題:コンストラクタの引数が増えるほど可読性・保守性が悪化する
- 解決策:関数型プログラミングの発想を応用し、引数を「オプションの関数片」にカプセル化する
type Server struct {
Host string
Port int
Timeout int
Password string
Option string
}
func NewServer(host string, port int, timeout int, password string, option string) *Server {
return &Server{Host: host, Port: port, Timeout: timeout, Password: password, Option: option}
}
// 引数が長すぎてミスしやすい
serv := NewServer("0.0.0.0", 8080, 30, "111111", "option")
new で Server
を作ると長すぎてミスしやすいし、読みづらい。もっと簡潔で分かりやすく書けないでしょうか?
→ はい、それを解決するのが Option パターン です。
3. 使い方
Option 型(関数型)の定義
type Option func(*Server)
type Server struct {
Host string
Port int
Timeout int
}
SetXxx
に相当するクロージャ関数を提供
func WithHost(host string) Option {
return func(s *Server) {
s.Host = host
}
}
func WithPort(port int) Option {
return func(s *Server) {
s.Port = port
}
}
func WithTimeout(timeout int) Option {
return func(s *Server) {
s.Timeout = timeout
}
}
プリセットの「デフォルト値」も簡単に定義できます:
func WithTimeoutTypeA() Option {
return func(s *Server) {
s.Timeout = 30
}
}
s := NewServer(
WithTimeoutTypeA(), // デフォルト値を適用
WithTimeout(60), // 上書き
)
⚠️ Option は適用順に上書きされるため、指定の順序に注意が必要です。
コンストラクタで Option を適用
func NewServer(opts ...Option) *Server {
s := &Server{
Host: "127.0.0.1",
Port: 8080,
Timeout: 30,
}
for _, opt := range opts {
opt(s)
}
return s
}
使用例
// デフォルト設定
s1 := NewServer()
// カスタム設定
s2 := NewServer(
WithHost("0.0.0.0"),
WithPort(9090),
WithTimeout(60),
)
4. 解決できる問題
従来の書き方 → Option パターンの比較
// 従来:必ず全引数を渡す必要あり
s := NewServer("127.0.0.1", 8080, 30)
// Option パターン:引数に意味が付与される
s := NewServer(
WithHost("127.0.0.1"),
WithTimeout(45),
)
- コードの意味が一目で分かる
- 新しいパラメータを追加しても
WithXxx
を増やすだけで既存の呼び出しは壊れない - デフォルト値がコンストラクタに内蔵され、呼び出し側は省略できる
5. 有名プロジェクトでの採用例
gRPC-Go クライアント
gRPC-Go
のソースコード
では DialOption
が定義されています:
type dialOptions struct {
block bool
returnLastError bool
timeout time.Duration
...
}
// インターフェース型 Option の定義
type DialOption interface {
apply(*dialOptions)
}
// ファクトリーラッパ
type funcDialOption struct {
f func(*dialOptions)
}
func (fdo *funcDialOption) apply(do *dialOptions) {
fdo.f(do)
}
func newFuncDialOption(f func(*dialOptions)) *funcDialOption {
return &funcDialOption{f: f}
}
// 公開されている関数
func WithInsecure() DialOption {
return newFuncDialOption(func(o *dialOptions) {
o.insecure = true
})
}
func WithBlock() DialOption {
return newFuncDialOption(func(o *dialOptions) {
o.block = true
})
}
使用例:
conn, err := grpc.Dial("localhost:40001",
grpc.WithInsecure(),
grpc.WithBlock(),
)
-
grpc.Dial
には認証、LB、インターセプタなど大量のオプションが存在する - Option パターンを インターフェース型
で実装しており、関数型との違いは定義の仕方だけ。使い方は同じ - 大規模ライブラリに適しており、柔軟な拡張性を提供できる
AWS / GCP SDK
AWS SDK for Go でも WithRegion
、WithCredentials
などのオプション指定が採用されています。
6. まとめ
- Option パターンは Go コミュニティにおける「ベストプラクティス」
- 本質は 関数型プログラミングの発想を用いた柔軟な API 設計
- 解決できる課題:
- コンストラクタの 引数爆発問題
- デフォルト値の管理
- 拡張性の不足
- デメリット:デバッグ時に Option
の呼び出しチェーンを追う必要があり、やや複雑になる - しかし 可読性・保守性の大幅な向上
に比べれば、十分受け入れ可能なコス - 将来的に Go
が「名前付き引数」や「デフォルト引数」を導入すれば部分的に置き換わる可能性はあるが、現状では最良の解決策といえる
7. 参考資料
-
Dave Cheney, Functional Options for Friendly APIs, 2014
https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis -
Dave Cheney, dotGo 2014 - Functional Options for Friendly
APIs(講演動画)
https://www.youtube.com/watch?v=24lFtGHWxAQ