emahiro/b.log

Drastically Repeat Yourself !!!!

ログを出力する車輪の再発明をしてみた

Go の 標準の log パッケージ の実装をベースに自前で管理のログ出力部分を実装してみました。

やったこと

Go の標準の log パッケージをベースにして自前でログを実装する機会があったので、そもそも log パッケージ内の実装を読んで見ようと思ったのがきっかけです。
以下のような感じで HTTP のリクエストログを表示するくんを実装しました。

2019/12/21 18:18:19 GET /
2019/12/21 18:18:20 GET /

コードは以下に置いてます。
(goimports かけ忘れましたw)

github.com

log パッケージの実装の詳細

ポイントを絞って車輪の再発明のときに参考にしたところを記載します。

感想

車輪の再発明がてら本家のパッケージがどう実装してるか見るのはとても参考になります。

node のバージョン管理に n を使い始めた

内容

理由

  • 年末だし、開発周りに環境をアップデートしていたこと
  • node-brew で管理すると、バージョンを切り替えるときに nodebrew を打つのがめんどくさい
  • 割と仕事でもそこそこ頻繁に node のバージョンを切り替えることが多いので、打ち込むコマンドは少ない方がいい
    • n って1文字じゃん!!!最高!!!!

nodebew を捨てる

  1. nodebrew のPATHを通してる箇所を削除
  2. ~/.nodebrew を削除
  3. usr/local/bin 配下の nodebrew を削除

n を入れる

https://github.com/tj/n/blob/master/README.md の通り。
これだけだと n 経由で特定バージョンの node を入れるときに /usr/local/n 配下に書き込み権限がないので、README.md に記載してる権限付与の作業が必要。

$ n 10.16.0

  installing : node-v10.16.0
       mkdir : /usr/local/n/versions/node/10.16.0
       fetch : https://nodejs.org/dist/v10.16.0/node-v10.16.0-darwin-x64.tar.gz

便利!

[追記]

初めて n を入れてから n {$VERSION} で指定したバージョンの node を入れようとすると権限エラーが起きることがある。

 n 9.11.2

  installing : node-v9.11.2
       mkdir : /usr/local/n/versions/node/9.11.2
mkdir: /usr/local/n/versions/node/9.11.2: Permission denied

  Error: sudo required (or change ownership, or define N_PREFIX)

N_PREFIX を指定すると解決する。以下を bash_profile や .zshrc に追加する。

export N_PREFIX="$HOME/.n"
export PATH="$PATH:$N_PREFIX/bin"

Go の httptest で立てたサーバーにアクセスする

テストなどで実際にサーバーを立てずに、HTTP のリクエストをシミュレートしたいときに httptest を使いますが、この httptest で立てたダミーサーバーそのものアクセスする方法はないかを調べてみました。

Motivation

あるテストをメンテしていた時に httptest.NewServer で作成したダミーのサーバーに対してリクエストを送っているのですが、ダミーサーバーにうまく送信できずにテストが落ちる、と言うことを繰り返してました。

結果としては、httptest.NewServer に router を差し込んで、アプリケーションで定義してる routing をシミュレートできていなかったことが原因でした。
このとき、テスト対象のアプリの routing に依存しない独自の Hnalder を定義した httptest.NewServer で立てたサーバーに向けて、 HTTP のリクエストを送る方法を知りたいないと思ったのがこのエントリを書こうと思った動機です。

サンプルコード

httptest.Server を立てます。

httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("hello in test server."))
}))

NewServer の引数に router を指定してないのでこの状態で httptest.NewRequst でリクエストを生成してサーバーにアクセスすることはできません。

ではどうやってこの生成したサーバーにアクセスするのかを godoc と実際のコードを追いながら調べたところ httptest.Serverhttp.Server 型の Config と言う Field が存在しており、これが httptest.NewServer して立てたサーバーの本体のようです。
(このフィールドの用途はサーバーを立てた後に構成を変更するためのものらしいので、実際の用途とは少し違う使い方をすることになりそうです。)

実際に httptest.NewServer で立てたサーバーにアクセスするコードは以下のようになります。

func TestMain(m *testing.M) {
    ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        fmt.Printf("request in test server. req: %+v", r)
        w.WriteHeader(http.StatusOK)
        w.Write([]byte("hello in test server."))
    }))

    r := httptest.NewRequest(http.MethodGet, "/", nil)
    w := httptest.NewRecorder()
    ts.Config.Handler.ServeHTTP(w, r)
}

実際にテストを実行してみます。
ちなみに TestMain にした意図はテストをしたいわけではなくて、テストでサーバーを起動してアクセスすることが目的だからです。

go test -v . 
request in test server. req: &{Method:GET URL:/ Proto:HTTP/1.1 ProtoMajor:1 ProtoMinor:1 Header:map[] Body:{} GetBody:<nil> ContentLength:0 TransferEncoding:[] Close:false Host:example.com Form:map[] PostForm:map[] MultipartForm:<nil> Trailer:map[] RemoteAddr:192.0.2.1:1234 RequestURI:/ TLS:<nil> Cancel:<nil> Response:<nil> ctx:<nil>}
ok      github.com/emahiro/ilhttptest   0.550s

ちゃんと作成したサーバーにアクセスできてますね。

