Help us understand the problem. What is going on with this article?

[翻訳+α] Go言語の設定ファイルライブラリ Viper

More than 3 years have passed since last update.

はじめに

自分のGoプログラムで設定ファイルやJSON周りのコードがどんどん膨れ上がって困り果てていたところ、Viperという、設定ファイルを扱える強力なライブラリを今頃知って、打ち震えた。謳い文句どおり、欲しいと思ってた機能が全部揃っている。世界に代わって泣いた。

この作者はCobraというコマンドラインの引数やオプションを扱うための総合ライブラリも公開していて、この種のライブラリをこいつらだけで終わらせるほどの勢い。これも今度訳しつつ補ってみよう。

ViperもCobraも、同じ作者のpflagというPOSIX/GNUスタイルのコマンドラインフラグライブラリを下敷きにしているっぽい。鬼や。この人は設定ファイルの鬼や。

ViperとCobraで、設定ファイルの読み書きからコマンドラインフラグ、引数処理までひととおりカバーしている。これはもうぜひまとめて標準ライブラリに昇格させて欲しい。他の言語でも、入出力ライブラリはこのぐらいの完成度のものを最初から備えていて欲しいと切に願う。もう車輪の再発明はやりたくない。

以下、自分用にReadmeを超ざっくざくに訳しつつ情報を補ってみる。

追記: ビルド時に設定や環境情報の一部をアプリ内に巻き取りたいのだけど、Viperだけでできるだろうか。go-bindataと組み合わせる必要がありそう。情報求む。
その後いろいろやってみたのだけど、go-bindata バイナリ内のJSONから Viper で読み出す方法は今のところわからずじまい。うーむ。


Viper Readme

viper logo

Go言語の設定ファイルがついに牙を剥く!

Build Status Join the chat at https://gitter.im/spf13/viper

Viperについて

Viperは、Twelve Factor appなどのGoアプリケーションにおける設定ファイル関連を一手に引き受ける万能ソリューションです。ライブラリとしてアプリケーション内での利用を想定しており、アプリケーションで設定ファイルのあらゆるニーズやファイル形式を扱えます。使える機能は以下のとおりです。

  • デフォルト値の設定
  • 設定ファイル(JSON、TOML、YAML、HCL)の読み込み
  • 設定ファイルの変更監視と動的な再読み込み(オプション)
  • 環境変数の読み込み
  • リモートの設定ファイル(etcdConsul)の読み込みと変更の監視
  • コマンドラインフラグ(=いわゆる-dなどのコマンドオプション)からの読み込み
  • バッファからの読み込み
  • 明示的な値の設定

Viperはアプリケーションのあらゆる設定に必要な万能レジストリとみなすこともできます。

Viperを使う理由

いまどきアプリケーションを開発するうえで、設定ファイルをどんな形式にするかなどというレベルでいちいち悩むなんて馬鹿馬鹿しい限りです。足手まといかつ単調な設定作業はさっさと終わらせて、アプリそのものの開発に集中したい。Viperはそんなあなたのためのライブラリです。

Viperでは次の機能を実現できます。

  1. JSON/TOML/YAML/HCL形式の設定ファイルの探索、読み込み、アンマーシャリングを行えます。
  2. 別の設定オプションで使用できるデフォルト値の設定メカニズムを提供します。
  3. コマンドラインフラグでオプションを指定して値をオーバーライドするメカニズムを提供します。
  4. 既存のパラメータを別名(エイリアス)を追加することで、パラメータを壊さずに名前を変更できます。
  5. 値がコマンドラインで提供されているのか、デフォルトとして設定ファイルから提供されているのかを明確に区別できます。

Viperの値は以下の優先順位で扱われます。上位の値は下位の値より優先度が高くなります。

  1. Setを明示的に呼び出す
  2. フラグ(コマンドラインで与える)
  3. 環境変数(env)
  4. 設定(config)
  5. キー/バリュー ストア
  6. デフォルト値

Viperの設定キーでは大文字と小文字が区別されます。

Viperに値を保存する

デフォルト値の設定

優れた設定値システムなら、デフォルト値をサポートしているものです。キーのデフォルト値は必須というわけではありませんが、キーが設定ファイル・環境変数・リモート設定ファイル・フラグで設定されなかった場合に備えて、デフォルト値を設定しておくと何かと便利です。

