はじめに
Goのプロジェクトで任意な設定フィールドが多くある構造体の初期化ではFunctional Options パターンがよく使われます。
Functional Options パターンとは?
Functional Options パターンは不透明なOption型を使って内部の構造体に情報を渡すパターンです。 可変長引数を受け取り、それらを順に内部のオプションに渡します。コンストラクタや、公開されたAPIで3つ以上の多くの引数が必要な場合、このパターンを使うと良いでしょう。 (https://github.com/knsh14/uber-style-guide-ja/blob/master/guide.md#functional-options より)
構造体の初期化としてはBuilderパターンも一般的に有名でもちろんGo言語でも実装できます。
特にこの2つのデザインパターンに対して同時に言及した記事がなかったので使い勝手とパフォーマンスの観点で比較してみました。
Goのバージョン
go version go1.13.1 darwin/amd64
TL;DR
比較検証した結果、少々乱暴に結論づけると以下になります。
総合 | Functional Optionsパターンの勝ち |
---|---|
使い勝手 | Functional Optionsパターンの勝ち |
パフォーマンス | Builderパターンの勝ち |
Main Content
サンプルコードの構成
サンプルコードを作りました。構成は以下のような感じ。
$ tree
.
├── application.go
├── builder.go
├── functional_option.go
└── main.go
ソースコードのリンクはこちら
共通部分
Functional Optionsパターン(以降FOP)とBuilderパターン(以降BP)の比較にて共通で初期化する構造体が以下のApplication
です。
package main
type Course int
const (
Basic Course = iota
Premium
)
func (c Course) String() string {
switch c {
case Basic:
return "Basic"
case Premium:
return "Premium"
default:
return ""
}
}
// Application は通信キャリアの契約申し込みをイメージしたものです。
type Application struct {
Course Course // ベーシックプランとプレミアプランがある
SubscribeSupportService bool // サポートサービスのオプション
SubscribeMovieService bool // 動画サービスのオプション
SubscribeBackupService bool // データバックアップサービスのオプション
}
Functional Options パターン
FOPでApplication
構造体を初期化する実装は以下のような感じになります。
package main
type Option func(*Application)
func WithSupport(flg bool) Option {
return func(a *Application) {
a.SubscribeSupportService = flg
}
}
func WithMovie(flg bool) Option {
return func(a *Application) {
a.SubscribeMovieService = flg
}
}
func WithBackupService(flg bool) Option {
return func(a *Application) {
a.SubscribeBackupService = flg
}
}
func NewApplicationWithFOP(course Course, ops ...Option) *Application {
a := Application{Course: course}
for _, option := range ops {
option(&a)
}
return &a
}
Builder パターン
BPでApplication
構造体を初期化すると以下のような感じ。
package main
type ApplicationBuilder interface {
WithSupport(flg bool) ApplicationBuilder
WithMovie(flg bool) ApplicationBuilder
WithBackupService(flg bool) ApplicationBuilder
Build() *Application
}
type appBuilder struct {
course Course
subSupport bool
subMovie bool
subBackup bool
}
func (b *appBuilder) WithSupport(flg bool) ApplicationBuilder {
b.subSupport = flg
return b
}
func (b *appBuilder) WithMovie(flg bool) ApplicationBuilder {
b.subMovie = flg
return b
}
func (b *appBuilder) WithBackupService(flg bool) ApplicationBuilder {
b.subBackup = flg
return b
}
func (b *appBuilder) Build() *Application {
return &Application{
Course: b.course,
SubscribeSupportService: b.subSupport,
SubscribeMovieService: b.subMovie,
SubscribeBackupService: b.subBackup,
}
}
func NewApplicationWithBP(course Course) ApplicationBuilder {
return &appBuilder{
course: course,
}
}
main.go
package main
import (
"fmt"
)
func main() {
// Functional Option Pattern (FOP)
fopApp := NewApplicationWithFOP(Premium,
WithBackupService(true),
WithSupport(true),
WithMovie(false),
)
// Builder Pattern (BP)
bpApp := NewApplicationWithBP(Premium).
WithBackupService(true).
WithSupport(true).
WithMovie(false).
Build()
fmt.Printf("%+v\n", fopApp) // &{Course:Premium SubscribeSupportService:true SubscribeMovieService:false SubscribeBackupService:true}
fmt.Printf("%+v\n", bpApp) // &{Course:Premium SubscribeSupportService:true SubscribeMovieService:false SubscribeBackupService:true}
}
使い勝手について
上記のmain.go
の見た目で比較してわかるようにこちらの記事などで考慮されている柔軟性に関してはどちらも同じ性質を持っていそうです。
functional_option.go
とbuilder.go
を比較するとBPの方がBuidlerのインターフェースと構造体を用意する必要があり少しFOPよりも複雑になってしまっています。
パフォーマンスについて
ベンチマークをとってみます。
package main
import (
"testing"
)
// Functional Options Pattern (FOP)
func BenchmarkFunctionalOption(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = NewApplicationWithFOP(Premium,
WithBackupService(true),
WithSupport(true),
WithMovie(false),
)
}
}
// Builder Pattern (BP)
func BenchmarkBuilder(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = NewApplicationWithBP(Premium).
WithBackupService(true).
WithSupport(true).
WithMovie(false).
Build()
}
}
$ go test -bench . -benchmem
BenchmarkFunctionalOption-4 12982681 85.4 ns/op 64 B/op 4 allocs/op
BenchmarkBuilder-4 25243935 44.8 ns/op 32 B/op 2 allocs/op
上記のコードではBPの方がFOPよりも約2倍早く、メモリアロケーションの数も半分であることがわかりました。
アロケーションされてしまうタイミングをGODEBUG=allocfreetrace=1
で見てみ(※1)ると、FOPではWithXXX
の関数実行の度にアロケーションが発生していました。Option
型の値を返すためにアロケーションが必要なようです。
BPではWithXXX
の関数はインスタンス化された構造体の中身を更新するだけなのでアロケーションはなく、New..()
とBuild()
の2回のみアロケーションがされていることを確認しました。
この検証でFOPの弱点を見つけることができました。
(※1) 以下のように実行するとトレース結果が出力されます
$ go build -o a.out; GODEBUG=allocfreetrace=1 ./a.out -test.run=none -test.benchtime=1ms 2>trace.log
結論としては
2つのデザインパターンで利用が想定される構造体の初期化は、プロセスとして始めの1度だけ実行されるようなユースケースという前提であれば使い勝手(メンテしやすさ)が優先されるという理由から、Functional Optionsパターンを使っていけば良さそうです。
参考
FOP
- https://qiita.com/weloan/items/56f1c7792088b5ede136
- https://blog.web-apps.tech/go-functional-option-pattern/
BP
Benchmark / Runtime debugging