ユースケース

正直これを書いた後に、じゃあ実際どう言うケースでこのテストサーバーにアクセスできることが嬉しくなるのだろう?っと考えて「これだ!」と言うことは思いつきませんでした...。

以下のようなミドルウェアを実装して

func mw() func(http.Handler) http.Handler {
    return func(h http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            // middleware で何かしらの処理をする
            fmt.Printf("This is in middleware\n")
        })
    }
}

サーバーを作る時に事前に差し込んでおくとか考えましたけど、その場合には

h := mw()(http.NewServeMux())
r := httptest.NewRequest()
w := httptest.NewRecorder()
h.ServeHTTP(w,r)

とした方が便利ですしコード少なくて済みます。

テストサーバーに直でアクセスできる方が嬉しいケースがまだ足りませんが、httptest で立てたサーバーへのアクセス方法を知ることはできました。

今回書いたコードは以下にあげてます。

github.com

Go で意図的に競合状態を発生させる

Summary

  • Go では concrrent map writes のような競合状態の可能性がある実装があるときに排他ロックをかける。
  • 競合状態を回避するためのサンプルとしてロックをかける実装はたくさんインターネット上に情報が出てくるが、そもそも意図的に競合状態を作り出すサンプルがなかったので書いてみた。
  • Go のテストで競合状態をチェックするには race オプションを使用する

競合状態チェックの実装

https://github.com/emahiro/il/pull/6 に記載した。
一応並行処理のため、毎回確実に発生するわけではないが数回に一度は concurrent map writes が発生する。
そのため go test . -race で競合状態のチェックをかけるとテストが落ちるようになっている。

コード上は同期的な処理に見えても、マイクロサービス内で飛び交うリクエストなど並行に処理がされるケースに置いて concurrent mas writes が発生しうる場合には test で並行に動かしてみて競合状態 (race conditons)をチェックすることが出来ると良いし、こう言うケースに置いて単体テストで競合状態をシュッと作り出せるととってもスマートだなと思う。

LINE Developer Day 2019 に参加して来た

linedevday.linecorp.com