例:

viper.SetDefault("ContentDir", "content")
viper.SetDefault("LayoutDir", "layouts")
viper.SetDefault("Taxonomies", map[string]string{"tag": "tags", "category": "categories"})

設定ファイルの読み取り

Viperでは、設定ファイルを探索するための最小限の設定を行わなければなりません。
JSON/TOML/YAML/HCLファイルが設定ファイルとしてサポートされています。

探索パスは複数指定できますが、現時点ではViperインスタンスひとつにつき設定ファイルひとつのみのサポートとなります。探索パスが複数ある場合、デフォルトの探索パスは固定されていないので、どの探索パスをデフォルトにするかをアプリケーション側から指定できます。

Viperで設定ファイルの探索と読み取りがどのように行われるかを例で示します。
設定ファイルの探索パスを必ず最低1つは指定しなければなりませんが、パスそのものはどこを指定しても構いません(特定のパスを指定しなければならないということはありません)。

viper.SetConfigName("config")          // 設定ファイル名を拡張子抜きで指定する
viper.AddConfigPath("/etc/appname/")   // 設定ファイルの探索パスを指定する
viper.AddConfigPath("$HOME/.appname")  // 探索パスを追加で指定する
viper.AddConfigPath(".")               // 現在のワーキングディレクトリを探索することもできる
err := viper.ReadInConfig()            // 設定ファイルを探索して読み取る
if err != nil {                        // 設定ファイルの読み取りエラー対応
    panic(fmt.Errorf("設定ファイル読み込みエラー: %s \n", err))
}

設定ファイルの更新検出と再読み込み

Viperはアプリケーションの実行中に設定ファイルの更新を自動検出して動的に読み込むことができます。設定変更を反映するためだけにいちいちアプリケーションを再起動する必要は、もうありません。

これを行うには、ViperのインスタンスからwatchConfigに監視を指定します。変更発生時に実行したい関数を渡すこともできます。

WatchConfig()を呼び出す前に必ずconfigPathsをすべて追加しておいてください

        viper.WatchConfig()
        viper.OnConfigChange(func(e fsnotify.Event) {
            fmt.Println("設定ファイルが変更されました:", e.Name)
        })

io.Readerから設定ファイルを読み込む

Viperの設定値は、ファイル/環境変数/フラグ/リモートのキーバリューストアなどさまざまな方法で取得できます。しかも、必要であればViperに値を設定する方法を自由に実装することもできます。

viper.SetConfigType("yaml") // viper.SetConfigType("YAML")としてもよい

// 以下の設定は一例であり、どのような方法でも構わない
var yamlExample = []byte(`
Hacker: true
name: steve
hobbies:
- skateboarding
- snowboarding
- go
clothing:
  jacket: leather
  trousers: denim
age: 35
eyes : brown
beard: true
`)

viper.ReadConfig(bytes.NewBuffer(yamlExample))

viper.Get("name") // "steve"という値を得る

設定のオーバーライド

オーバーライドは、コマンドラインフラグで指定することも、アプリケーションのロジックに基づいて行うこともできます。

viper.Set("Verbose", true)
viper.Set("LogFile", LogFile)

別名(エイリアス)の登録と利用

別名を指定することで、同じ値をさまざまなキーで参照できます。

viper.RegisterAlias("loud", "Verbose")

viper.Set("verbose", true) // 下と同じ値になる
viper.Set("loud", true)    // 上と同じ値になる

viper.GetBool("loud")      // true
viper.GetBool("verbose")   // true

環境変数を扱う

Viperは、Twelve Factor appの開発に欠かせない環境変数を完全にサポートしています。環境変数は以下の4とおりの方法で扱うことができます。

  • AutomaticEnv()
  • BindEnv(string...) : error
  • SetEnvPrefix(string)
  • SetEnvReplacer(string...) *strings.Replacer

※Viperでは環境変数の大文字と小文字が区別されますので、ご注意ください。

Viperには、環境変数が重複しないようにするためのメカニズムがあります。SetEnvPrefixを使用すると、読み込んだ環境変数にプレフィックスを追加できます。追加したプレフィックスは、BindEnvAutomaticEnvの両方で使用されます。

