Go言語でpythonやscalaのようなDefault引数を実現する


はじめに

Dave Cheney氏のFunctional options for frendly APISを読みました。
https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis
この記事で学んだデザインパターンを活かして問題を解決してみたいと思います。

Go言語にはpythonやscalaのようなデフォルト引数を指定することはできません。
ですが、構造体の初期化時に引数を省略可能にしたい場合ありませんか?

この問題を解決するために最善な解決策までをプロセスを交えながら説明していきます。

解決したい問題

以下のようなfunctionをイメージしてください。

func NewServer(addr string, clientTimeout time.Duration, maxconns int, maxconcurrent int, cert *tls.Cert) (*Server, error)

簡単に使えそうでしょうか?
このfunctionにはいくつかの問題を感じます。
・テストの時などtimeout値やTLSはなんでも良いのにわざわざ利用者はこの値を設定する必要がある。
・引数が増えた時に、このfunctionを使ってる箇所を全て修正する必要がある
・設定する値を知るために仕様を理解する必要がある。
・etc...

これらの問題を解決するためにどのような策が有効でしょうか。

解決策1 コンストラクタで解決

コンストラクタで解決出来そうです。

func NewServer() (*Server, error)

func NewServer(addr string) (*Server, error)

func NewServer(addr string, clientTimeout time.Duration) (*Server, error)

func NewServer(addr string, clientTimeout time.Duration, maxconns int) (*Server, error)

func NewServer(addr string, clientTimeout time.Duration, maxconns int, maxconcurrent int) (*Server, error)

func NewServer(addr string, clientTimeout time.Duration, maxconns int, maxconcurrent int, cert *tls.Cert) (*Server, error)

どうでしょうか?
6つの引数が存在するので、6*6=36通りのコンストラクタを用意すれば、柔軟に引数の設定が出来そうですね!!
しかし、36通りとなると...用意するのも面倒ですが、一部変更することで全て変更する必要が出てきてしまいます。

解決策2 Config構造体で解決

ならConfig構造体で解決するのはどうでしょうか?

type Config struct {
    Timeout time.Duration
    Cert *tls.Cert
    // ・・・
}

func NewServer(addr string, config Config) {
    // ...
}
func main() {
    srv, err := NewServer("localhost", Config{})
    // ...
}

Configで値を自由に設定し、渡すことが可能になりました。
ただ、値を設定しない時は、nilや空の構造体を渡す必要があります。
値を設定する必要がないなら何も引数に渡したくない・・・

解決策3 Config構造体を可変長引数で解決

Config構造体を可変長引数で渡したらどうでしょうか?

func NewServer(addr string, config ...Config) (*Server, error) {
    // ...
}
func main() {
    srv, err := NewServer("localhost")
    // ...
}

ついに解決しましたね!!
と思いきや、こんな使い方をする人を想定していませんでした。

func main() {
    srv, err := NewServer("localhost", Config{/* ・・・ */}, Config{/* ・・・ */})
    // ...
}

コメントに「Configの引数は1つのみにしてください」と書けば良いでしょうか?
機能を提供していながら、利用者に色々考えさせる設計って微妙ですよね。

解決策4 Functional Optionsで解決

Fuctional Option Patternは構造体の初期化時にオプション引数を与えるためのデザインパターンです。

type Option func(*Server)

func Timeout(t int) Option {
    return func(s *Server) {
        s.Timeout = time.Duration(t) * time.Second
    }
}

// 他にも設定したいパラメータを上記のように記載する

func NewServer(addr string, options ...Option) (*Server, error) {
    l, err := net.Listen("tcp", addr)
    if err != nil {
        return nil, err
    }

    srv := Server{listener: l}

    for _, option := range options {
        option(&srv)
    }
    return &srv, nil
}
func main() {
    srv, err := NewServer("localhost", Timeout(30))
}

ついに可変長引数でパラメータを渡すことが出来ました。
これがデフォルト引数を使った構造体生成への最善の解決策です。

それでは振り返りとして何が解決されたのかまとめです。
・1つ1つのパラメータを独立して設定する事が出来る
・引数を設定しなくても、defaultの設定値が使われる
・記述量も多くなく読みやすい
・引数が拡張されても、使う側に影響がない
・nilや空の構造体を渡す必要がない

まとめ

Go言語のようにデフォルト引数を設定する機能が提供されていなかったとしても、
ここで示した解決策のように自分でdefault引数機能を作る事が出来る事がわかりました。