今年も参加してきたので備忘録として感想をつらつらまとめます。 スライドは [こちら https://speakerdeck.com/line_devday2019]

感想

まず会場がすごい。グランドニッコーを二日間使うってどういうことだ。
僕は技術的なカンファレンスでここにきたことがあるのは5年前くらいの Unite Tokyo (Unity のカンファレンス) 以来だった。
このペースならそのうち日比谷公園ジャックとかしそうな勢いを感じた。
(天候が安定しない日本においては野外の選択肢は難しいが、今のような時期なら晴れも多くワンチャンあるかも。でもきっと寒い笑)

しかしお台場は遠いので朝苦手勢からすると、1日目のキーノートに間に合うか前日から不安だった(結果間に合った。)

セッション全体の感想については自分が参加したものから受けた印象に限るが、1日目の2日目で毛色が異なる構成で二日間参加したけどどちらも興味深い話をたくさん聞けた。

1日目は技術的な内容が非常に盛りだくさんだった。 去年までの新製品や新サービス発表のような内容とは異なり、LINEの技術そのものや、長年蓄積してきた技術的な課題に立ち向かう現場の話、さらにはリアルな世界でプロダクトを作る上で必ず考えないといけないもの(セキュリティなど)に関するセッションも多く、ある意味ハレの面だけでないところにも目を向けたセッションも多かった。

2日目は技術的な内容もありつつ、LINEで働くことやプロダクトマネジメントに寄った内容もあり、技術だけでなく「ものづくり」全般に関わる内容が多かったように思う。
1日目に技術的な話をモリモリ聞いていて、ちょっと食傷気味だったところに毛色の違う話が多かったので、聞いて楽しく、脳みそもリフレッシュした状態で聞くことができた。

会場全体も色々工夫してあった。

  • 休憩スペースの机の並びが「LINE」になっていたり
  • ガチャがあってガチャ結果でノベルティの内容が変わっていたり(僕は2等で卓上加湿器をもらった)

などなど。
あ、お弁当は二日ともとてもお高いものだったので美味しかった。

ざっとまとめるとこんな感じ。
LINE のカンファレンスは当たり前のように英語セッションがあったり、普通に英語や韓国語が飛び交っていたりして、グローバル企業なんだなぁということをつくづく感じる。

また来年も遊びにこようと思えたイベントでした。楽しかったー。

リポジトリのオーナーを移行しました

報告

以下のリポジトリのオーナーを移行しました。

App Engine の 2nd Gen 移行のために業務で作っていたものです。

もう一つ作ってるツールがあるのでそちらもできたらこういう風に使おうと思って作りました、みたいな内容のエントリを書こうかなと思います。

App Engine Logger の更新

Overview

以前作成した ae-plain-logger を aelog としてパッケージ名を変更した。

github.com

Motivation

  1. emahiro/aehcl とシリーズものとして合わせたかった。
  2. ローカルだと JSON じゃなくてテキスト出力できた方が便利とコメントもらった。
  3. Testable Examples を書こうと思った。

改善した PullRequest 一覧

そのほか

  • 今回の修正に合わせて README.md を更新。
  • Go1.11 のサポートを切った。

ひとこと

OSS を作ったんだし、Go のバージョンの更新とか今後も続けていかないとなぁと思った。
あと package 名変更したら go get するときに リポジトリ名も 更新した module 名にしておかないといけなくて get できないという問題でハマった。
エラーメッセージ読めよと言われたけど読んでもすぐには解決できなかった(言い訳)

精進する。やっていき!。

その他

本家の人がサンプル実装っぽい logger を実装していた。名前も同じだった笑。

https://github.com/broady/aelog

解決済み: net/http.Header の Clone の実装についての疑問点

net/httpHeader の Clone の実装について調べる機会があったので、そこで気になったことをまとめました。

net/http.Header の cloneHeader の実装

本家の Clone の処理は以下

// Clone returns a copy of h.
func (h Header) Clone() Header {
    // Find total number of values.
    nv := 0
    for _, vv := range h {
        nv += len(vv)
    }
    sv := make([]string, nv) // shared backing array for headers' values
    h2 := make(Header, len(h))
    for k, vv := range h {
        n := copy(sv, vv)
        h2[k] = sv[:n:n]
        sv = sv[n:]
    }
    return h2
}

ref: https://github.com/golang/go/blob/13d0af4e704bee164f873701e326048bdaf23933/src/net/http/header.go#L82-L96

元々は以下のような unexport な実装になっていたが、最新版では export な関数になっている。

func (h Header) clone() Header {
    h2 := make(Header, len(h))
    for k, vv := range h {
        vv2 := make([]string, len(vv))
        copy(vv2, vv)
        h2[k] = vv2
    }
    return h2
}

メモリ効率の悪さがもともとあったらしく https://github.com/golang/go/issues/29915 のissue で起票され、変更が取り込まれて上記のような現在の実装になっている。

net/http.Header.Cloen() の疑問点

https://github.com/golang/go/issues/29915 で起票されている通り元の実装ではメモリ効率が悪かったので現在のような実装になっているらしい。

ただ、試しに以下のように loop の外で コピー先の実装を確保して loop 内で新しい Header に差し込む実装をしてもコピーの動作に変化はなかった。
(※ for range pointer だけど for 内部でコピーをしなくても差し込む新しい Header が別なので別ものとしてコピーされているらしい)

func (h Header) clone() Header {
    h2 := make(Header, len(h))
    for k, v := range h {
        h2[k] = v
    }
    return h2
}

サンプル実装: https://play.golang.org/p/nnF7ERKOrEA

このサンプル実装の結果、コピー前後の Header の pointer のアドレスの値は変わっているので、やりたいことは同じに見える。

計測する

以下の3つの実装を用意する。

  • 本家を踏襲した実装(無印)
  • 本家の1つの前のバージョンの実装(V1)
  • 今回計測したい実装(V2)
func cloneHeader(h http.Header) http.Header {
    nv := 0
    for _, v := range h {
        nv += len(v)
    }

    sv := make([]string, nv) // shared backing array for headers' values
    h2 := make(http.Header, len(h))
    for k, v := range h {
        n := copy(sv, v)
        h2[k] = sv[:n:n]
        sv = sv[:n]
    }
    return h2
}

func cloneHeaderV1(h http.Header) http.Header {
    h2 := make(http.Header, len(h))
    for k, v := range h {
        v2 := make([]string, len(v))
        copy(v2, v)
        h2[k] = v2
    }
    return h2
}

func cloneHeaderV2(h http.Header) http.Header {
    h2 := make(http.Header, len(h))
    for k, v := range h {
        h2[k] = v
    }
    return h2
}

ベンチの実装は以下

func Benchmark_cloneHeader(b *testing.B) {
    header := http.Header{}
    for i := 0; i < 10000; i++ {
        header.Add(string(i), string(i))
    }

    for i := 0; i < b.N; i++ {
        cloneHeader(header)
    }
}

func Benchmark_cloneHeaderV1(b *testing.B) {
    header := http.Header{}
    for i := 0; i < 10000; i++ {
        header.Add(string(i), string(i))
    }

    for i := 0; i < b.N; i++ {
        cloneHeaderV1(header)
    }
}

func Benchmark_cloneHeaderV2(b *testing.B) {
    header := http.Header{}
    for i := 0; i < 10000; i++ {
        header.Add(string(i), string(i))
    }

    for i := 0; i < b.N; i++ {
        cloneHeaderV2(header)
    }
}

ベンチ結果は以下

go test -bench . -benchmem -run Benchmark_cloneHeader
# 略
Benchmark_cloneHeader-8             2000           1041890 ns/op          902115 B/op         19 allocs/op
Benchmark_cloneHeaderV1-8           1000           1164574 ns/op          899178 B/op      10007 allocs/op
Benchmark_cloneHeaderV2-8           2000            827401 ns/op          738271 B/op         18 allocs/op

仮説とわからないこと

ヘッダーのコピーの挙動が同じであれば、計測結果を見る限り V2 の実装の方が効率が良い。
しかし、Go の本家の header.go の中の実装ではそうなっていないのは何故なんだろう...??
V2 の実装を採用した場合に、何か意図しない挙動がおこなるのでは?とも思って実際のアドレスを見たけどアドレスは異なっているのでやりたいことは同じだと思う。

Header の Clone について本家のような実装になってる意図が見えてこないので教えて欲しいし、計測方法や自分が気づいていないバグがあるかもしれないからその点もフィードバックが欲しい。

追記にて解決しました

その他

Full slice expressions

初めて知った。Go の Slice は index の指定で slice の要素を取り出すときに capacity を指定できる。

func main() {
    str := []string{"a", "b", "c", "d"}
    str2 := str[1:2:3]
    fmt.Printf("str2: %v\n", str2)
    fmt.Printf("cap(str2): %v", cap(str2))
}

// Output
// str2: [b]
// cap(str2): 2

s[low: high: max] とあると max で指定した cap のあたいは max - low の値になる。

ref: https://golang.org/ref/spec#Slice_expressions

追記

解決した。

V2 の実装でもうまいこと動作していたのは cap を指定していなかったから。 cap を指定すると h と h2 の 間で slice を共有してしまっているので、h のcap に余裕がある状態で Add すると あと勝ちが発生 してしまう。

ref: https://play.golang.org/p/6QmI__2DoNw

これは意図した振る舞いではないので shallow copy 出なく deep copy が必要になるので本家のような実装が正しい。

shallow copy による「意図しない振る舞い」をどう再現するのかわからなくて悩んでしまっていた....。

glc(go local cache) というライブラリを作りました

[]byte でローカルにキャッシュを保存する glc (go local cache) というライブラリを作ってみました。

Motivation

APIのレスポンスやそんなに頻繁に更新しないデータを一定時間ローカルにキャッシュとして持っておきたいケースは多いと思います。

Go であれば https://github.com/patrickmn/go-cache などのオンメモリにキャッシュするライブラリを使うのが一般的かなと思いますが、上記のライブラリは interface を受けて interface を返す仕様のため、Set するときはなんでも突っ込めば key に合わせてキャッシュをセットしてくれますが、取り出すときも interface のために取り出した側で適切にキャストないし、デコードすることが必要でした。

使う側でキャストすることに違和感があったのでそれなら使う側で []byteエンコードしてしまって、[]byte を受け取って []byte を返すだけの小さな仕様のライブラリがあってもいいかなーと思ったのと、どうやら go-cache は公式には Go Module を現時点では考えてないっぽい?という issue も見つけた(※.1)ので、今後のことを考えて自作で「ローカルキャッシュする君」を作ってみました。

この Go Local Cache はあくまで []byte に変換した値に有効期限をつけてメモリ(orファイル上)に保存するものなので、もし変換するコストをかけたくないのであれば go-cache を使う方がいいと思います。

繰り返しですが、go-cache だと使う側でキャストすることが必要になりますが、それが少し気持ち悪かったのがこれを作ったモチベーションです。
go-cache で要件を満たせるのであればそちらを使うでもいいかと思います。

※1. https://github.com/patrickmn/go-cache/pull/89
この issune に記載されてることは若干疑問ではある。
(普通に go.mod 対応して新しくバージョン切って go get github.com/patrickmn/go-cache@v2.X.X で行けそうな気もする。)

How to Use

In memory cache

README の通りです。

[]byte 型に変換してしてしまえばどんな値でも保存できます。

Local file cache

Go Local Cache を唄っているので local でのファイルキャッシュも実装しました。

使い方は In memory cache と似ています。内部実装としては NewFileCache を実行するタイミングで prefix に指定したディレクトリを新しく作成してそこに key で指定したファイル名でファイルを作成していきます。

FileCache オブジェクトはファイルキャッシュを使用するパッケージごとに作成するとパッケージごとに同一のファイルキャッシュ用のディレクトリを参照するようになります。

なお、このファイルキャッシュの昨日は Google App Engine でも使えます(※1)

※1. tempファイルにキャッシュ用のディレクトリを作成するため。
ref: 一時ファイルの読み取りと書き込み

感想

ちょうど業務で似たような機能を作る機会があったので、単純な機能だけ切り出してライブラリにしてみました。
ヘビーに使うかはわからないですが、少なくとも自分がほしいと思うものは作れたんじゃないかなと思います。

作ってる最中にもメソッドのシグネチャ悩んだり、MemoryCacheの構造を悩んだり、さっと実装してみたら思ってた振る舞いをしないものを作っていたりと、コードはとても小さいですが、汎用的に作るものの難しさをやはり痛感しました。

ちなみにすでに業務で一部投入してるので商用環境で動いてる実績あります(最後にささやかなアピール笑)

Cloud Run を使う

別に目新しいこともないですが、Cloud Run の走りだけ使ってみたので雑な備忘録。

Docker image を用意して gcr に上げる

起動したい Docker image を作成して gcr.io に上げておきます。
Cloud Run を起動するときは gcr.io 上に上がっている image のみ選択できます。

起動

Cloud Run を起動するときには http server になれる && Cloud Run の用意する port が空いている image を用意しないといけません。
試しに適当な nginx の image を Docker Hub から持ってきて gcr に上げて指定 -> 起動を試みましたがダメでした。

カスタムドメイン設定

Cloud Run のコンテナ一覧の画面に MANAGE CUSTOM DOMAIN というボタンがあり、こちらからサブドメインを設定できます。
Mapping Custom Domain の詳細は https://cloud.google.com/run/docs/mapping-custom-domains に記載てあります。

SSLの設定が必要そう。あとで調べる。

はまったところ

Docker にハマる

Docker 使い慣れてないので Cloud Run 以前に Docker にハマりました。。。 特に gcr に上げ直すとき...w

docker tag {{ old name(Docker Hub から pull してきたときの image 名) }} {{ new name (gcr.io.. を指定する) }}

で image の名前を変更しないとそもそも gcr に上げられなかったという....。

権限でハマる

Cloud Run を起動するときに、IAMの権限の大きさによって認証必要なコンテナか無認証でアクセスできるコンテナを起動するのかを選ぶことができます。
また特定の role 以上の権限を持ったアカウントでないと起動直後に発行されるURLへのアクセスすらできません。

role については以下にまとまっています。
https://cloud.google.com/run/docs/reference/iam/roles

Github Actions (β) を使う

Github Actions (β) がリリースされていたので、せっかくなので自分が現在作成しているプロジェクトをネタに Github Actions の設定を行ってみました。

まずはβに参加する

help ページから Github Actions に参加します。

https://help.github.com/ja/articles/about-github-actions#about-github-actions

このページの GitHub Actionsの限定パブリックベータへの参加をリクエストする というところから参加可能です。

使えるようになったら登録してるメールアドレス宛に連絡がきます。

設定

Github Actions が使えるようになるとリポジトリの上部に Actions タブが出てくるのでそこから設定を行います。

今回は今自分で作成している Go で書かれたプロジェクトを使って Github Actions の設定をしてみます。

ます最初に言語ごとに指定された「Set up workflow」を押下します。
押下すると {{ Project Name }}/.github/workflows/{{ $lang }}.yaml というファイルが生成されます。

Go を選択した場合、なぜか標準では dep を使うようになっているので通常の go get で依存関係を取得するように修正します。

デフォルトで作成される Workflow を定義した yaml ファイルは以下

name: Go
on: [push]
jobs:

  build:
    name: Build
    runs-on: ubuntu-latest
    steps:

    - name: Set up Go 1.12
      uses: actions/setup-go@v1
      with:
        go-version: 1.12
      id: go

    - name: Check out code into the Go module directory
      uses: actions/checkout@v1

    - name: Get dependencies
      run: |
        go get -v -t -d ./...
        if [ -f Gopkg.toml ]; then
            curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
            dep ensure
        fi

    - name: Build
      run: go build -v .

Install Dependencies で go modules を使うことと、今回はテストを走らせたいだけなので Build を step に追加するのではなく、Test を step に追加します。

詳細な Workflow syntacs は こちら に記載してあります。

設定して commit を行うとActions を設定したファイルが作成されます。

{{ $Project }}/.github/workflows/{{ $lang }}.yaml があると自動的に Actions が走ります。
(どうやらファイル名はなんでも大丈夫なようです。追記あり)

あとは Actions タブを見ると自動的に設定した通りに自動でワークフローが実行されているのがわかります。

サンプル: https://github.com/emahiro/glc/actions

まとめ

Github Actions は想像以上に簡単にセットアップできました。
Github 謹製のツールなので CI から解放されるのもとてもいいなと思います。

早く正式版のリリースがされてほしいなと感じました。

追記

複数 の Goのバージョンでテストする

複数のGoのバージョンに対応させた設定ファイルを用意します。

ref: https://github.com/emahiro/glc/tree/master/.github/workflows

imageについて

現状 github の公式が指定してる環境しか作ることはできません。
今後 image を指定できるようになってほしいなと思いました。特殊な環境 (appengine とか) を作るのがまだめんどくさそう...

やってる人とかいるのかな。

GoogleCloudSDK アップデート後に aetest でインスタンスの起動失敗時の対処

2019/10/09 Google Cloud SDK を v266.0.0 に更新すると aetestの更新せずとも直ります。 2019/10/08 google.golang.org/appengine を最新版にすると本問題は解決します。
※詳しくは下部の追記参照

Google Cloud SDK を最新版にアップデートした際に aetest を使っていた appengine のテストが unable to find admin server URL と言われて落ちる(= 動かなくなってしまった)ようになってしまったので、その対処方法について記載しいます。
※ 最新版は Google Cloud SDK 265.0.0 です。

gcloud version
Google Cloud SDK 265.0.0

結論から先に言うと、最新の Google Cloud SDK のアップデートによって goapp コマンドが SDK から削除された(= Go1.9 のランタイムのローカルでの非サポートになった)ことにより、 appengine/aetest が壊れたことが原因です。

このエントリでは一時的な対処法と調査方法について記載します。

対処法

先に対処法について記載します。 aetest が起動しなくなったのは Google Cloud SDK が更新されたことによって、ローカルでも第一世代の appengine が起動しなくなったことが原因です。

そのため対処法は以下の2点のどちらかになると思います。

  • aetest のappengine の設定ファイルを Go1.11 の設定に合わせる
  • dev_appserver.py コマンドの内部で goroot-1.9 を使っている箇所にパッチを当てる。

aetest のappengine の設定ファイルを Go1.11 の設定に合わせる

なぜ appengine が起動しなくなったかは aetest 内の以下の設定を見ればわかります。

const appYAMLTemplate = `
application: %s
version: 1
runtime: go
api_version: go1
handlers:
- url: /.*
  script: _go_app
`

ref: https://github.com/golang/appengine/blob/5f2a59506353b8d5ba8cbbcd9f3c1f41f1eaf079/aetest/instance_vm.go#L278-L287

ここを以下のように Go1.11 に合わせた形式で修正します。

runtime: go111

dev_appserver.py コマンドの内部で goroot-1.9 を使っている箇所にパッチを当てる

※ この方法をとるに至った経緯については後述の調査方法に記載します。

Google Cloud SDKをアップデートしたことにより dev_appserver.py ないの goroot-1.9 を参照してる箇所が壊れてる(参照先のディレクトリが削除されている) ので aetest を走らせた時に appengine が起動する前に aetest が落ちてしまう、と言うのが原因だったので dev_appserver.py に置いて goroot-1.9 を参照してる箇所を変更します。

具体的には ~/google-cloud-sdk/platform/google_appengine/google/appengine/tools/devappserver2/go/application.py 内の以下の2箇所です。

@staticmethod
def _get_architecture(goroot):
    # 略
    for platform in os.listdir(os.path.join(goroot, 'pkg', 'tool')): # ← ここの goroot に 1.9 が存在しないのでエラーが発生して appengine が起動しない。
        # Look for 'linux_amd64', 'windows_386', etc.
        if '_' not in platform:
        continue
        architecture = platform.split('_', 1)[1]
        if architecture in architecture_map:
        return architecture_map[architecture]
    raise go_errors.BuildError(
        'No known compiler found in goroot (%s)' % goroot)

なのでこのループに入る前に darwin_amd64 を return 指定しまいます。(Macの場合)
Look for ... のコメントにどんな値が変えるのか記載してあったので多分わかるはず。

もう1箇所に多様な処理をしてる箇所がどうファイル内の def _get_pkg_path(goroot) の内部にもあるので、同様にパス検査のループに入る前に darwin_amd64_appengine で return してしまいます。

上記のパッチを dev_appserver コマンドに適用することで aetest のライブラリを修正しなくてもローカルで aetest の中で dev_appserver のインスタンスが起動するようになります。

調査方法

今回の対処を適用するに当たっての調査方法を記載します。

ここから先は個人の調査記録なので、興味のある方は読んでみてください。
内容はほぼ自分の脳内のダンプです。

ログをみる

まず調べるに当たって、エラーメッセージを見ました。

unable to find admin server URL と言うエラーが発生して appengine が起動しておらずこのエラー自体は以前も見かけたことがあって、素の dev_appsever が起動するかを確認するために普通に dev_appserver.py を叩いて appengine が起動するかチェックします。

これは起動したので、aetest を起動させた時のログをチェックすることにします。

aetest には SuppressDevAppServerLog があり、これは testerator がデフォルトで on にしているので aetest を起動した時に、素の dev_appserver を起動したときのようなログは出てきません。

ref: https://github.com/golang/appengine/blob/a37df1387b4521194676d88c79230c613610d5f4/aetest/instance.go#L31-L33

まずはこのフラグを off にしてログを出力する状態にして再度 aetest を走らせてみます。

go test ./app
# 略

Traceback (most recent call last):
  File "{{ $HOME }}/google-cloud-sdk/platform/google_appengine/dev_appserver.py", line 96, in <module>
    _run_file(__file__, globals())
  File "{{ $HOME }}/google-cloud-sdk/platform/google_appengine/dev_appserver.py", line 90, in _run_file
    execfile(_PATHS.script_file(script_name), globals_)
  File "{{ $HOME }}/google-cloud-sdk/platform/google_appengine/google/appengine/tools/devappserver2/devappserver2.py", line 600, in <module>
    main()
  File "{{ $HOME }}/google-cloud-sdk/platform/google_appengine/google/appengine/tools/devappserver2/devappserver2.py", line 588, in main
    dev_server.start(options)
  File "{{ $HOME }}/google-cloud-sdk/platform/google_appengine/google/appengine/tools/devappserver2/devappserver2.py", line 360, in start
    options.api_host, apiserver.port, wsgi_request_info_)
  File "{{ $HOME }}/google-cloud-sdk/platform/google_appengine/google/appengine/tools/devappserver2/dispatcher.py", line 248, in start
    ssl_port)
  File "{{ $HOME }}/google-cloud-sdk/platform/google_appengine/google/appengine/tools/devappserver2/dispatcher.py", line 384, in _create_module
    ssl_port=ssl_port)
  File "{{ $HOME }}/google-cloud-sdk/platform/google_appengine/google/appengine/tools/devappserver2/module.py", line 1309, in __init__
    super(AutoScalingModule, self).__init__(**kwargs)
  File "{{ $HOME }}/google-cloud-sdk/platform/google_appengine/google/appengine/tools/devappserver2/module.py", line 598, in __init__
    self._module_configuration)
  File "{{ $HOME }}/google-cloud-sdk/platform/google_appengine/google/appengine/tools/devappserver2/module.py", line 231, in _create_instance_factory
    module_configuration=module_configuration)
  File "{{ $HOME }}/google-cloud-sdk/platform/google_appengine/google/appengine/tools/devappserver2/go/instance_factory.py", line 137, in __init__
    go_config.enable_debugging)
  File "{{ $HOME }}/google-cloud-sdk/platform/google_appengine/google/appengine/tools/devappserver2/go/application.py", line 118, in __init__
    self._arch = self._get_architecture(self._goroot)
  File "{{ $HOME }}/google-cloud-sdk/platform/google_appengine/google/appengine/tools/devappserver2/go/application.py", line 213, in _get_architecture
    for platform in os.listdir(os.path.join(goroot, 'pkg', 'tool')):