BindEnvはパラメータを1つまたは2つ取ります。1番目はキー名、2番目は環境変数名です。環境変数名は大文字小文字を区別します。
環境変数名を渡さなかった場合は、自動的にキー名と環境変数名が同じであるとみなされますが、その場合環境変数名はすべて大文字であるものとして扱われます。
環境変数名を明示的に渡す場合は、プレフィックスは自動的には追加されないのでご注意ください。

Viperでは、環境変数はアクセスのたびに常に最新の値を読み取ります。BindEnvにアクセスした時点での値に固定されているわけではありません。

AutomaticEnvは、SetEnvPrefixと組み合わせることで力を発揮します。AutomaticEnvを呼び出すと、以後viper.Getリクエストが行われるたびにその時点の環境変数を実際にチェックします。Viperでチェックする環境変数は、キーをすべて大文字に変換した名前で探します。EnvPrefixが設定されている場合は、探す環境変数名にプレフィックスを追加します。

SetEnvReplacerを実行すると、strings.Replacerオブジェクトを使ってキー名を(ある程度)差し替えることができます。これは、たとえば環境変数で_が区切り文字として使用されている状態で、Get()呼び出しのキーで-などの別の文字を使用したい場合に便利です。viper_test.goのコードで実例を見ることができます。

環境変数の使用例

SetEnvPrefix("spf")       // プレフィックス"spf"は自動的に大文字の"SPF"に変換される
BindEnv("id")

os.Setenv("SPF_ID", "13") // (実際は外部アプリで環境変数が設定されるのが普通)

id := Get("id")           // 値"13"を得る

フラグを扱う

Viper ではコマンドラインフラグをバインドできます。特に、Cobraでも使用されているPflagsをサポートしています。

BindEnvと同様に、バインドメソッドを呼び出した時点ではフラグの値は設定されず、実際にアクセスした時点で初めて設定されます。これにより、init()関数のようなうんと初期の段階でも事前にバインドを行うことができます。

BindPFlag()メソッドでは以下の機能を利用できます。

例:

serverCmd.Flags().Int("port", 1138, "アプリケーション・サーバーのポート番号")
viper.BindPFlag("port", serverCmd.Flags().Lookup("port"))

Viperでpflagを使用しても、標準ライブラリのflagなどのフラグ処理には影響しません。pflagパッケージでは、そうした別パッケージで定義されているフラグをインポートしたうえで扱っているからです。この機能は、pflagパッケージのAddGoFlagSet()という便利な関数を呼び出すことで利用できます。

例:

package main

import (
    "flag"
    "github.com/spf13/pflag"
)

func main() {
    pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
    pflag.Parse()
    ...
}

フラグのインターフェイス

Pflagsを使用したくない方のために、他のフラグシステムとバインドできるGoインターフェイスが2種類用意されています。

FlagValueは単一のフラグを表します。以下はこのインターフェイスのきわめてシンプルな実装例です。

type myFlag struct {}
func (f myFlag) IsChanged() { return false }
func (f myFlag) Name() { return "my-flag-name" }
func (f myFlag) ValueString() { return "my-flag-value" }
func (f myFlag) ValueType() { return "string" }

自分のフラグをこのインターフェイスに実装したら、以下のようにViperにバインドします。

viper.BindFlagValue("my-flag-name", myFlag{})

FlagValueSetは複数のフラグを扱います。以下はこのインターフェイスのきわめてシンプルな実装例です。

type myFlagSet struct {
    flags []myFlag
}

func (f myFlagSet) VisitAll(fn func(FlagValue)) {
    for _, flag := range flags {
        fn(flag)    
    }
}

自分のフラグをこのインターフェイスに実装したら、以下のようにViperにバインドします。

fSet := myFlagSet{
    flags: []myFlag{myFlag{}, myFlag{}},
}
viper.BindFlagValues("my-flags", fSet)

リモートのキー/バリューストアのサポート

Viperでリモートの設定を利用するには、以下のようにviper/remoteパッケージをブランク(_)にインポートします。

import _ "github.com/spf13/viper/remote"

これにより、etcdやConsulなどのリモート・キー/バリューストアのパスにある設定ファイル(JSON/TOML/YAML/HCL)をViperで読み取れるようになります。取得した値はデフォルト値よりも優先されますが、ディスクから読み取った値・フラグ・環境変数をオーバーライドしません。

