Goのstructを比較してdiffを見るなら godebug/pretty が便利

Goのテストでstructを比較するときに reflect.DeepEqual で比較することがあります。

テストを書き、テストを実行するとFAILだったとき...つらい...
structが大きいとさらにつらい。どのフィールドの値が違うのかわかると早くテストを直せて(または実装を直せて)嬉しいですよね。

そんなときgodebug/prettyを使うとdiffを見れて便利です。

github.com

こんなUserのstructがあったとします

type User struct {
    ID string
    Name string
    Email string
}

テストでstructを reflect.DeepEqual で比較し一致するかをチェックします
一致しなかった場合、pretty.Compare でdiffを表示してみます

func TestCompare(t *testing.T) {
    tests := []struct{
        user User
        want User
    } {
        {
            user: User{
                ID: "id",
                Name: "inari",
                Email: "inari111@example.com",
            },
            want: User{
                ID: "id",
                Name: "inari111",
                Email: "inari111@example.com",
            },
        },
        {
            user: User{
                ID: "id",
                Name: "inari111",
                Email: "inar222@example.com",
            },
            want: User{
                ID: "id",
                Name: "inari111",
                Email: "inari111@example.com",
            },
        },
    }

    for _, test := range tests {
        if !reflect.DeepEqual(test.user, test.want) {
            t.Errorf("error: %s", pretty.Compare(test.user, test.want))
        }
    }
}

テスト実行結果

$ go test
--- FAIL: TestCompare (0.00s)
    user_test.go:49: error:  {
          ID: "id",
        - Name: "inari",
        + Name: "inari111",
          Email: "inari111@example.com",
         }
    user_test.go:49: error:  {
          ID: "id",
          Name: "inari111",
        - Email: "inar222@example.com",
        + Email: "inari111@example.com",
         }
FAIL
exit status 1
FAIL    github.com/inari111/godebug-pretty  0.011s

小さいstructなら必要ありませんが、大きいstructの場合は助かりますね。

Goでsliceを結合するときのパフォーマンスについて

sliceを結合するときに、どういう書き方をするのがパフォーマンスがいいのか気になって検証してみた。 検証したのは2パターン。

パターン1: slice同士を結合

func appendSlice(n int) []string {
    sliceA := make([]string, n)
    sliceB := make([]string, n)

    for i := 1; i <= n; i++ {
        sliceA = append(sliceA, "a")
        sliceB = append(sliceB, "b")
    }
    sliceA = append(sliceA, sliceB...)

    return sliceA
}

パターン2: forで回しながらappend

func appendSlice2(n int) []string {
    sliceA := make([]string, n)
    sliceB := make([]string, n)

    for _, s := range sliceB {
        sliceA = append(sliceA, s)
    }

    return sliceA
}

パフォーマンスを計測するためにベンチマークを書く。

package main

import "testing"

func BenchmarkAppendSlice(b *testing.B) {
    b.ResetTimer()
    appendSlice(b.N)
}

func BenchmarkAppendSlice2(b *testing.B) {
    b.ResetTimer()
    appendSlice2(b.N)
}

パターン1のベンチマーク

$ go test -count 10 -test.bench BenchmarkAppendSlice

BenchmarkAppendSlice-8       2000000           631 ns/op
BenchmarkAppendSlice-8       2000000           596 ns/op
BenchmarkAppendSlice-8       2000000           631 ns/op
BenchmarkAppendSlice-8       2000000           595 ns/op
BenchmarkAppendSlice-8       2000000           602 ns/op
BenchmarkAppendSlice-8       2000000           600 ns/op
BenchmarkAppendSlice-8       2000000           596 ns/op
BenchmarkAppendSlice-8       2000000           598 ns/op
BenchmarkAppendSlice-8       2000000           597 ns/op
BenchmarkAppendSlice-8       2000000           596 ns/op
PASS
ok      github.com/inari111/test    18.915s

パターン2のベンチマーク

$ go test -count 10 -test.bench BenchmarkAppendSlice2
BenchmarkAppendSlice2-8      5000000           253 ns/op
BenchmarkAppendSlice2-8     10000000           243 ns/op
BenchmarkAppendSlice2-8     10000000           206 ns/op
BenchmarkAppendSlice2-8     10000000           204 ns/op
BenchmarkAppendSlice2-8     10000000           198 ns/op
BenchmarkAppendSlice2-8     10000000           205 ns/op
BenchmarkAppendSlice2-8     10000000           200 ns/op
BenchmarkAppendSlice2-8     10000000           201 ns/op
BenchmarkAppendSlice2-8     10000000           201 ns/op
BenchmarkAppendSlice2-8     10000000           202 ns/op
PASS
ok      github.com/inari111/test    22.293s

まとめ

パターン1のslice同士の結合のほうがパフォーマンスよさそう。
もし間違ってたら教えていただけると嬉しい。

datastoreに依存したテストはStronglyConsistentDatastoreをtrueにする

Goを書き始めた頃、テストで下記のようにcontextを生成していた。

ctx, done, e := aetest.NewContext()
if e != nil {
    t.Fatal(e)
}
defer done()

