Echoで環境変数を使い回す

状況

Echoでデータベースに接続するときなどに環境変数を使ってデータベースのホスト名などの情報を取得したい。

問題

必要なときに都度os.Getenvで環境変数の値を取得すると、各ハンドラーで同じようなコードを何度も書くことになる。

また、環境変数が設定されていないときのデフォルト値を設定したい場合やstring以外の型に変換したい場合、さらにコード量が増えてしまう。

解決

kelseyhightower/envconfigを使って環境変数を簡単に扱えるようにし、すべてのハンドラーからカスタムコンテキストを通して環境変数にアクセスできるようにした。

// config.go
type Config struct {
  DatabaseHost     string `split_words:"true"`
  DatabaseName     string `split_words:"true"`
  DatabasePassword string `split_words:"true"`
  DatabasePort     int    `split_words:"true"`
  DatabaseUser     string `split_words:"true"`
}
// server.go
func main() {
  var config Config
  err := envconfig.Process("", &config)
  if err != nil {
    log.Fatal(err.Error())
  }

  // ...
}
  • DATABASE_HOSTのような環境変数にConfigという構造体からアクセスできるようにしている。config.DatabaseHostのようにアクセスできるようになる。string型であればos.Getenvでも問題ないけど、int型やbool型の場合は変換処理が面倒なのでenvconfigを使っている。
  • split_words="true"というアノテーションをつけることで、スネークケースからキャメルケースに変換している。
  • envconfig.Processの第1引数は環境変数のプレフィックスになっている。envconfig.Processs("database", &config)とすると、config.Hostで環境変数DATABASE_HOSTにアクセスできるようになる。必要なければ空文字でいい。
// custom_context.go
type CustomContext struct {
  echo.Context
  Config
}

func CustomContextMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
  return func(c echo.Context) error {
    cc := &CustomContext{c}
    return next(cc)
  }
}

func ConfigMiddleware(config Config) echo.MiddlewareFunc {
  return func(next echo.HandlerFunc) echo.HandlerFunc {
    return func(c echo.Context) error {
      cc := c.(*CustomContext)
      cc.Config = config
      return next(cc)
    }
  }
}
// server.go
func main() {
  // ...

  e := echo.New()
  e.Use(CustomContextMiddleware)
  e.Use(ConfigMiddleware(config))

  // ...
}
  • すべてのハンドラーからConfigにアクセスできるようにカスタムコンテキストを用意し、そのフィールドにConfigを追加する。
  • カスタムコンテキストをデフォルトのコンテキストで拡張するため、middlewareを設定している。さらに、上で初期化したConfigをカスタムコンテキストのフィールドに追加するためのmiddlewareも設定している。

このように実装することで、以下のように簡単に環境変数にアクセスできるようになる。

// server.go
func main() {
  // ...

  e.GET("/tasks", getTasks)

  // ...
}

func getTasks(c echo.Context) error {
  cc := c.(*CustomContext)
  dsn := cc.GetDSN()

  // ...
}
// config.go
func (c Config) GetDSN() string {
  return fmt.Sprintf(
    "%s:%s@tcp(%s:%i)/%s",
    c.DatabaseUser,
    c.DatabasePassword,
    c.DatabaseHost,
    c.DatabasePort,
    c.DatabaseName,
  )
}
  • ConfigCustomContextの匿名フィールドなので、CustomContextから直接ConfigのメソッドであるGetDSNを呼ぶことができる。
  • 上で説明したとおり、c.DatabaseUserなどは環境変数DATABASE_USERなどから値を取得している。