20200707のGoに関する記事は5件です。

Go: Starttls でメールの送信

こちらと同じことを Deno で行いました。
Python3: Starttls でメールの送信
hi-ho.ne.jp で試しました。

hi-ho.go
// ---------------------------------------------------------------
//
//  hi-ho.go
//
//                  Jul/07/2020
// ---------------------------------------------------------------
package main

import (
    "fmt"
    "os"
    "net/smtp"
    "github.com/joho/godotenv"
)


func main() {

    fmt.Fprintf (os.Stderr,"*** 開始 ***\n")

err := godotenv.Load(".env")
    if err != nil {
        panic(err)
    }

    server := os.Getenv("SERVER")
    port := os.Getenv("PORT")
    usr := os.Getenv("USR")
    password := os.Getenv("PASSWORD")

    fmt.Fprintf (os.Stderr,server + "\n")
    fmt.Fprintf (os.Stderr,port + "\n")

    from := os.Getenv("FROM")
    to := os.Getenv("TO")

    auth := smtp.PlainAuth("", usr, password, server)

    msg := []byte("" +
        "From: " + from + "\n" +
        "To: " + to + "\n" +
        "Subject: Test from Hi-ho PM 20:02\n" +
        "\n" +
        "テスト\n" +
        "今晩は。\n" +
        "PM 20:02\n" +
    "")

    err = smtp.SendMail(server + ":" + port, auth, from, []string{to}, msg)
    if err != nil {
        fmt.Fprintf(os.Stderr, "エラー: %v\n", err)
        return
    }

    fmt.Print("success\n")

    fmt.Fprintf (os.Stderr,"*** 終了 ***\n")
}

// ---------------------------------------------------------------
.env
SERVER = 'hi-ho.mose-mail.jp'
PORT = 587
USR = '****@hi-ho.ne.jp'
PASSWORD = '****'
FROM = '****@hi-ho.ne.jp'
TO = 'sample@example.com'

実行結果

$ go run hi-ho.go
*** 開始 ***
hi-ho.mose-mail.jp
587
success
*** 終了 ***
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Golang 設定ファイルライブラリ 「Viper」 README翻訳 2020年7月

はじめに

spf13/viper: Go configuration with fangsの翻訳
すでに翻訳されている方[翻訳+α] Go言語の設定ファイルライブラリ Viper - Qiitaがいたので、それを元に現在のREADMEとの差分だけ付け足しました。
差分:https://github.com/spf13/viper/compare/30ce444..13df721
README_jp.mdとしてプルリク送りますが、マージされない可能性もあるのでQiitaとgistで公開しておきます。

変更箇所は、利用プロジェクト紹介、INI、envfileのサポート、一部APIの変更、追加、リモートのこコード例の追加、くらいでした。


Viper Readme

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

多くのGoプロジェクトがViperを使って構築されています。

インストール

go get github.com/spf13/viper

Viperについて

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

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

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

Viperを使う理由

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

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

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

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

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

重要Viperの設定キーでは大文字と小文字が区別しません。
これをオプションにすることについては、現在進行中のissueがあります。

Viperに値を保存する

デフォルト値の設定

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

例:

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

設定ファイルの読み取り

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

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

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

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

設定ファイルが見つからない場合は以下のように対応します

if err := viper.ReadInConfig(); err != nil {
    if _, ok := err.(viper.ConfigFileNotFoundError); ok {
        // 設定ファイルが見つかりません。必要に応じてエラーを無視してください。
    } else {
        // 設定ファイルは見つかりましたが、別のエラーが発生しました。
    }
}
// 設定ファイルが見つかり、正常に解析されました

注 [v1.6以降]: 拡張子のないファイルでも、プログラム的にフォーマットを指定できます。.bashrc のような拡張子を持たない設定ファイルをユーザのホームに置くこともできます。

設定ファイルの書き込み

設定ファイルからの読み込みは便利ですが、実行時に行った変更をすべて保存しておきたい場合もあるでしょう。
そのために、いくつかのコマンドが用意されていて、それぞれに目的があります。

  • WriteConfig - 現在のviperの設定を、定義済みのパスがあればそれに書き込みます。定義済みのパスがない場合はエラーになります。現在の設定ファイルが存在する場合は上書きされます。
  • SafeWriteConfig - 現在のviper構成を事前に定義されたパスに書き込みます。事前に定義されたパスがない場合はエラーになります。現在の構成ファイルが存在する場合は、上書きされません。
  • WriteConfigAs - 指定されたファイルパスに現在のviper構成を書き込みます。指定されたファイルが存在する場合、上書きされます。
  • SafeWriteConfigAs - 指定されたファイルパスに現在のviper構成を書き込みます。指定されたファイルが存在する場合、上書きされません。