OSError: [Errno 2] No such file or directory: '{{ $HOME }}/google-cloud-sdk/platform/google_appengine/goroot-1.9/pkg/tool'
Exception TypeError: "'NoneType' object is not callable" in <bound method DatastoreEmulator.__del__ of <google.appengine.tools.devappserver2.cloud_emulators.datastore.datastore_emulator.DatastoreEmulator object at 0x10b88d250>> ignored
unable to find admin server URL

dev_appserver.py にパッチを当てる

OSError: [Errno 2] No such file or directory: '{{ $HOME }}/google-cloud-sdk/platform/google_appengine/goroot-1.9/pkg/tool'

ログをみたら一発で今回の課題となっていた箇所の原因らしきものが出てきました。 ここからどうやら Google Clodu SDK を更新したタイミングで goroot-1.9 が消えていたので appengine が起動しないと言うことがわかりました。

ここから 対処法2 の対応が思い浮かびます。

aetest の設定を修正する

対処法2 についてはログからなんとかく類推可能だと思うのですが、では 対処法1 に到るまでの過程ですが、goroot-1.9 でうまくいかないと言うところまでわかったので「もしかして App Engine の設定周りとか関係してる?」というところを仮説立てして aetest の設定ファイルをみに行きました。

その結果、第一世代 App Engine の設定のままだったのでここを Go1.11 の設定に書き直してみるか、という方法を思いつきました。