// 1. Putする

// 2. 1でPutしたデータをGetする

このコードには、datastoreにPutした直後にGetするとデータを取得できずテストが落ちるという問題があった。
これを解決するには aetest.Options{StronglyConsistentDatastore: true} Instance生成時の引数に渡してあげればいい。
しかし、aetest.NewContext には引数でoptionを渡すことはできない。 github.com

aetest.NewInstance にoptionを渡せるメソッドを作り、datastoreが絡むテストはこのメソッドを使うようにしている。

func NewContext(option aetest.Options) (context.Context, func(), error) {
    inst, err := aetest.NewInstance(&option)
    if err != nil {
        return nil, nil, err
    }
    req, err := inst.NewRequest("GET", "/", nil)
    if err != nil {
        inst.Close()
        return nil, nil, err
    }
    ctx := appengine.NewContext(req)
    return ctx, func() {
        inst.Close()
    }, nil
}

呼び出し側

option := aetest.Options{StronglyConsistentDatastore: true}
ctx, done, e := NewContext(option)
if e != nil {
    t.Fatal(e)
}
defer done()

このようにすれば、Putしてdatastoreに反映したものをGetできるようになる。

ブログに残しておこうと思っていたのにいつの間にか数ヶ月経っていてよくないな。。

hatebu: はてブのホットエントリーを表示するCLIツールを作った

最近Goを書いているので、勉強がてらCLIツールを作ってみた。

作ったもの

はてブのホットエントリー(テクノロジー)の一覧を表示するCLIツール hatebu を作った。

github.com

学生の頃からはてブが好きで、2014年頃はホットエントリーを収集して過去のホットエントリーをまとめて見れるようなサービスを作ったりしていた。
CLIツールを作りにあたり、普段よく使うものがよかったので、ホットエントリーを表示するものにした。 今はテクノロジーしか取得していないが、将来的に他のもサブコマンドで指定できるようにしたい。

今回、CLIパッケージは spf13/cobra を使ってみた。元Dockerの中の人で、今はGoogleで働いているらしい。

github.com

サブコマンドは個別にファイル作れば追加できるので拡張性がよさそう。

ホットエントリー一覧を表示するCLIツールは既に作っている人がいて、実装する上で大変参考にさせていただきました。ありがとうございます。
github.com

fish shellでgvmを使う

gvmを使うとGoをバージョン指定してインストールできる。
https://github.com/moovweb/gvm

以下、Macにインストールするのを前提とする。

依存ツールのインストー

➤ brew update
➤ brew install mercurial 

gvmのインストールは

bash < <(curl -s -S -L https://raw.githubusercontent.com/moovweb/gvm/master/binscripts/gvm-installer)

または

zsh < <(curl -s -S -L https://raw.githubusercontent.com/moovweb/gvm/master/binscripts/gvm-installer)

なんだけど、このままではfishではエラーになり使えない。

そこでこのIssueを見ると解決方法が書いてあった。
https://github.com/moovweb/gvm/issues/137

fishermanというfishのプラグインマネージャーを使ってbassプラグインをインストールすれば解決できるらしい。

fishermanインストー

➤ curl -Lo ~/.config/fish/functions/fisher.fish --create-dirs git.io/fisher

bassをインストー

➤ fisher edc/bass

gvmをインストー

bashに切り替えて

➤ bash
bash-3.2$ bash < <(curl -s -S -L https://raw.githubusercontent.com/moovweb/gvm/master/binscripts/gvm-installer)

source /Users/username/.gvm/scripts/gvm 実行し、
config.fishに以下を追記して設定反映すれば、gvmコマンドを使うことができる。

function gvm
  bass source ~/.gvm/scripts/gvm ';' gvm $argv
end

Go1.6をインストー

本当は1.8を使いたいところだけど、1.6をインストー

➤ gvm install go1.6
ERROR: Failed to compile. Check the logs at /Users/koori/.gvm/logs/go-go1.6-compile.log
ERROR: Failed to use installed version
exit code: 1

1.5からはセルフホスティングされているため、ビルドに1.4以上が必要らしい・・・あんまりこの辺は理解していない。

バイナリからインストー

➤ gvm install go1.4 -B
Installing go1.4 from binary source

➤ gvm use go1.4
➤ set -x GOROOT_BOOTSTRAP GOROOT

Go1.6をインストー

➤ gvm install go1.6 -B
Installing go1.6 from binary source
➤ gvm use go1.6
Now using version go1.6

確認してみる

➤ gvm list

gvm gos (installed)

   go1.4
   go1.6
➤ go version
go version go1.6 darwin/amd64

OK

--allow-emptyで空コミットを作る

プルリクはできるだけ粒度を小さくしレビューをしやすくしていきたい。
そんなとき、空コミットがあると便利。

git commit --allow-empty -m "commit message"

feature/hogeブランチで空コミットを作ってPushしWIPのプルリクを作る。 各作業はfeature/hogeからブランチを切って作業しプルリクを作り、レビューが通ればfeature/hogeにマージ。

雑なブログになってしまった。