キー/バリューストアからの読み出しにはcryptというライブラリを使用できます。これにより、正しいGPGキーリングがあれば読み取り時の復号化と保存時の暗号化も自動的に行えるようになります。暗号化はオプションであり、必須ではありません。

リモート設定ファイルは、ローカル設定ファイルと組み合わせることも、ローカル設定から独立して使用することもできます。

cryptライブラリにはコマンドラインヘルパが用意されており、キー/バリューストアに値を保存するのに使用できます。cryptは、デフォルトで http://127.0.0.1:4001 のetcdを使用します。

$ go get github.com/xordataexchange/crypt/bin/crypt
$ crypt set -plaintext /config/hugo.json /Users/hugo/settings/config.json

次のコマンドで、値が設定されたことを確認できます。
bash
$ crypt get -plaintext /config/hugo.json

値を暗号化して設定する方法の例や、Consulの使用方法については、cryptのドキュメントを参照してください。

リモートのキー/バリューストアの例(暗号化なし)

viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001","/config/hugo.json")
viper.SetConfigType("json") // バイトストリームにはファイル拡張子がないのでここで補う
err := viper.ReadRemoteConfig()

リモートのキー/バリューストアの例(暗号化あり)

viper.AddSecureRemoteProvider("etcd","http://127.0.0.1:4001","/config/hugo.json","/etc/secrets/mykeyring.gpg")
viper.SetConfigType("json") // バイトストリームにはファイル拡張子がないのでここで補う
err := viper.ReadRemoteConfig()

etcdの変更を検出(暗号化なし)

// (参考)以下のようにviperのインスタンスを作成してもよい
var runtime_viper = viper.New()

runtime_viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001", "/config/hugo.yml")
runtime_viper.SetConfigType("yaml")  // バイトストリームにはファイル拡張子がないのでここで補う

// リモート設定の初回読み込み
err := runtime_viper.ReadRemoteConfig()

// 設定をアンマーシャリングする
runtime_viper.Unmarshal(&runtime_conf)

// open a goroutine to watch remote changes forever
go func(){
    for {
        time.Sleep(time.Second * 5) // リクエストの間隔を5秒おきに設定
        // (現時点ではetcdのサポートのみテストしました)
        err := runtime_viper.WatchRemoteConfig()
        if err != nil {
            log.Errorf("unable to read remote config: %v", err)
            continue
        }

        // 新しい設定をアンマーシャリングして現在の設定の構造体に読み込む。
        // システムの変更通知をチャネルで実装してもよい
        runtime_viper.Unmarshal(&runtime_conf)
    }
}()

Viperから値を取り出す

値の種類に応じて、以下のようなさまざまな関数やメソッドで値を読み出すことができます。

  • Get(key string) : interface{}
  • GetBool(key string) : bool
  • GetFloat64(key string) : float64
  • GetInt(key string) : int
  • GetString(key string) : string
  • GetStringMap(key string) : map[string]interface{}
  • GetStringMapString(key string) : map[string]string
  • GetStringSlice(key string) : []string
  • GetTime(key string) : time.Time
  • GetDuration(key string) : time.Duration
  • IsSet(key string) : bool

Get系の関数はキーがない場合はゼロを返すことにご注意ください。キーがあるかどうかを確認するにはIsSet()を使います。

例:

viper.GetString("logfile") // ※setやgetのキーでは大文字小文字は区別されない
if viper.GetBool("verbose") {
    fmt.Println("verbose enabled")
}

ネストしたキーにアクセスする

Viperのアクセサメソッドでは、ネストの深いところにあるキーを指すフォーマット済みパスを使用することもできます。たとえば以下のJSONファイルを読み込んだとします。

{
    "host": {
        "address": "localhost",
        "port": 5799
    },
    "datastore": {
        "metric": {
            "host": "127.0.0.1",
            "port": 3099
        },
        "warehouse": {
            "host": "198.0.0.1",
            "port": 2112
        }
    }
}

以下のように、ドット.で区切ったパスでネストを指定してフィールドにアクセスできます。

GetString("datastore.metric.host") // ("127.0.0.1" を返す)

この記法は、前述の優先順位ルールに従います。

  • ルートキー(上の例ではdatastore)を探索すると、そのキーが見つかるまで以後の設定レジストリをカスケードします。
  • サブキー(上の例ではmetrichostなど)の探索ではカスケードは行われません。