フォーラムやコミュニティに投稿がないかを見る

今回はエラーメッセージはすぐにわかっても最新のSDKの問題ですぐには検索には出てこない内容でした。
また、issue にもすぐには上がってこない内容でもあったので、検索から問題を特定するのはほぼできませんでした。

こうなった時はフォーラムなどのコミュニティに何かとっかかりになる情報が転がってないか見に行きます。

今回のケースに関してはすでに issuetracker に話題が上がっていました。解決方法までは上がってませんでしたが、、、

ref: https://issuetracker.google.com/issues/142004500

ハマったところ

dev_appserver のログを出すために aetest ないの SuppressDevAppServerLog をオフにするところで、プロジェクト内の vendor 配下のフラグをオフにしてましたが、aetest を走らせる時はローカルのモジュールキャッシュの中の aetest(testerator) を使っており、 $GOPATH/pkg/mod 配下にキャッシュされてるファイルを編集する必要がありました。

僕の個人環境の問題かもしれません...。

追記

Google に早く対応して欲しいところなので、続報はこちらに記載します。

追記1

PR は出ていたので早く merge されて欲しいです...
https://github.com/golang/appengine/pull/214

2019/10/08 追記 mergeされていましたので appengine のパッケージを更新するとて aetest でインスタンスが起動するようになります。

