はじめに
HTTPサーバを立ち上げる際、アドレスとポートを設定する一番思いつきやすいのは以下のような形である。
func NewServer(addr string, port int) (*http.Server, error) {
//
}
ここで、GinのNew() をみてみると以下のような形となっている。
func New(opts ...OptionFunc) *Engine
この記事では、このオプションの設定方法のベストプラクティスを解説していきます。
一番シンプルなオプション設定方法
オプションの書き方を知らない人がやってしまいがちなオプションの設定方法をすると、どうなるかを見てみます。
func NewServer(addr string, port int) (*http.Server, error) {
//
}
前提として、NewServer()
を公開する場合を考えます。
クライアントがNewServer("0.0.0.0", 80)
とこのNewServerを使用しているとする。Debugするかどうかを設定できるように以下のように改善した。
func NewServer(addr string, port int, debug bool) (*http.Server, error) {
//
}
すると、クライアント側のNewServer()は引数が足りず、壊れてしまいます。
つまり、関数に他のパラメータを追加すると、関数の呼び出し側でも変更が必要となるという問題が発生します。
Config構造体
以下のコードのようにオプションを Config構造体として渡します。
type Config struct {
Port int
}
func NewServer(addr string, cfg Config) {
}
すると、新たなパラメータを追加したい場合は、Configに追加すればよく、クライアント側に影響も発生しません。
実際にDebugフィールドを追加してみます。
type Config struct {
Port int
Debug bool
}
func NewServer(addr string, cfg Config) {
}
このとき、クライアント側で 以下のように呼び出しても問題ありません。
config := Config{
Port: 3,
}
NewServer("0.0.0.0", config)
しかし、これには問題があります。
指定しなかったパラメータにはゼロ値が入ります。(上記の場合だったら、Debug=false
で初期化される。)これは意図的に0を代入したのか、未入力なのかの違いが分かりません。
ポインタを使用すると、未入力の場合はnilになるので、上記の問題は解決できます。
ただし、ポインタを使用した場合、例えばportのように整数のポインタを渡す場合、使い勝手が悪くなります。
port := 0
config := Config {
Port: &port,
}
また、すべてのオプションをデフォルトで使用したい場合、以下のように空の構造体を渡す必要があり、見栄えが悪くなります。
NewServer("0.0.0.0", Config{})
Builderパターン
GoFのデザインパターンの1つであるBuilderパターンを用いることで、柔軟性の高い解決策を提供される。Builderパターンの書き方も複数ある。
const defaultHTTPPort = 8080
// Config構造体
type Config struct {
Port int
}
// オプションのポートを含むConfigBuilder構造体
type ConfigBuilder struct {
port *int
}
// ポートを設定する公開メソッド
func (b *ConfigBuilder) Port(port int) *ConfigBuilder {
b.port = &port
return b
}
// config構造体を作成するためのBuildメソッド
func (b *ConfigBuilder) Build() (Config, error) {
cfg := Config{}
// ポート管理の主要ロジック
if b.port == nil {
cfg.Port = defaultHTTPPort
} else {
if *b.port == 0 {
cfg.Port = randomPort()
} else if *b.port < 0 {
return Config{}, errors.New("port should be positive")
} else {
cfg.Port = *b.port
}
}
return cfg, nil
}
func NewServer(addr string, config Config) (*http.Server, error) {
return nil, nil
}
func randomPort() int {
return 4 // Chosen by fair dice roll, guaranteed to be random.
}
設定メソッドがBuilder自身を返すので、builder.Foo("foo").Bar("bar")
のように設定することが可能になる。
func client() error {
builder := ConfigBuilder{} // ConfigBuilderの作成
builder.Port(8080) // 設定値のセット
config, err := builder.Build() // Config構造体の作成
if err != nil {
return err
}
server, err := NewServer("localhost", config)
if err != nil {
return err
}
_ = server
return nil
}
しかし、Config構造体と同じようにデフォルトの設定を使用したい場合は空の構造体を渡す必要があり、見栄えが悪い。
server, err := NewServer("localhost", Config{})
ポートが無効である時に正しく対処する場合、エラー処理が複雑になる可能性がある。
関数オプションパターン
関数オプションパターンは可変数個引数に依存する方法。Goでは関数オプションパターンを使用する方法が慣用的。
WithPortはクロージャを返す。クロージャ : その本体の外からの変数を参照する無名関数。
type options struct {
port *int
}
type Option func(options *options) error
func WithPort(port int) Option {
return func(options *options) error {
if port < 0 {
return errors.New("port should be positive")
}
options.port = &port
return nil
}
}
func NewServer(addr string, opts ...Option) ( *http.Server, error) {
var options options
for _, opt := range opts {
err := opt(&options)
if err != nil {
return nil, err
}
}
// この段階で、options 構造体が構築され、構成が含まれる
// したがって、ポート設定に関連するロジックを実装できる
var port int
if options.port == nil {
port = defaultHTTPPort
} else {
if *options.port == 0 {
port = randomPort()
} else {
port = *options.port
}
}
// ...
}
呼び出し方は、以下のようになる。
server, err := NewServer("0.0.0.0", WithPort(8080), WithTimeout(time.Second))
関数オプションパターンでは、デフォルトの設定を使用したい場合、引数を渡す必要がない。
server, err := NewServer("0.0.0.0")
Dysfunctional Options Pattern
Functional Options Patternについて調べていると、Dysfunctional Options Pattern
というパターンを見つけました。
この記事を読むと、関数オプションパターンは中間層が多く、複雑であるということを問題に挙げている。
type config struct {
foo, bar string
fizz, bazz int
}
func (c *config) WithFizz(fizz int) *config {
c.fizz = fizz
return c
}
func (c *config) WithBazz(bazz int) *config {
c.bazz = bazz
return c
}
func NewConfig(foo, bar string) *config {
return &config{foo, bar, 10, 100}
}
func Do(c *config) {}
そして、クライアント側では以下のように呼び出しております。
func main() {
c := src.NewConfig("hello", "world").WithFizz(0).WithBazz(42)
src.Do(c)
}
察しの言い方はもう気付いたと思いますが、これは一種のBulderパターンです。
故に、デフォルト値を使用したい場合は、空の構造体を渡す必要があります。
func main() {
Do(&config{})
}
結局どれがいいのか
Goで一番慣用的で好まれるものは、関数オプションパターンです。Builderパターンは実装しやすいという利点がありますが、Javaなどと違い、少し癖があります。
スライド
参考文献
- Japanese Version - 100 Go Mistakes and How to Avoid Them
- 【Go】Builder Patternで実装してみた
- GoでFunctional Options Patternを使うとモックで引数の比較ができない問題に対応したい
- Unlocking the Power of Functional Options Pattern in Go | Medium
- Dysfunctional options pattern in Go | Redowan's Reflections
- guide/style.md at master · uber-go/guide
- Go の機能不全なオプションパターン : r/golang