Safeが付いているものは、どのファイルも上書きしませんが、存在しない場合は作成します。デフォルトの動作は作成または上書きです。

例をあげます:

viper.WriteConfig() // 現在の設定を'viper.AddConfigPath()'と'viper.SetConfigName'で設定した定義済みのパスに書き込みます。
viper.SafeWriteConfig()
viper.WriteConfigAs("/path/to/my/.config")
viper.SafeWriteConfigAs("/path/to/my/.config") // これはすでに書かれているのでエラーになります。
viper.SafeWriteConfigAs("/path/to/my/.other_config")

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

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の開発に欠かせない環境変数を完全にサポートしています。環境変数は以下の5とおりの方法で扱うことができます。

  • AutomaticEnv()
  • BindEnv(string...) : error
  • SetEnvPrefix(string)
  • SetEnvKeyReplacer(string...) *strings.Replacer
  • AllowEmptyEnv(bool)

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

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

BindEnvはパラメータを1つまたは2つ取ります。1番目はキー名、2番目は環境変数名です。環境変数名は大文字小文字を区別します。
環境変数名を渡さなかった場合は、Viperは自動的に環境変数が以下の形式にマッチしていると仮定します: prefix + "_" + すべて大文字のキー名。環境変数名(2番目のパラメータ)を明示的に指定した場合、自動的にプレフィックスは追加されません。たとえば2番目のパラメータがidの場合、Viperは環境変数IDを探します。

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

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

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

あるいは、NewWithOptions ファクトリ関数を使って EnvKeyReplacer を使うこともできます。
SetEnvKeyReplacer とは異なり、StringReplacer インターフェイスを受け入れるので、カスタムの文字列置換ロジックを書くことができます。
デフォルトでは、空の環境変数はセットされていないとみなされ、次の設定ソースにフォールバックします。空の環境変数をセットとして扱うには、AllowEmptyEnv メソッドを使用します。

環境変数の使用例

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"))

既存のpflagsのセットをバインドすることもできます (pflag.FlagSet)。

例:

pflag.Int("flagname", 1234, "help message for flagname")
pflag.Parse()
viper.BindPFlags(pflag.CommandLine)
i := viper.GetInt("flagname") // pflag の代わりに viper から値を取得します。

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

例:

package main

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

func main() {

    // 標準ライブラリ"flag"を使います
    flag.Int("flagname", 1234, "help message for flagname")
    pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
    pflag.Parse()
    viper.BindPFlags(pflag.CommandLine)

    i := viper.GetInt("flagname") // viperから値を取得します
    ...
}

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

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

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

type myFlag struct {}
func (f myFlag) HasChanged() bool { return false }
func (f myFlag) Name() string { return "my-flag-name" }
func (f myFlag) ValueString() string { return "my-flag-value" }
func (f myFlag) ValueType() string { 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/envfile)をViperで読み取れるようになります。取得した値はデフォルト値よりも優先されますが、ディスクから読み取った値・フラグ・環境変数をオーバーライドしません。

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

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

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

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

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

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

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

etcd

viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001","/config/hugo.json")
viper.SetConfigType("json") // バイトストリームにはファイル拡張子がないのでここで補う。サポートされている拡張子は "json", "toml", "yaml", "yml", "properties", "props", "prop", "env", "dotenv" です。
err := viper.ReadRemoteConfig()

Consul

You need to set a key to Consul key/value storage with JSON value containing your desired config.
For example, create a Consul key/value store key MY_CONSUL_KEY with value:
ご希望のコンフィグを含むJSON値でConsulのキー/バリューストレージにキーを設定する必要があります。
たとえばConsulのキー/バリューストアキー MY_CONSUL_KEYを値を指定して作成します。

{
    "port": 8080,
    "hostname": "myhostname.com"
}
viper.AddRemoteProvider("consul", "localhost:8500", "MY_CONSUL_KEY")
viper.SetConfigType("json") // 明示的に json に設定する必要があります。
err := viper.ReadRemoteConfig()
fmt.Println(viper.Get("port")) // 8080
fmt.Println(viper.Get("hostname")) // myhostname.com