go get google.golang.org/appengine@latest

追記2

ノウハウにも情報上がってましたね。

github.com

追記3

devappserver の patch を gist に上げておきました。

https://gist.github.com/emahiro/d0b78d40475300561ad424b0b3741a1c#file-application-py-L194-L241

Struct tag の記法を編集する

Go の struct tag の記法を編集する方法を記載します。

IntelliJVSCode の2つでの設定方法を記載しました。

VSCode

https://github.com/fatih/gomodifytagsを使います。

これは VSCode に限らず各種 Editor 向けにに Go の Struct tag の自動生成機能を提供してる便利ライブラリです。(後で知りました。)

How to use

手元に gomodifytags を落としてきます。

go get github.com/fatih/gomodifytags

使い方は Ctrl + Shift + P で Add Tag.. を打つと 自動生成してくれるコマンドが出てきます。

ただ、なぜかこれもデフォルトが snake_case で、しかも omitempty までついてきてしまいます。

フォーマットやデフォルトの omitempty をつけたくないので setting.json に以下を追記します。
各記法については gomodifytags の README に記載してますのでそちらを参考にしてください。

"go.addTags": {
    "tags": "json",
    "options": "json=",
    "promptForTags": false,
    "transform": "camelcase"
},

options の指定の仕方は以下の issue を参考にしました。

