Go Conference 2019 Spring に登壇者として参加してきました

Go Conference 2019 Spring - Gocon に登壇者として参加してきました。

登壇の詳細は以下になります。

タイトル: 「Go1.9 で作られた App Engine のサービスを Go1.11 に移行した話」 登壇資料

speakerdeck.com

こういった大きなカンファレンスで発表するのは、エンジニアキャリアの中で初めてだったので、実際に登壇者として参加してみた学びと反省、感想をまとめてみました。

学びと反省

登壇について

社外の大きなカンファレンスでの初めての登壇でした。
かなり緊張しましたが、実際に登壇をしてみると、聞く側と話す側では全く違いました。
普段から使ってる App Engineについての発表にも関わらず、細かいところは案外曖昧で、理解を一段進める意味でも、発表する機会を頂けたことは良かったです。

ただ登壇については、練習では時間通りに進められていましたが、当日思わぬトラブルがあり、最終的に時間が足りなくなったために、最後の方は駆け足でまとめてしまいました。
流石に今の自分にはリカバれるスキルはなかったので、次回以降の課題として持ち越しかなと思います。

内容にについて

Google App Engineについての発表をしました。
イムリーに話題に上がっている内容で、かつ普段の業務で行なっている内容を取り上げることができました。

ただし、少しApp Engineによりすぎてしまったと思っており、知ってる人にはわかるけど、知らない人は置いてけぼり...みたいな内容にもなってしまったかなと思います。ここはもう少し説明を増やしたり、コンテンツの量を調整するなど、練り込める余地は残っていました。
加えて、Go の話はバージョンをあげること以外であまり盛り込むことができませんでした。

スライド作りについて

視認性の高いテキストサイズ、コードのシンタックスハイライト、細かい文言や単語の表記揺れなどPCでスライドを作っているときは気にならなくても、実際に話してみると、見づらいスライドに当初はなってしまってました。

特にコードのシンタックスハイライトは結構悩んで、最初はダーク系にしていたんですが、ライト系にした方がいいとアドバイスもらったのでそちらを採用しました。
幸い、自分のセッションのときは部屋を暗くしてもらったので、シンタックスハイライトが原因でコードが見づらい、ということはなかったと思います。
ダーク系の場合、部屋が明るかったときに見えづらい可能性もあったので、この辺の塩梅がとても悩ましかったです。

全く知見がないので、スライドで視認性が高く、かつ部屋の大きさや明るさに左右されないシンタックスハイライトのテーマについて教えて欲しいです...。そういうエントリとかないかあ...。

また、スライドを作りながら、テキストは少量で、テキストサイズは大きいに越したことはない、ということを感じました。
作成してる時はPCに向かっているので、特に文字サイズについて気にすることがありませんでしたが、GoCon前に社内で発表練習していたときに、文字サイズのフィードバックを多くもらいました。
自分がちょうどいいと思うサイズは案外小さく見づらく、大き過ぎでは?と思うサイズくらいがちょうどいいのかなと感じました。

なお、こちらは余談ですが、Googleスライド力がまた少し上がりました。 Outputする機会があるごとに新しい機能を知っている気がします。今回も2つくらい新しい機能を知ることができました。
Googleスライドってとても便利ですね(小並)

スピーチについて

これが一番難しかったです。
ストーリーに仕立てることに非常に苦労しました。
スライド間のつなぎの言葉というか、話してるスライドから次のスライドに行くときに、どういう接続をすると綺麗にプレゼンが流れていくのかは試行錯誤しました。

今回に関しては無理矢理繋げていった、という感じは否定できないので、スライドのアウトラインに加えてストーリー構成を考えてからプレゼンを組み立てていくといいのかなと思いました。

ちなみに、自分は普段めちゃくちゃ早口で、今回みたいな緊張したときは、いつも以上にさっさと進んでいくのかと思いきや、なぜか当日登壇してみたらスライドが全く進まず、なんでや!となりました。
(時間計りながら、なんで進まんのや...と自分が一番驚きました。)

感想と今後の意気込み

不安だらけでしたが、なんとかやり切れたのでよかったです。

2月に社内の技術勉強会へ登壇したときですらとても緊張したのに、それから3ヶ月後に、社外で、かつ現在自分が使用してるソフトウェアの大きなカンファレンスに登壇するなんて考えてもみませんでした。来週誕生日なので28才最後の週末にいい経験ができました(笑)

反省も多かったので Autumn 目指してまた登壇できそうなネタを貯めていきたいと思います。
(次回もまた App Engineネタかなぁ....)

最後に、初登壇で不安だらけだった中、スライドのフィードバックや発表練習などで同僚の皆さんに多大な協力をいただきました。感謝です。
自分一人ではどうすることもできませんでしたmm