Firestore

viper.AddRemoteProvider("firestore", "google-cloud-project-id", "collection/document")
viper.SetConfigType("json") // Config's format: "json", "toml", "yaml", "yml"
err := viper.ReadRemoteConfig()

もちろん、SecureRemoteProviderも利用できます。

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

viper.AddSecureRemoteProvider("etcd","http://127.0.0.1:4001","/config/hugo.json","/etc/secrets/mykeyring.gpg")
viper.SetConfigType("json") // バイトストリームにはファイル拡張子がないのでここで補う。サポートされている拡張子は "json", "toml", "yaml", "yml", "properties", "props", "prop", "env", "dotenv" です。
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")  // バイトストリームにはファイル拡張子がないのでここで補う。サポートされている拡張子は "json", "toml", "yaml", "yml", "properties", "props", "prop", "env", "dotenv" です。

// リモート設定の初回読み込み
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
  • GetIntSlice(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
  • AllSettings() : map[string]interface{}

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.metric.hostdatastore.metric.port の両方が既に定義されています (上書きされても構いません)。さらに datastore.metric.protocol がデフォルトで定義されていれば、Viper はそれを見つけることができます。

しかし、datastore.metric が (フラグ、環境変数、Set()メソッドなどによって)即時値でオーバーライドされた場合、datastore.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 := viper.Unmarshal(&C)
if err != nil {
    t.Fatalf("unable to decode into struct, %v", err)
}

キー自体にドット(デフォルトのキー区切り文字)が含まれている設定をアンマーシャリングしたい場合。
区切り文字を変更する必要があります。

v := viper.NewWithOptions(viper.KeyDelimiter("::"))
v.SetDefault("chart::values", map[string]interface{}{
    "ingress": map[string]interface{}{
        "annotations": map[string]interface{}{
            "traefik.frontend.rule.type":                 "PathPrefix",
            "traefik.ingress.kubernetes.io/ssl-redirect": "true",
        },
    },
})
type config struct {
    Chart struct{
        Values map[string]interface{}
    }
}
var C config
v.Unmarshal(&C)

Viperは、組み込み構造体へのアンマーシャリングもサポートしています。

/*
Example config:
module:
    enabled: true
    token: 89h3f98hbwf987h3f98wenf89ehf
*/
type config struct {
    Module struct {
        Enabled bool
        moduleConfig `mapstructure:",squash"`
    }
}
// moduleConfig could be in a module specific package
type moduleConfig struct {
    Token string
}
var C config
err := viper.Unmarshal(&C)
if err != nil {
    t.Fatalf("unable to decode into struct, %v", err)
}

Viperは値のアンマーシャリングにgithub.com/mitchellh/mapstructureを使用しており、デフォルトではmapstructureタグを使用しています。

文字列へのマーシャリング

viperに保持されているすべての設定をファイルに書き込むのではなく、文字列にまとめる必要があるかもしれません。
AllSettings()が返す設定を使って、好きなフォーマットのマーシャラーを使うことができます。

import (
    yaml "gopkg.in/yaml.v2"
    // ...
)
func yamlStringSettings() string {
    c := viper.AllSettings()
    bs, err := yaml.Marshal(c)
    if err != nil {
        log.Fatalf("unable to marshal config to YAML: %v", err)
    }
    return string(bs)
}

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

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: なんで「Viper」(マムシ)なんですか?

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

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

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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

golangci-lintに自作linterを追加する

はじめに

golangci-lintは、golangci-lint run とするだけで、様々な有名なlinterを実行することができます。

これには以下のメリットがあります。

  • 各linterの導入方法や設定方法を覚える必要なく、共通の設定で管理できる
  • 自分でlinter探さなくても、良いlinterが日々追加されていく
  • 誤検出がある場合に、linterによらず共通の設定方法でその結果を無視することができる

この記事では、golangci-lintに自作linterを追加する方法を公式ドキュメントと実際にsonatardがPRを出したファイルを参考にして紹介します。
golangci/golangci-lint - Add Noctx #1179

他にも日本の方では timakinさん作のbodyclosenakabonneさん作のnestif がgolangci-lintに採用されています。

公開Linterをgolangci-lintに追加する

0. Linterを実装する

golangci-lintは go/analysis packageを利用していないLinterをマージしないので、必ず利用するようにしましょう。

golangci-lint公式が紹介しているWriting Useful go/analysis Lintertenntennさんが公開されている資料を参考に実装してみてください。

1. fork

https://github.com/golangci/golangci-lint を fork します。

2. テストの追加

Linterのためのテストを実装します。 test/testdata/{yourlintername}.go に テストのファイルを追加します。サンプル

そして T=yourlintername.go make test_linters を実行して、テストが失敗することを確認します。

3. Linterの追加

pkg/golinters/{yourlintername}.go という新しいファイルを追加します。 このディレクトリ内の他のLinterの実装を参考にしてください。 サンプル

pkg/golinters/{yourlintername}.go で実装したLinterの新しい構造体を pkg/lint/lintersdb/manager.goGetAllSupportedLinterConfigs 関数に追加します。 確実な場合のみデフォルトで有効にしてください。 サンプル私が作成した noctx はデフォルトではオフになっています。

T=yourlintername.go make test_linters でテストが通ることを確認します。

4. Linterのオプションの設定

Linterに設定する必要があるオプションを調べます。例えば、govetには check-shadowing 1つのオプションしかありません。golangci-lintのユーザーの迷惑にならないようにデフォルト値を選択します。 設定ファイルの例 .golangci.example.yml

golangci-lintプロジェクト向けのgolangci-lintの設定にデフォルト値以外を設定する必要があるのならば .golangci.ymlに追加します。

config struct - pflag が適切な設定ファイルを解析するためのmapstructureタグを忘れないでください。

5. PRを送る

プルリクエストを送ると、CIが走るのですべて通過することを確認します。
またbotによりCLAの同意が求められるので同意する必要があります。

最後にレビューとそのコメントの修正が完了するとマージされて終了になります。

まとめ

既にテストさえ書いていれば数ファイルを修正するだけで、PRを送ることができます。
自作Linterを作成しても中々使ってもらうことは難しいので「多くの開発者に自作のLinterを使ってもらうことでGoコミュニティをより良くしたい!」と考える方は、是非golangci-lintにPRを送ってみてください。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

context.ContextなしでHTTP Requestを送信しているコードを発見する `noctx`

はじめに

https://github.com/sonatard/noctx/

noctxは context.Context なしでHTTPリクエストを送信しているコードを発見します。

もしあなたがライブラリ開発者であるなら context.Context をHTTPリクエストに渡していないと、ライブラリを利用するユーザはHTTPリクエストのキャンセルやトレース情報の取得ができません。

そのためライブラリの中でHTTPリクエストを送信している場合には noctx を利用することで、そのような問題を防ぐことができます。

特に会社からAPIのGo SDKを公開している場合には、noctxを利用することで多くのユーザが安心して利用することができるようになります。

インストール方法

$ go get -u github.com/sonatard/noctx/cmd/noctx

使い方

$ go vet -vettool=`which noctx` main.go
./main.go:6:11: net/http.Get must not be called

golangci-lintからの実行

noctx を configで有効にする。

.golangci.yml
# デフォルトのlinterに加えてnoctxを追加する
linters:
  enable:
    - noctx

# もしくはすべてのlinterを有効にして不要なlinterを無効化する

linters:
  enable-all: true
  disable:
   - xxx # 不要なlinter
# configに基づいた実行
$ golangci-lint run

# noctxだけを実行
golangci-lint run --disable-all -E noctx

検出のルール

  • 以下の関数を実行している場合はエラー
    • net/http.Get
    • net/http.Head
    • net/http.Post
    • net/http.PostForm
    • (*net/http.Client).Get
    • (*net/http.Client).Head
    • (*net/http.Client).Post
    • (*net/http.Client).PostForm
  • http.NewRequest 関数から返された http.Request を他の関数に渡しているとエラー

検出した場合の修正方法

  • HTTPリクエストの送信では (*http.Client).Do(*http.Request) メソッドを利用する。
  • Go 1.13以降では、 http.NewRequest 関数の代わりに http.NewRequestWithContext 関数を利用する。
  • Go 1.12以前では、 http.NewRequest のあとに (http.Request).WithContext(ctx) を実行する。

(http.Request).WithContext(ctx)http.Request をコピーして返すためパフォーマンスのデメリットがあります。もしあなたの作成したライブラリがGo1.13以降しかサポートしないのであれば http.NewRequestWithContext を利用してください。

修正のサンプル

もしあなたがライブラリの作者で既にcontextを受け付けない関数を提供してしまっている場合は、別途contextを受け付ける関数を定義して、既存の関数はcontextを受け付ける関数のラッパーにすることをお勧めします。

// 修正前
// HTTPリクエストを送信しているが、contextを受け付けていない
func Send(body io.Reader)  error {
    req,err := http.NewRequest(http.MethodPost, "http://example.com", body)
    if err != nil {
        return nil
    }
    _, err = http.DefaultClient.Do(req)
    if err !=nil{
        return err
    }

    return nil
}
// 修正後
func Send(body io.Reader) error {
    // SendWithContextを呼び出し、contextにはcontext.Background()を渡す
    return SendWithContext(context.Background(), body)
}

// contextを受け付ける
func SendWithContext(ctx context.Context, body io.Reader) error {
    // NewRequestをNewRequestWithContextに変更してcontextを渡す
    req, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://example.com", body)
    if err != nil {
        return nil
    }
    _, err = http.DefaultClient.Do(req)
    if err != nil {
        return err
    }

    return nil
}

検出例

package main

import (
    "context"
    "net/http"
)

func main() {
    const url = "http://example.com"
    http.Get(url) // want `net/http\.Get must not be called`
    http.Head(url)          // want `net/http\.Head must not be called`
    http.Post(url, "", nil) // want `net/http\.Post must not be called`
    http.PostForm(url, nil) // want `net/http\.PostForm must not be called`

    cli := &http.Client{}
    cli.Get(url) // want `\(\*net/http\.Client\)\.Get must not be called`
    cli.Head(url)          // want `\(\*net/http\.Client\)\.Head must not be called`
    cli.Post(url, "", nil) // want `\(\*net/http\.Client\)\.Post must not be called`
    cli.PostForm(url, nil) // want `\(\*net/http\.Client\)\.PostForm must not be called`

    req, _ := http.NewRequest(http.MethodPost, url, nil) // want `should rewrite http.NewRequestWithContext or add \(\*Request\).WithContext`
    cli.Do(req)

    ctx := context.Background()
    req2, _ := http.NewRequestWithContext(ctx, http.MethodPost, url, nil) // OK
    cli.Do(req2)

    req3, _ := http.NewRequest(http.MethodPost, url, nil) // OK
    req3 = req3.WithContext(ctx)
    cli.Do(req3)

    f2 := func(req *http.Request, ctx context.Context) *http.Request {
        return req
    }
    req4, _ := http.NewRequest(http.MethodPost, url, nil) // want `should rewrite http.NewRequestWithContext or add \(\*Request\).WithContext`
    req4 = f2(req4, ctx)
    cli.Do(req4)

    req5, _ := func() (*http.Request, error) {
        return http.NewRequest(http.MethodPost, url, nil) // want `should rewrite http.NewRequestWithContext or add \(\*Request\).WithContext`
    }()
    cli.Do(req5)

}

参照

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

go修行17日目 Webサーバ

参考サイト

https://echo.labstack.com/guide
https://rightcode.co.jp/blog/information-technology/golang-introduction-rest-api

パッケージインストール

go get -u github.com/labstack/echo/...

コード

  • localhost:1323で起動する
package main

import (
    "net/http"

    "github.com/labstack/echo"
)

func main() {
    e := echo.New()
    e.GET("/", func(c echo.Context) error {
        return c.String(http.StatusOK, "Hello, World!")
    })
    e.Logger.Fatal(e.Start(":1323"))
}
PS C:\Users\yuta\go\src\server> go run .\server.go

   ____    __
  / __/___/ /  ___
 / _// __/ _ \/ _ \
/___/\__/_//_/\___/ v4.1.16
High performance, minimalist Go web framework
https://echo.labstack.com
____________________________________O/_______
                                    O\
⇨ http server started on [::]:1323

image.png

ユーザー名を返す

package main

import (
    "net/http"

    "github.com/labstack/echo"
)

func main() {
    e := echo.New()
    e.GET("/users/:name", getUserName)
    e.Logger.Fatal(e.Start(":1323"))

}

func getUserName(c echo.Context) error {
    name := c.Param("name")
    return c.String(http.StatusOK, name)
}

image.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む