たとえば、metricというキーがデフォルト値としては定義されているが、読み込んだ設定ファイルでは定義されていない場合は、ゼロを返します。

逆に、このmetric主キーが定義されていない場合は、以後のレジストリでこのキーを探索します。

最終的に、区切り文字によるキーパスを指定してキーが見つかったら、その値を返します。次の例をご覧ください。

{
    "datastore.metric.host": "0.0.0.0",
    "host": {
        "address": "localhost",
        "port": 5799
    },
    "datastore": {
        "metric": {
            "host": "127.0.0.1",
            "port": 3099
        },
        "warehouse": {
            "host": "198.0.0.1",
            "port": 2112
        }
    }
}

GetString("datastore.metric.host") //"0.0.0.0" を返す

サブツリーを展開する

Viperでサブツリーを展開できます。

変数viperが以下のようになっているとします。

app:
  cache1:
    max-items: 100
    item-size: 64
  cache2:
    max-items: 200
    item-size: 80

以下を実行すると、

subv := viper.Sub("app.cache1")

subvの内容は以下のようになります。

max-items: 100
item-size: 64

subvの形式の設定情報を使用してキャッシュを作成する以下のような関数が既にあるとします。

func NewCache(cfg *Viper) *Cache {...}

前述の記法を使えば、以下のように2とおりのキャッシュを簡単に作成できます。

cfg1 := viper.Sub("app.cache1")
cache1 := NewCache(cfg1)

cfg2 := viper.Sub("app.cache2")
cache2 := NewCache(cfg2)

アンマーシャリング

特定の値(またはすべての値)をアンマーシャリングして、構造体やマップなどに保存できます。
以下の2つのメソッドを使用できます。

  • Unmarshal(rawVal interface{}) : error
  • UnmarshalKey(key string, rawVal interface{}) : error

例:

type config struct {
    Port int
    Name string
    PathMap string `mapstructure:"path_map"`
}

var C config

err := Unmarshal(&C)
if err != nil {
    t.Fatalf("unable to decode into struct, %v", err)
}

追記: 構造体内の変数名は大文字で始めないとunmarshalで読み込めませんでした。対応する読み込み元のJSONのキーは小文字でもよいようです。
しかし構造体の変数がboolのときは変数名先頭が小文字でもよいのに、変数がstringだと変数名の先頭は大文字でなければなりませんでした。もしかしてバグ?

シングルトンか、インスタンス化か?

Viperは設定や初期化を行わなくても、すぐに使うことができます。多くのアプリケーションでは設定をひとつのリポジトリに集約しているので、Viperパッケージはそうしたスタイルを想定して、シングルトン的に使えます。

ここまでに使用したViperの使用例では、すべてシングルトン的スタイルを使用しています。

Viperインスタンスを複数使用する

そしてもちろん、アプリケーションでViperのインスタンスを複数使用することもできます。各インスタンスは設定や値を独自に持つことができます。それぞれ別の設定ファイルやキー/バリューストアなどから設定を読み込むこともできます。Viperパッケージでサポートする関数は、すべてviperのメソッドとしてミラーリングされます。

例:

x := viper.New()
y := viper.New()

x.SetDefault("ContentDir", "content")
y.SetDefault("ContentDir", "foobar")

//...

Viperインスタンスを複数使用する場合は、各自でインスタンスを管理してください。

Q & A

Q: INIファイルじゃだめなんですか?

A: 正直、INIファイルには問題がありすぎます。ファイル形式も標準化されておらず、壊れてないか検証するだけで一苦労です。ViperはJSON/TOMO/YAML/HCLを扱えます。追加して欲しいものがあったら喜んでマージしますので、プルリクお願いします。現在対応しているファイル形式でも、ほとんどのアプリケーションのニーズに十分対応できると思います。

Q: なんで「Viper」(マムシ)なんですか?

A: Cobraというライブラリとセットで使う設計なので、コブラとくればやはりマムシかなと。もうひとつ、G.I.ジョーのViperにもかけてあります。日本ならさしずめケロロ軍曹のヴァイパーですかね。ViperとCobraは互いに完全に独立しているのでそれぞれ単独でも使用できますが、組み合わせるとさらに強力になります。

Q: なんで「Cobra」なんですか?

A: コブラコマンダー(G.I.ジョー)が好きだからに決まってるっしょ。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away