おしまい。懇親会のベイスターズビールはとても美味しかったです(ステマ

メモリアロケーションなしで slice をフィルタする

令和最初のエントリです。
連休前に教えてもらったことについてまとめました。

このエントリに記載する内容については、githubSliceTricks · golang/go Wiki · GitHub に記載されてる内容になります。

go の sliceについて

go の slice はポインタ型です。また、go で slice を初期化する際には、通常はメモリアロケーションが走ります。

// sample
a := []int{1, 2, 3}
b := make([]string, 0, 0)

go の slice については Go Slices: usage and internals - The Go Blog この辺が詳しいです。

slice をメモリアロケーションなしでフィルタする

ある slice から特定の値を持ってるものだけを抜き出し、別の slice にコピーするようなユースケースを考えてみます。
素直にコードを書くと以下のようになると思います。

func main() {
    arr := []string{"apple", "banana", "orange"}
    arr2 := make([]string, 0, 0)
    for _, v := range arr {
        v := v
        if v == "apple" {
            arr2 = append(arr2, v)
        }

    }

    fmt.Printf("arr:%v, address: %p\n", arr, arr)
    fmt.Printf("arr2:%v, address: %p\n", arr2, arr2)
}

// 出k力結果
// arr:[apple banana orange], address: 0x43e260
// arr2:[apple], address: 0x40c128

通常、こういったある slice の中から特定の値を取り出した slice を作り直したい(slice の値を詰め直す)ときは、別に詰め直す用の slice をインスタンス化しておいて、loopで回して詰め直すのが一般的な方法だと思います。

しかし、この方法だと、ソースとなる slice と詰め直す先の slice で二重にメモリアロケーションが必要になります。
※ 出力結果でもアドレスの値は異なります。

しかし、現実世界でプロダクトを運用しているとメモリが厳しく、slice 一つとっても極力メモリ空間を使いまわして、メモリアロケーションを走らせることなく slice をフィルタリングしたいユースケースってあると思います。ソースの slice で使用されてるメモリをうまいこと再利用して slice を詰め直す方法が SliceTricks · golang/go Wiki · GitHub に書いてありました。

この gowiki に記載されてる Tips を元に実際のサンプルコードを書き直してみます。

func main() {
    arr := []string{"apple", "banana", "orange"}
    arr2 := arr[:0] // ここが異なる。
    for _, v := range arr {
        v := v
        if v == "apple" {
            arr2 = append(arr2, v)
        }

    }

    fmt.Printf("arr:%v, address: %p\n", arr, arr)
    fmt.Printf("arr2:%v, address: %p\n", arr2, arr2)
}

// 出力内容
// arr:[apple banana orange], address: 0x43e260
// arr2:[apple], address: 0x43e260

ref: https://play.golang.org/p/D9ubw_22WTw

出力を確認すると、詰め直した先の slice もソースとなった slice のアドレスと同じなので、うまくメモリを再利用できています。

注意点も gowiki には記載してあります。

This trick uses the fact that a slice shares the same backing array and capacity as the original, so the storage is reused for the filtered slice. Of course, the > original contents are modified.

以下のような slice の各要素の値を変更する場合、同じアドレスで詰め替えを行うので元のソースの slice も書き換えてしまいます。

func main() {
    a := []string{"a", "b", "c"}
    b := a[:0]
    fmt.Printf("a: %[1]p = %+[1]v\n", a)

    for _, aa := range a {
        aa := aa
        b = append(b, fmt.Sprintf("1-%s", aa))
    }

    fmt.Printf("b: %[1]p = %+[1]v\n", b)
    fmt.Printf("a: %[1]p = %+[1]v\n", a)
}


// 出力結果
// a: 0x43e260 = [a b c]
// b: 0x43e260 = [1-a 1-b 1-c]
// a: 0x43e260 = [1-a 1-b 1-c]

ref: https://play.golang.org/p/LxYnXCUNpjP

MicroServices本3兄弟を読んで考えたこと

平成最後のエントリです。(ギリギリ間に合った...)
10連休中の目標であった『進化的アーキテクチャ』を読み切ることができたので備忘録を残します。

※ 僕は別に設計について明るいわけでもマイクロサービスについて明るいわけでもありません。

サマリ

  • 『マクロサービスアーキテクチャ』、『プロダクションレディマイクロサービス』、『進化的アーキテクチャ』のマイクロサービスアーキテクチャ本3兄弟(僕が勝手にそう呼んでるだけです。)を読み終えました。
  • 拡大するフェーズのマイクロサービス化を終えて、運用人数が限られて行く中でのマイクロサービスをどう運用するのかに最近の個人的な関心があります。

雑感

下記三冊を2年がかりくらいでようやく読み終えました。

そこで考えたことをまとめてみます。 マイクロサービスアーキテクチャって昨今、様々な企業で採用されるケースがとても増えてきてると思うんですよ。
特にモノリスで作られたサービスを分割する、という文脈でのマイクロサービスを採用するケースをよく目にするようになってきました。(個人の観測範囲の話です。)

僕も業務でマイクロサービスアーキテクチャで作られたサービスの運用に関わっています。
ただ、運用をする中で最近、成熟したプロダクトでマイクロサービスアーキテクチャを採用してる際にどう運用していくべきなのか、ということを考えてます。

というのも、組織や企業が拡大するフェーズにおいてマイクロサービスを採用するのは、マイクロサービス本に書かれてる通り嬉しいことが多いと思います。
その一方で、サービスとして成熟してしまって、自動化などもありつつ、開発・運用する人数が限られていく(減っていく)にも関わらず、マイクロサービスの数は変わらず、エンジニア1人が見るべきサービスの守備範囲(ドメインの守備範囲と言ってもいいかもしれません)が広がっていって負荷が上昇していく可能性がある場合に、どう安定的にサービスを運用していくのか、ということに個人的に興味があります。
サービスを拡大する中で、マイクロサービス化して、疎結合で、技術スタックも柔軟に、デプロイ頻度も増やして、プロダクトの改善イテレーションを高速に回していく、というのはプロダクトと組織が拡大する前提があってこそでは?と最近考えるようになりました。プロダクトが現状維持、開発メンバーの工数削減傾向というフェーズにある場合はどうするのか、まだその実践的な活きた情報は多くありません。

先を見据えながら、開発サイドがつらくならない程度でマイクロサービスを少しずつ減らしていく(統合していく)方向もあるでしょうし、さらなる自動化もあるでしょう。(あんまり積極的ではないですけど)ドキュメンテーションで解決できることもあるかなと妄想してます。

マイクロサービスは事実として採用されるケースが増えてきてますが、ある時点で、ナノサービスではない粒度で、適切な粒度に切られていたマイクロサービス達は、プロダクトそのものが成熟し、本当の意味での運用フェーズに入ったときにどういう手法をとるのがベターなのか、その実践的なプラクティスをどんどん知りたいなと思っています。

平成最後の雑エントリ終わり。令和でもアウトプットの継続頑張るぞ。

CircleCIの高速化ログ

Overview

circleci でのビルド時間の高速化をやったので、その備忘録です。

方針

circleci の高速化 = ビルド時間の短縮になるんですが、そんなことしたことなかったのでまず何をするかを考えました。
circleci の中でやってることは基本的には、手動で叩いてるコマンド群を設定することで自動でゴリゴリタスクを回してる、というだけです。
そのため、このタスクの実行時間を短縮することがそのまま circleci のビルド時間の短縮に繋がります。

やったこと

cacheを使う

npm install した結果の node_modules や パッケージ管理ツールで取得した中身をキャッシュして、キャッシュが存在する場合は、パッケージのインストールプロセスをスキップするようにしました。

circleci.com

steps:
      - restore_cache:
         keys: cache-yarn-{{ package.json }} // key名は適当
      - run:
          name: install node_modules
          command: |
            if [ ! -e node_modules ]; then
              yarn install // node_modulesがなければ yarn install が走る。cacheを解凍してすでにあれば走らない。
            fi
      - save_cache:
         key: cache-yarn-{{ package.json }}
         path: 
           - ./workingDir/node_modules

なんだかんだこれが一番効果がありました。npm にしろ yarn にしろ1分近くかかっていたのでcacheするだけで丸々1分削除できたのは大きかったです。

unittestを高速化する

普段 gae/go で開発をしているので、GitHub - favclip/testerator: test accelerator for appengine/go を使って appengine を使ったテストを高速化しました。
なぜ高速化できるのかについては以前のエントリでも記載しました。

ema-hiro.hatenablog.com

詳しくは testerator - GoDoc にある通りですが、インスタンスの残機が1つ以上残ってるケースではインスタンスを完全に落とさないので、起動が早くなります。 TestMain をうまく使いながら常に残機が1以上残ってるようにテストを組み立てると恩恵が大きいです。

fmtかけるファイルを減らす

gofmt などの format や lintツールを使ってる場合 vendor ディレクトリを検査対象から外すことで多少速くなります。

まとめ

circleci のビルド時間短縮なんてやったことなかったですが、案外やり始めると楽しかったです。
少しでも他の開発者の人が待ち時間少なくなると嬉しいなと思いました。

ビルドタグで appengine.Main() の向き先が変わる話

Overview

AppEngine の Go1.11 対応において gin の version を最新の 1.3 系にあげたときに以下の問題にはまったのでその調査メモです。

  • gin の version を現時点(201904)での最新の 1.3 にあげ、appengine.Main() を使うコードに変更すると、AppEngine / Go1.9 で動いていた c.Request.Header.Get("Host") が空文字で返ってきてしまい、既存コードに影響が出てしまったこと。

原因

  • gin1.3 以降の appengine.Main() では internal.Main() が呼ばれる。appengine.Main() の中身は appengineかどうかのビルドタグで制御されていて、Go1.9 までのビルドと Go1.11 からのビルドでは appengine.Main() の実行される先が異なる。

  • gin 1.3 系(Go1.11)

  • gin 1.1系(~Go1.9)

挙動の違いを調査するために Go1.9 までで動作していた appengine.Main() の定義元の appengine_internal.Main() の中身を探します。

appengine_internal パッケージについて。

appengine_internal パッケージは Go1.11 で対応した gin の appengine.Main() では呼ばれません。

探し方

goapp env GOROOT
/PATHTO/google-cloud-sdk/platform/google_appengine/goroot-1.9 

このように appengine の goroot が出力されるので、このディレクトリ配下の src を探します。
※ goapp の go の version は 1.9

まず見たのは以下 - /PATHTO/google-cloud-sdk/platform/google_appengine/goroot-1.9/src/appengine_internal/internal.go

ここの Main 関数を確認します。

func Main() {
    close(appPackagesInitialized)
    flag.Parse()
    serveHTTP()
}

serveHTTP() メソッドを確認します。

// serveHTTP serves App Engine HTTP requests.
func serveHTTP() {
    // The development server reads the HTTP address and port that the
    // server is listening to from stdout. We listen on 127.0.0.1:0 or
    // [::1]:0 to avoid firewall restrictions.
    conn, err := net.Listen("tcp", "127.0.0.1:0")
    if err != nil {
        log.Print("appengine: couldn't listen on IPv4 TCP socket: ", err)
        conn, err = net.Listen("tcp", "[::1]:0")
        if err != nil {
            log.Fatal("appengine: couldn't listen on IPv6 TCP socket: ", err)
        }
    }

    addr := conn.Addr().(*net.TCPAddr)

    fmt.Fprintf(os.Stdout, "%s\t%d\n", addr.IP, addr.Port)
    os.Stdout.Close()

    err = http.Serve(conn, http.HandlerFunc(handleFilteredHTTP))
    if err != nil {
        log.Fatal("appengine: ", err)
    }
}

この err = http.Serve(conn, http.HandlerFunc(handleFilteredHTTP)) に着目します。

func handleFilteredHTTP(w http.ResponseWriter, r *http.Request) {
    // Patch up RemoteAddr so it looks reasonable.
    if addr := r.Header.Get("X-Appengine-Remote-Addr"); addr != "" {
        r.RemoteAddr = addr
    } else {
        // Should not normally reach here, but pick
        // a sensible default anyway.
        r.RemoteAddr = "127.0.0.1"
    }

    // Create a private copy of the Request that includes headers that are
    // private to the runtime and strip those headers from the request that the
    // user application sees.
    creq := *r
    r.Header = make(http.Header)
    for name, values := range creq.Header {
        if !strings.HasPrefix(name, "X-Appengine-Dev-") {
            r.Header[name] = values
        }
    }
    ctx := &httpContext{req: &creq, done: make(chan struct{})}
    r = registerContext(r, ctx)

    http.DefaultServeMux.ServeHTTP(w, r)
    close(ctx.done)

    unregisterContext(r)
}

以下の部分を抜粋します。

r.Header = make(http.Header)
for name, values := range creq.Header {
    if !strings.HasPrefix(name, "X-Appengine-Dev-") {
        r.Header[name] = values
    }
}
  • 1.9以下でビルドされ、call されていた appengine_intenal.Main() では Header に関して X-Appengine-Dev-XXXX と言う文字列を持つヘッダー以外を Request Header に入れ直していました。

念の為、go1.11 以降で使用される appengine.Main() の実装について調べてみます。

エントリの冒頭でも記載してますが、 https://github.com/golang/appengine/blob/master/internal/main_vm.go#L19 を確認します。

func Main() {
    MainPath = filepath.Dir(findMainPath())
    installHealthChecker(http.DefaultServeMux)

    port := "8080"
    if s := os.Getenv("PORT"); s != "" {
        port = s
    }

    host := ""
    if IsDevAppServer() {
        host = "127.0.0.1"
    }
    if err := http.ListenAndServe(host+":"+port, http.HandlerFunc(handleHTTP)); err != nil {
        log.Fatalf("http.ListenAndServe: %v", err)
    }
}

この実装の中の handleHTTP の実装の中を見ます。 (ref:https://github.com/golang/appengine/blob/master/internal/api.go#L87-L152 )

※ 長いので header に値を set してる箇所のみ抜粋。
(ref: https://github.com/golang/appengine/blob/master/internal/api.go#L140 )

w.Header().Set(logFlushHeader, strconv.Itoa(flushes))

go1.11 以降で call されてる appengine.Main() の実装の中身を確認すると go1.9 までとは実装が異なっていました。

1.9以下でビルドされ、call されていた appengine_intenal.Main() では Header に関して X-Appengine-Dev-XXXX と言う文字列を持つヘッダー以外を Request Header に入れ直していました。

go1.11 以降では、Header から 取り出して Header に入れ直す処理を通っていないので c.Request.Header.Get("XXXX") で取り出せないものが発生していました。

まとめ

1.11 対応では call される appengine.Main() の中身が違うので、既存のコードを見直してみると良いかもしれません。

ビルドタグについて

qiita.com

追記

godoc の Request の説明を見ると

For incoming requests, the Host header is promoted to the Request.Host field and removed from the Header map.

ref: https://golang.org/pkg/net/http/#Request

と記載されていた。最新版の go のドキュメントによるとそもそも Request.Header の中に Host field は含まないことになったらしい。

intellij で go module の設定を行う

intellij で go module を設定して開発を行う設定を行います。

手順

Preference > Languages & Frameworks > Go > Go Modules(vgo) を選択。

f:id:ema_hiro:20190412020707p:plain

Enable Go Modules(vgo) integration を ON にする。

f:id:ema_hiro:20190412020719p:plain

Vgo Executable は 使っている go の version(SDK) を指定する。
Proxy は direct を設定する。

これで Go Modules の設定は完了です。

参考

Go modules (vgo) - Help | GoLand

app.yamlでmainを指定して静的ファイルをサーブする

Overview

Google App Engine 2ndGenにてapp.yamlの設定でmain property を指定して、mainで指定した path をプロジェクトの root として静的ファイル( static や template 配下のファイル)が正常に読み込めるかどうか調べました。

以前も静的ファイルのサーブに関連したエントリは書いたのですが、今回はそこからアップデートのあった内容も含めて記載してます。

ema-hiro.hatenablog.com

main propertyについて

公式ドキュメントには以下のように記載されています。

Optional. The path or fully qualified package name of the main package.

You must declare the path to the main package if your package main is not in the same directory as your app.yaml. The main element supports file paths relative to app.yaml or full package names.

cf. app.yaml Configuration File  |  App Engine standard environment for Go 1.11 docs  |  Google Cloud

  1. ファイルの相対パスはmain propertyで指定されたpathの相対パスになります。
  2. main.goがapp.yamlと同階層にないとき、main packageのパスを定義します。

app.yamlの設定

以下のようなディレクトリ構成に置いて

├── app
│   └── cmd
│       └── main.go
├── app.yaml
└── go.mod

app.yaml で main property に ./app/cmd を指定することで、このアプリケーションにおける main package の path を設定します。
これにより app.yaml と main.go が同階層になくても main で指定されたディレクトリの main package が app.yaml と同階層にあるものとして読み込むことができます。

sample app.yaml

runtime: go111
service: gae-go111-app
main: ./app/cmd 

GAE 2nd Generation 以降はデプロイした時に CloudBuild でアプリケーションのビルドが行われるので、CloudBuild の実行ログから main package を ./app/cmd に変更してるログを確認することができます。

Building /tmp/stagingXXXXXXXXX/srv, with main package at ./app/cmd, saving to /tmp/stagingXXXXXXXXX/usr/local/bin/start

正常に main が再設定されてる場合は上記のようなログが出力されます。

goのコードから静的ファイルを読み込む

実際に go のファイルの中で特定のファイルを開きたい、ようなケースがあった場合も main を指定することにより、main.go が app.yaml と違う階層にあった場合も、app.yaml がある階層をルートとして go のコードから呼ぶことができるようになります。
そのため、app.yaml を配置した階層と同じ階層に特定のファイルを配置して、そのファイルをダイレクトに指定する(相対パス等を考えなくていい)ことでファイルにアクセスすることができます。
具体的には ./src/app/cmd/main.go の内部で以下のような実装をしたとしても、app.yaml そのものが出力されます。

f, err := os.Open("app.yaml")
if err != nil {
    panic(err)
}

io.Writer(os.Stdout, f)

templateを読み込む

go のコードから静的なファイルを読み込む時と同様に app.yaml があるディレクトリと同じ階層に template のディレクトリを作成し go のコードから相対パスなしで直接 template のファイルを指定することで読み込むことができます。

ディレクトリ構成は以下です。

├── app
│   └── cmd
│       └── main.go
├── app.yaml
├── go.mod
├── handler
│   └── index.go
└── templates
    └── index.tmpl

template を呼び出す側は変更ありません。

tmpl, err := template.ParseFiles("templates/index.tmpl")
if err != nil {
    panic(err)
}
if err := tmpl.Execute(w, nil); err != nil {
    panic(err)
}

static ファイルを読み込む

最後に js や css といったファイルのサーブの設定ついてですが、これは元々の設定と変わらず handler property で設定します。
/static/js/index.js というファイルを読みたい場合は、app.yaml の static_dir に static を指定します。

ディレクトリ構成は以下

├── app
│   └── cmd
│       └── main.go
├── app.yaml
├── go.mod
├── handler
│   └── index.go
├── static
│   └── js
│       └── index.js
└── templates
    └── index.tmpl

sample app.yaml

runtime: go111
service: gae-go111-app
main: ./app/cmd
handlers:
  - url: /static
    static_dir: static
    secure: always

sample index.tmpl

{{ define "index.tmpl" }}
    <html lang="ja">
    <head>
        <title>test</title>
    </head>
    <body>
    <p>hello world</p>
    </body>
    <script src="/static/index.js" ></script>
    </html>
{{ end }}

これらを実際に設定してみて、デプロイすると template を読み込んだ時に一緒に js も読み込まれます。

※ localで go run ./PathTo/main.go で起動した場合、static ディレクトリにルーティングされない(appengine を起動しないといけない) ので、デプロイするか、 dev_appserver.py app.yaml で appengine を起動させてみての確認が必須です。

まとめ

app.yaml の main を指定することで 1st Generation の時のようなプロジェクト構成でも静的なファイルをサーブして読み込むことが可能です。

ref

コードはこちらに置いておきました。

github.com

自前でUnmarshalを実装したときの panic を回避する

カスタム Struct に自前で Unmarshal を実装して json -> object に decode するときに無限ループを引き起こして panic しないようにするTipsです。

サンプルケースですが、以下のコードは json を decodeす るときに panic を引き起こします。

type Person struct {
    Name string `json:"name"`
    Age int64 `json:"age"`
}

func (p *Person) UnmarshalJSON(data []byte) error {
    if p.Age == 0 {
        p.Age = 10
    }
    return json.Unmarshal(data, p)
}

refs: https://play.golang.org/p/7RtVampOalT

理由は json を decode する対象の Person struct に対して再帰的に UnmarshalJSON をかけてしまい無限ループに陥ってしまうからです。

こう言ったケースでは UnmarshalJSON メソッド内で別の type として Alias を切ることで無限ループを回避できます。

func (p *Person) UnmarshalJSON(data []byte) error {
    if p.Age == 0 {
        p.Age = 10
    }
    type Alias Person
    pp := &struct {
        *Alias
    }{
        Alias: (*Alias)(p),
    }
    return json.Unmarshal(data, pp)
}

ref: https://play.golang.org/p/lymBTGoMbB5

元ネタはこちらの記事です↓

http://choly.ca/post/go-json-marshalling/

テストの中で使うダミーデータを作成する

前提

を使ったケースにおいてデータベースのアクセスする実装のテストを書くときにダミーデータをセットアップする方法を記載します。

今回は追加でテストツールとして testerator を使っています。 testerator: testerator - GoDoc

愚直にデータを作って、消す

ベタな方法です。単体テストの内部でデータを作成してそのテストケースが終わったら削除して次のテストケースでもう一回作ります。

type Data struct {
    ID   int64  `datastore:"-"`
    Name string `datastore: "Name"`
}

func TestMethod (t *testing.T) {
    tests := []struct{
        name  string
        src   *Data
        want  string
    }{
        { name: "case_1", src: &Data{name: "alice"}, want: "bob"},
        { name: "case_1", src: &Data{name: "hoge"}, want: "fuga"},
    }
    
    _, ctx, _ := testerator.SpinUp()
    defer testerator.SpinDown()

    for _, tt := range tests {
        tt := tt
        t.Run(tt.name, func(t *testing.go) {
            k := datastore.NewIncompleteKey("SampleKind")
            kk, err := datastore.Put(ctx, k, tt.src)
            if err != nil {
                panic(err)
            }

            // 何かしらのテスト

            // テスト終了後にput時に返されたkeyを指定して削除する。
            if err := datastore.Delete(ctx, kk); err != nil {
                panic(err)
            }
        })
    } 
}

特に何も考えずにやるならこの方法かなと思います。DeleteするのはPutしたときに帰ってきたCreateされたレコードのDatastoreのKeyを使うことくらいかと思います。

ここでは純粋なDatastoreのパッケージ使いましたが、普段は業務ではgoon使ってるのでデータの作成と削除はgoonのPutとDeleteを使ってます。

テストケースごとにSpinDownする

これがもう一つ考えたのが、テストケース回すごとにInstance消せばdatastore丸ごとリセットできるんじゃないかと思って以下のようなパターンです。 上記のテストごとに作って消すでもいいと思ったんですが、常にフレッシュな状態のインスタンスでテストしたいケースとかあると思うのでテストケースごとにappengineのインスタンスを消すパターンを考えました。

testerator内のdatastoreやmemcacheをimportしておくと、SpinDown() ごとにdatastore、memcacheを丸ごと消してくれるのでテストケースごとに SpinDown() させます。

import (
    // do testerator feature setup
    _ "github.com/favclip/testerator/datastore"
    _ "github.com/favclip/testerator/search"
    _ "github.com/favclip/testerator/memcache"
)

# 略

type Data struct {
    ID   int64  `datastore:"-"`
    Name string `datastore: "Name"`
}

func TestMethod (t *testing.T) {
    tests := []struct{
        name  string
        src   *Data
        want  string
    }{
        { name: "case_1", src: &Data{name: "alice"}, want: "bob"},
        { name: "case_1", src: &Data{name: "hoge"}, want: "fuga"},
    }
    
    for _, tt := range tests {
        tt := tt
        t.Run(tt.name, func(t *testing.go) {
            _, ctx, _ := testerator.SpinUp()
            defer testerator.SpinDown() // func(t *testing.T) ごとに呼ばれる。
         
            k := datastore.NewIncompleteKey("SampleKind")
            kk, err := datastore.Put(ctx, k, tt.src)
            if err != nil {
                panic(err)
            }

            // 何かしらのテスト
        })
    } 
}

この方法、綺麗なインスタンスの状態が欲しいときだったり、Context引き回したくない場合にはいいかなと思いますが、テストごとにインスタンス立ち上げるコストがかかるので多分テスト遅くなります。

追記

testerator.SpinDownは、起動しているインスタンス数の残数が0の時はインスタンスをそのまま落としますが、1つ以上残数が残っているとインスタンスを落とさずに状態をcleanupのみしてくれます。

SpinDown dev server.

This function clean up dev server environment. However, internally there are two types of processing. #1. if internal counter == 0, spin down dev server simply. #2. otherwise, call each DefaultSetup.Cleaners. usually, it means cleanup Datastore and Search APIs. see document for SpinUp function.

refs: https://godoc.org/github.com/favclip/testerator#SpinDown

そのため テストケースごとにSpinDownする 場合のときは TestMainで先に testerator.SpinUp しておく良いです。そうすると、forの中でインスタンスのSpinUp/Down を繰り返しても、常にインスタンスの残機が1つ以上残ってる状態なのでインスタンスを落とさずにDatastoreの状態をCleanupしてくれて、毎回インスタンスを起動し直さなくても、まっさら状態のインスタンスを使えて、かつテストが高速化できます。

残機のカウントの実装は以下にあります。
refs: https://github.com/favclip/testerator/blob/master/testarator.go#L164-L175

まとめ

ケースバイケースだと思いますが、多分都度作って消す方がコスト安いし良さそうです。(個人の主観です。) TestMainの実装をするのであれば、テストケースごとに SpinUp/Down をする場合もアリかなと思います。

CookPad TechConf 2019に参加してきた

CookPad TechConf 2019に参加してきたのでさらっとまとめておきます。

概要

https://techconf.cookpad.com/2019

スライドは後日上記公式サイトで公開されるとのことなのでここでは省きます。

感想

箇条書きでまとめます。

  • 前半はTechConfというよりProductConfといった感じで、プロダクト開発における実践的な内容のセッションが多かったです。エンジニアだけでなく、PdMやデザイナーの方も参加するといいなと思いました。
  • Rubyに関連するセッションがありませんでした。
  • もの作りに置いて仮説検証にとても大事にしていました。
    • 検証過程や検証ツール、その都度作り手としてどんな仮説を立てているのか?ということを話ていただけました。
      • 実践「デザインスプリント」くらい徹底していたセッションもあって勉強になりました。
    • 話してる内容それ自体はサービス土着のものだけど仮説に至る経路は汎用的な内容でした。
    • 泥臭い内容もあって好き。
  • とにかく ユーザーのため ということが徹底されていました。
    • とても好印象。上述の仮説検証に重きを置いてることとも重複しますが、技術をユーザーのために使ってました。
      • コード0行でMVP作った話やカオスエンジニアリングのセッションでは特に感じました。

まとめると、プロダクト開発における技術の使い方の思想が一貫してると思いました。

  • 新規で開発するフェーズは技術に寄らず仮説の精度を高める施策をバシバシ打つ。
  • グロースするフェーズではゴリゴリ自動化するために技術を使いまくる。
  • しかし全てはユーザーのため、そしてそれを支える開発者のため。

雑にまとめましたがこんな感じでした。
ちなみにRubyの会社だと思ってましたがGoも使われてました。Gopherくんが登場していました。

そのほか

気づいたことまとめておきます。

  • 数年参加し続けてますが、多分平日開催だったの初めて。
  • 今年は司会がAlexaではなく人間だった。

App Engineでマルチテナントなアプリを作る

Google App Engine上でマルチテナントなアプリを作る方法について調査したのでブログにまとめてみます。

マルチテナントとは?

IT用語辞典には以下のように記載されています。

マルチテナントとは、SaaSクラウドコンピューティングなどで、機材やソフトウェア、データベースなどを複数の顧客企業で共有する事業モデル。

マルチテナントとは1つのアプリケーションを複数のクライアントで共有することを意味します。

App Engineにおけるマルチテナント

App Engineは Namespace API を使ったマルチテナントの実装がサポートされています。 具体的には 公式ドキュメント に以下のように記載されています。

複数のクライアント組織に対応する個別のデータ パーティション(テナント)を提供することで、「マルチテナンシー」をサポートできます。これにより、すべてのテナントで同じデータスキーマを保持しながら、各テナントのデータ値をカスタマイズできます。マルチテナンシーではテナントを追加するときにデータ構造を変更する必要がないため、新しいテナントのプロビジョニングが効率的になります。

これにより、1つのアプリケーションをNamespaceで区切られた複数の環境で同じように動作させることが可能で、さらにこのNamespaceで区切られた環境は互いに干渉することはありません。

Namespace API

マルチテナントアプリケーションを実現するために App Engineでは Namespace API が用意されています。

cloud.google.com

ドキュメントに記載されていますが、現在サポートされてる言語は javapython、goに3言語で、Namespace API名前空間を使用するApp Engine(※1)のサービスは以下です。

  • Datastore
  • MemCache
  • Task Queue
  • Search

※1. google.golang.org/appengine のappengine package。cloud.google.comの方ではない。

このAPIを使ってマルチテナントアプリケーションを構築することで、例えば以下のようなことが可能になります。

  • Namspaceで区切られた環境は互いに干渉することがないのでそれぞれ独立したアプリケーションとして動かすこと。
  • ある名前空間を持つ環境で操作したデータが別の名前空間に影響するようなことがない状態を作ること。
  • これにより 1つのコードベースで様々な用途の環境を同時に複数用意する こと。(ex. 検証環境 etc...)

実装方法

具体的な実装方法については 公式のドキュメントを参照してください。ここでは名前空間を指定してDatastoreの新しいデータを追加する方法について記載します。

// 登録する名前空間をcontextに入れる
ctx, err := appengine.Namespace(appengine.NewContext(req), "emahiro")
if err != nil {
   panic(err)
}

key := datastore.NewKey(ctx, "SampleKind", "prop", 0, nil)
// datastoreのput処理

これだけで名前空間で区切られた環境にデータが登録されます。consoleでは以下のようにKindを選ぶ前に名前空間を選ぶUIが表示されるようになります。
ここではAppEngineのデフォルトの名前空間の他に新しく登録した emahiro という名前空間が作られていることがわかります。

※ 繰り返しになりますが、詳細な実装方法、およびDatastore以外の実装については公式ドキュメントを参考にしてください。

contextのなかに Namespaceの文字列を入れるだけという非常にシンプルな方法で名前空間を分けてデータを入れることができました。
ここでは Datastoreでの実装方法を記載しましたが、Namespace APIは DatastoreとMemCacheの両方で使われるので https://github.com/mjibson/goon を使っていてもマルチテナントアプリケーションに対応できます。(Goonも中ではdatastoreのAPIを使っているで。)

まとめ

  • App Engineにはマルチテナントアプリケーションを構築できる機能が備わっている。
  • Namespace APIを使うと簡単に名前空間を設定可能。
  • 分けられた名前空間は互いに干渉しないので、検証や配布など様々な用途で使うことができる。

参考資料

初めて技術発表をした話

社内向けではありましたが、エンジニアキャリアの中で初めて大勢の前で技術発表をするという機会を貰い、本日発表をしてきたので、そこで感じた内容をまとめます。

発表する前の自分

  • キャリアの中で規模の大小に関わらず発表経験なし。もっぱら聞く専。
  • 自分のやってきたことが発表に値するものではないと感じていたので、技術的な発表に対して後ろ向きな姿勢。
  • 発表に対して間違いを指摘されたり、Feedbackで辛辣なことがくるのではないかとビビってた。

発表した後の自分

  • 案外喋れた。(ビビってたので結構準備したのは事実)
  • 内容だけでなくプレゼンの仕方そのものも結構難しい。
  • 思ってたほど怖くなかった。
  • そこまで辛辣なFeedbackは来ない。

発表を通して勉強になったこと

  • Google スライドの使い方
    • 特にプレゼン時にトークノートと一緒にスライドを表示させる方法に手間取ったりしました。
    • PDFの出力方法が最初わからなかったりしました。
  • 時間計測の重要性
  • スライドの作り方
    • スライドで伝えたいこととスライドの説明の補足(プレゼンそのもの)のバランスを取るのが難しかったです。どうしてもスライドに全て載せたくなってしまう。1スライド1センテンス(だったかな?)とか言いますけど、知ってるのとやるのとでは大違いでした。

今回は技術についての発表それ自体よりもプレゼンについて勉強したことが多かったです。

まとめ

案外自分のやってきたことは知らない誰かのためになることがあるんだなぁと実感しました。 自分でハードルを勝手に高くしてた節があるので、少し気が楽になったかなと思います。 (とはいえ、まだちょっと苦手です。)

あと発表後にいくつか質問受けて、うまく答えられなかったり、深く検討してなかったこともあるなーと思ってあとから調べようかなと思ったので、インプットとアウトプットのサイクルってこういうことか!って腹落ちしました。

まぁなんにしても無事に終わってよかったです。

追記

許可が降りたので当日発表した資料を記載します。
FeedbackはTwitterのDM等でいただけると。

speakerdeck.com

Intellij IDEA をCLIから起動する

Intellij IDEAをコマンドラインから起動する設定について記載します。

手順

  1. メニューバーのTools > Create Command-line Launcher ... を選択

image_create_cl_launcher

  1. コマンドの作成先を指定する(必要に応じてpathを通す。)

set_idea_cmd_path

  1. コマンド確認
$ which idea
/usr/local/bin/idea

使い方

コマンド単体で叩いて Intellij IDEA を起動することもできますが、プロジェクト指定すればプロジェクトまで一気に開いてくれます。

# 単体起動
$ idea

# プロジェクト起動
$ idea ./PROJECT_PATH

IDEを使ってるとDockから起動するか、spotlightやalfredから起動することが多いですが、やはりterminalから起動したくなることが多いので、設定方法をまとめてみました。

『静的解析をはじめよう - Gopherをさがせ!』をやった

golang.tokyoが公開しているCodeLabをやってみました。

golangtokyo.github.io

普段からあまりコードの静的解析をやっているわけではありませんが、Goで静的解析をする場合にはどうするのか、というのを学ぶにはとてもいい教材でした。

特に「構文解析」の章が勉強になって、以下の部分

   // ファイルごとのトークンの位置を記録するFileSetを作成する
    fset := token.NewFileSet()

    // ファイル単位で構文解析を行う
    f, err := parser.ParseFile(fset, "_gopher.go", nil, 0)
    if err != nil {
        log.Fatal("Error:", err)
    }

これだけで構文解析が完了して、あとはトークンの位置情報を記録した fset 変数を ast.Inspect メソッドに渡してガリガリ構文チェックする。
Goだと結構簡単にファイルの構文解析ができるんですね。

普段web開発にしか触れてないですけど、たまにこういったことに触れておきたいなと思いました。