2
3
お題は不問!Qiita Engineer Festa 2024で記事投稿!
Qiita Engineer Festa20242024年7月17日まで開催中!

【Go言語】オプション設定のベストプラクティス

Last updated at Posted at 2024-07-11

はじめに

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などと違い、少し癖があります。

スライド

参考文献

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3