Go1.17の新機能を導入しました

Go1.17の新機能を導入しました

こんにちは。エンジニアの牧田です。今回はみんな大好きGoについてです。

Go1.17リリース!

先月Go1.17がリリースされました!自分はGoを使い始めてまだ半年ほどなので、今回が初めてのバージョンアップの経験になりましたが、よく使っている言語に新しい機能が追加されるとなると、やはりとても気になりますね。

リリースノートはこちらです。Go1.17では全体的に細かい機能の追加が多く、そこまで目玉機能のようなものはありませんでしたが、いくつか便利な機能も追加されていたので、現在開発中のプロダクトにも導入してみました。また、機能の他にも、コンパイラの改善がなされており、Go1.17に変えるだけで約5%高速化したというのも嬉しいところです。(https://golang.org/doc/go1.17#compiler

実際に導入した機能

ここからは、現在開発中のプロダクトに実際に取り入れた新機能を紹介します。取り入れた機能は、主にテスト関連についてです。

T.Setenv

Setenv calls os.Setenv(key, value) and uses Cleanup to restore the environment variable to its original value after the test.
This cannot be used in parallel tests.

T.Setenvは、テスト中に一時的に環境変数を設定できる機能です(ベンチマーク中に同様に機能するB.Setenv も追加されています)。つまり、元から環境変数がセットされていた場合は、テスト中だけ指定した値に切り替わり、テストが終わったら元の値に戻るということです。

内部の実装は↓のようになっており、os.Setenvで環境変数を指定した値に書き換えて、テスト終了時に元の値に書き換えています。今までは自分で実装しないといけなかったので、少し便利になりましたね。

https://cs.opensource.google/go/go/+/go1.17:src/testing/testing.go;l=1120

func (c *common) Setenv(key, value string) {
    prevValue, ok := os.LookupEnv(key)

    if err := os.Setenv(key, value); err != nil {
        c.Fatalf("cannot set environment variable: %v", err)
    }

    if ok {
        c.Cleanup(func() {
            os.Setenv(key, prevValue)
        })
    } else {
        c.Cleanup(func() {
            os.Unsetenv(key)
        })
    }
}

注意点として、ドキュメントにも書かれているように、t.Parallelと同時に使用することはできません。

テストのShuffle機能

-shuffle off,on,N
Randomize the execution order of tests and benchmarks.
It is off by default. If -shuffle is set to on, then it will seed
the randomizer using the system clock. If -shuffle is set to an
integer N, then N will be used as the seed value. In both cases,
the seed will be reported for reproducibility.

テストのShuffle機能を使うと、複数テストの実行順序をランダムにすることができます。go test -shuffle=onのようにして使うことができます(デフォルトではオフになっています)。

実行順序をランダムにすることによって、もし順序に依存するテストが書かれていた場合、それを検知することができます。順序に依存するテストは、例えばデータベースに前のテストで使ったデータが残っていたり、前のテストでグローバル変数が変更されていたりする場合に起こります。本来はそうならないように気をつける(そうならない仕組みを作る)必要がありますが、もしそうなっていた場合にも気づくことができるということです。

T.Cleanup(Go1.17の新機能ではないが、新しく導入した)

今回テストに関する新しい機能を取り入れるにあたって色々調べていたところ、Go1.14でT.Cleanupという機能が追加されており、これが上手く使えそうだったので導入してみました。お気づきの方もいるかもしれませんが、これはT.Setenvの内部で使われている機能です(自分は後で気づきました…)。

T.Cleanupを使うと、テスト終了時に実行する関数を設定することができます。deferを使うのと同じじゃないの?と思うかもしれませんが、それより便利になる場面もあります。例えば複数のテストで使う共通の関数で上手く使うことができます。

実際に使用した例を紹介します。この例では、テストの最初にテスト用のDBをセットアップして、テスト終了時にDBを削除する関数を作っています(この関数をいろんなテストの最初に使い回す)。

今までのdeferを用いた場合は↓のような感じになります。

func SetupDB(t *testing.T) (*sql.DB, func()) {
    // DBを作成
    // db, err := sql.Open("mysql", ...)
    // db.Exec("CREATE DATABASE xxx")

    return db, func() {
        db.Exec("DROP DATABASE xxx") // DBを削除
    }
}

func Test_main(t *testing.T) {
    db, clearDB := SetupDB(t)
    defer clearDB()

    // dbを使ってテスト
}

これをt.Cleanupを使うと↓のように書けます。

func SetupDB(t *testing.T) *sql.DB {
    // DBを作成
    // db, err := sql.Open("mysql", ...)
    // db.Exec("CREATE DATABASE xxx")

    t.Cleanup(func() {
      db.Exec("DROP DATABASE xxx")
    })

    return db
}

func Test_main(t *testing.T) {
    db := SetupDB(t)

    // dbを使ってテスト
}

今までは、defer用の関数を返してテスト本体の関数で実行しなければいけませんでしたが、t.Cleanupを使うことでそれが必要なくなりました。結構便利ですね。

まとめ

今回はGo1.17で追加された新機能の一部を紹介しました。今回紹介した他にも色々機能が追加されているので、気になった方は調べてみてください。Go1.18ではジェネリクスが追加されるという話なので、とても楽しみです。次回のバージョンアップも楽しみにしてます!