Hori Blog

フリーランスでバックエンドエンジニアとして活動している Ryota Hori のブログです。
最近はテック系記事より雑記ブログ気味。

golang で汎用的な任意数のオプションを取るメソッドの作り方

grpc の DialOption のオプション設定方法が上手いなーと感心したのですが、初見で理解するのが難しかったので備忘録がてら解説してみます。

やりたいこと

golang にはオーバーロード(同名メソッドでも引数によって呼び出されるメソッドが変わるやつ)がないので、任意のオプションを取るメソッドを作るのに工夫が必要です。

可変引数の仕組みはあるので任意数の値をメソッドに渡したり、オプション用の struct を引数に取ることで実装できなくはないのですがデメリットがあります(後述)。

DialOption がそのへんの葛藤を上手いこと解決していたのでご紹介。

ダメだった例

はじめに検討したのですがよろしく無かった例です。

  • 可変引数でオプションを渡す
  • オプション用の struct を渡す

可変引数でオプションを渡す

golang では可変引数の仕組みがあるので、任意数のオプションを渡すこと自体はできます。

The Go Programming Language Specification - The Go Programming Language

ただ、この場合はオプションとして取れる型が一種類になってしまうので interface{} 型を使うことになるかと思います。

main.go
func Foo(arg1 string, options ...interface{})

この場合、各 option が何を意味しているか判定するには型判定が必要になります。

main.go
type TimeoutOpt time.Duration
type BarOpt     string

type fooOptions struct {
    timeout time.Duration
    bar     string
}

func Foo(arg1 string, opts ...interface{}) {
    opt := fooOptions{}
    for _, o := options {
        switch v := o.(type) {
            case TimeoutOpt:
                opt.timeout = time.Duration(v)
            case BarOpt:
                opt.bar = string(v)
            default:
                // unknown option
        }
    }

    // use opt
}

func main() {
    Foo("baz", TimeoutOpt(time.Second), BarOpt("qux"))
}

気になる点は以下です。

  • interface{} を使っているので未知の値が入ってくる可能性がある
  • case に全オプションの処理を書くので漏れが生じそう

どちらのデメリットも option 用に interface を定義したり、テストを書くことによって防げないこともないのはもちろんですが、 DialOption のほうが綺麗に書けました。

オプション用の struct を渡す

引数に struct を取るパターンです。

main.go
type FooOptions struct {
    Timeout time.Duration
    Bar     string
}

func Foo(arg1 string, opt fooOptions) {
    // use opt
}

func main() {
    Foo("baz", FooOptions{
        Timeout: time.Second,
        BarOpt:  "qux",
    })
}

シンプルでいいのですが、 option が不要な場合に空の struct (ポインタなら nil )を渡すことになります。

main.go
func main() {
    Foo("baz", FooOptions{})
}

ちょっとダサい。

DialOption の実装

DialOption を参考に実装すると以下のようになります。

main.go
type fooOptions struct {
    timeout time.Duration
    bar     string
}

type FooOption func(*fooOptions)

func WithTimeout(timeout time.Duration) FooOption {
    return func(ops *fooOptions) {
        ops.timeout = timeout
    }
}

func WithBar(bar string) FooOption {
    return func(ops *fooOptions) {
        ops.bar = bar
    }
}

func Foo(arg1 string, options ...FooOption) {
    opt := fooOptions{}
    for _, o := range options {
        o(&opt)
    }

    // use opt
}

func main() {
    Foo("baz", WithTimeout(time.Second), WithBar("qux"))
}

WithXXX らへんが独自定義の struct を引数をとるメソッドを返しているのでちょっとわかりづらいですが、 main を見ると型安全に任意数のオプションが取れているのが分かるかと思います。

また、 Foo メソッド内も for 文で回すだけのシンプルな実装です。

オプションの型はメソッド型

main.go
type FooOption func(*fooOptions)

この FooOption

main.go
func(ops *fooOptions) {
    ops.timeout = timeout
}

のように struct の fooOptions をポインタで受けて書き換えるメソッドの型です。メソッドなので

main.go
func Foo(arg1 string, options ...FooOption) {
    opt := fooOptions{}
    for _, o := range options {
        o(&opt)
    }

    // use opt
}

o(&opt) のように fooOptions のポインタを引数に取れば値を設定することができます。

メソッドとして定義したオプションを可変引数としてとることで、オプションの中身が何であるかを気にせずに単純な for 文によって適用できる仕組みです。

WithXXX はクロージャを利用して関数リテラルを返す

そして肝となる WithXXX ですが、

main.go
func WithTimeout(timeout time.Duration) FooOption {
    return func(ops *fooOptions) {
        ops.timeout = timeout
    }
}

こちらは JavaScript などでよく見かけるクロージャの仕組みを利用しています。

関数リテラルのスコープにある変数は関数内から参照することができるため、 WithTimeout(timeout time.Duration) の引数である timeout 値を、返却した FooOption 関数から参照することができます。

つまり、

main.go
WithTimeout(time.Second)

とすることで

main.go
func(ops *fooOptions) {
    ops.timeout = time.Second
}

と等価な関数リテラルが返却され、 WithTimeout(time.Second) の引数で渡された値を fooOptions struct に設定する FooOption メソッドが取得できます。

オプションとして渡す値を関数リテラルで統一し、設定の詳細と設定値は関数リテラル内に閉じ込めることで、型安全で汎用的なオプション設定が実現できました。

以上

クロージャを利用した関数リテラルを返却することでオプション設定とする、という発想がなかったので非常に勉強になりました。

使い所としては任意オプションが欲しくなるところ全般ですが、 exec.Command を使った外部コマンド実行のオプション設定時などに使うと便利ですね。

業務で go 1.5 時代からの試行錯誤コードをリファクタリングしているところなので、また色々と気づいたことをメモしていきたいと思います。