github.com

json 以外のタグの指定方法

sqlx などの ORM 系のライブラリを使うケースでは上記の設定に ORM で指定されているタグを指定します。
例えば、sqlx を使う場合には db をカンマつなぎで tags フィールドに追加します。

"go.addTags": {
    "tags": "json,db", // カンマつなぎ
    "options": "",
    "promptForTags": false,
    "transform": "snakecase"
}, 

promptForTags option は add struct tag するときにどのタグを入れるかを GUI で逐一確認してくれる option です。json タグは入れたくないけど db タグは入れたい、みたいなことを struct で個別に設定したいときに便利です。

IntelliJ

IntelliJ はデフォルトの自動 struct tag 補完は snake_case が採用されているのですが、仕事ではほぼ camelCase しか使わないのでなんとか camelCase に変更したいと思ったのが今回のモチベーションです。

詳細は以下のFAQに記載してあります。これをみれば大体OKでした。

https://intellij-support.jetbrains.com/hc/en-us/community/posts/360000525304-camelCase-json-for-Golang-struct-tadsintellij-support.jetbrains.com

以下の順で Go Struct Tags を編集します。

ソースコードビューについて考えていること

最近、とある機会にソースコードレビューについて質問されたので、考えてること、意識していることをまとめてみました。
なお、これは私自身の考えていることで人によって異なるところは多いと思いますので一個人のお気持ち表明として読んでもらえればと思います。

目次

  • 自分がソースコードレビューのときに心掛けていること
  • テストを書くことについて
  • まとめ
  • 参照

自分がソースコードレビューのときに心掛けているいること

ソースコードレビューはレビュアーになることもレビュイーになることもあります。そのときに心掛けていることについて記載します。

レビュアーとして

  • コードレビューはバグを見つけるものではないと思ってみること
    • バグの混入はあくまで実装者の責任であって、レビュアーの責務ではない(見つけられたらラッキー程度に思っておく)
  • 極力依頼された瞬間にレビューすること
    • ソフトウェアはチームで開発することが多く、チームの全体最適を考えると極力レビューを依頼されたときにみる方がプロダクト全体で見ると速度が出ます。。これは依頼されたレビューがブロッカーになるときもあるので、ブロッカーは素早く取り除く意味でも依頼されたらまずサッと見る、というのはいいプラクティスだと思っています。
    • もちろん業務状況や勤怠状況によるので必ずこれをしないといけないという訳ではありません。
  • 「指摘」「直して」という言葉を極力使わない。
    • レビューはそもそもレビュアーとしてもレビュイーとしてもコストが高い作業なので、強い言葉は極力使わないようにしています。そのため、自分が依頼されたレビューでは以下のように言い換えるように気をつけています。
      • 指摘 -> コメント、フィードバック
      • 直す -> 取り込む

レビュイーとして

レビューを受けるときはレビュアーからのフィードバックは コードという成果物へのフィードバックであって、人格否定ではない ことを肝に銘じています。
このため 自身の成果物としてのコードに人格を載せない。成果物と人格を同一視しない というスタンスで臨んでいます。

また変更差分には

  • この変更は何か?
  • なぜこの変更をするのか?

をセットでPull Request の description に記載するようにしています。

テストを書くことについて

※ 先に言い訳しておくと、自分はテストについての専門家ではないです。

ソースコードレビューはテストコードとセットにすることが多いと思います。 テストを書くそれ自体については、色んな意見があると思いますが、自分は以下の理由からテストを書いています。

  1. デバックコストを下げること
  2. テスト = ソフトウェアの資産
  3. 「期待する動作をすること」をテストなしに説明するのがめんどくさいこと

デバックコストを下げること

実装の実践的な話になるのですが、開発するときは必ず、必要な要件に対して自分の書いたコードが正しい動作をしているかを検証しながらコードを書くことが多いと思います。
自分は普段 Go で API を書く仕事をしているのですが、実装した Endpoint に対して、

  1. 正常系と異常系のリクエストを作る
  2. local で サーバーを立ち上げる or 開発環境に撒く
  3. curl を使って動作検証を行う

という手順で検証を進めることがありますが、これがマイクロサービスでのAPI開発のケースになってくると local でも認証を突破するためのリクエスト作ったり、デバックのためにわざわざアクセストークン発行したり、といったことをする必要があります。
もちろん、最終的な検証ではこの手順を行いますが、モデルを実装しただけとか、とりあえず正常系だけ書いたので Endpoint の動作を一時的にテストしたいケースにおいてははっきり言ってめんどくさいです。

そういうときに Endpoint のテストを先に書いておいて、テストケースを追加していくだけで意図した動作になっているかはシュッと確認できるので、テストを書きます。

この開発方法を取るようになってから、開発速度が上がり始めたので、テスト書くとスピードが落ちる、は私には当てはまりませんでした。

テスト = ソフトウェアの資産

これはそのままです。この考え方を教わってからテストへの考え方が変わりました。

「期待する動作をすること」をテストなしに説明するのがめんどくさいこと

若干脳死気味でもあるんですが、PullRequest を出すときに、「なぜテストがないのか?」を説明するのがめんどくさいのでテストもセットで書いてます。
テストもなしに「自分の書いたコードは必ず正しく動きます」と文章で説明するくらいなら、テスト書いちゃった方が早いです。きっと。

まとめ

こういった考えに到るまでに様々な変更と、そのレビューを受けてきました。
何度か心が折れかけたこともありましたが、コードという成果物と私自身という人格を分けること を会得したことでかなりコードレビューというものに対してのメンタル的なコストが軽減しました。

このエントリで記載した「意識していること」のいくつかは、仕事をしている中で尊敬するエンジニアの方々からインプットされたもので僕個人の考えという訳ではありませんが、私自身としても大切にしたい考えだと思ってここに記載してます。

refs

google の文書の以下の邦訳はとてもいい文章でした。コードレビューをする・受ける側どちらでも参考になることが多いと思います。

shuuji3.github.io