通常
$cd | . ./config/fish/config.fish
GitHub - oh-my-fish/oh-my-fish: The Fish Shell Framework を使っているとき
$omf reload
以上
通常
$cd | . ./config/fish/config.fish
GitHub - oh-my-fish/oh-my-fish: The Fish Shell Framework を使っているとき
$omf reload
以上
Go Conference 2019 Spring - Gocon に登壇者として参加してきました。
登壇の詳細は以下になります。
タイトル: 「Go1.9 で作られた App Engine のサービスを Go1.11 に移行した話」 登壇資料
こういった大きなカンファレンスで発表するのは、エンジニアキャリアの中で初めてだったので、実際に登壇者として参加してみた学びと反省、感想をまとめてみました。
社外の大きなカンファレンスでの初めての登壇でした。
かなり緊張しましたが、実際に登壇をしてみると、聞く側と話す側では全く違いました。
普段から使ってる App Engineについての発表にも関わらず、細かいところは案外曖昧で、理解を一段進める意味でも、発表する機会を頂けたことは良かったです。
ただ登壇については、練習では時間通りに進められていましたが、当日思わぬトラブルがあり、最終的に時間が足りなくなったために、最後の方は駆け足でまとめてしまいました。
流石に今の自分にはリカバれるスキルはなかったので、次回以降の課題として持ち越しかなと思います。
Google App Engineについての発表をしました。
タイムリーに話題に上がっている内容で、かつ普段の業務で行なっている内容を取り上げることができました。
ただし、少しApp Engineによりすぎてしまったと思っており、知ってる人にはわかるけど、知らない人は置いてけぼり...みたいな内容にもなってしまったかなと思います。ここはもう少し説明を増やしたり、コンテンツの量を調整するなど、練り込める余地は残っていました。
加えて、Go の話はバージョンをあげること以外であまり盛り込むことができませんでした。
視認性の高いテキストサイズ、コードのシンタックスハイライト、細かい文言や単語の表記揺れなどPCでスライドを作っているときは気にならなくても、実際に話してみると、見づらいスライドに当初はなってしまってました。
特にコードのシンタックスハイライトは結構悩んで、最初はダーク系にしていたんですが、ライト系にした方がいいとアドバイスもらったのでそちらを採用しました。
幸い、自分のセッションのときは部屋を暗くしてもらったので、シンタックスハイライトが原因でコードが見づらい、ということはなかったと思います。
ダーク系の場合、部屋が明るかったときに見えづらい可能性もあったので、この辺の塩梅がとても悩ましかったです。
全く知見がないので、スライドで視認性が高く、かつ部屋の大きさや明るさに左右されないシンタックスハイライトのテーマについて教えて欲しいです...。そういうエントリとかないかあ...。
また、スライドを作りながら、テキストは少量で、テキストサイズは大きいに越したことはない、ということを感じました。
作成してる時はPCに向かっているので、特に文字サイズについて気にすることがありませんでしたが、GoCon前に社内で発表練習していたときに、文字サイズのフィードバックを多くもらいました。
自分がちょうどいいと思うサイズは案外小さく見づらく、大き過ぎでは?と思うサイズくらいがちょうどいいのかなと感じました。
なお、こちらは余談ですが、Googleスライド力がまた少し上がりました。
Outputする機会があるごとに新しい機能を知っている気がします。今回も2つくらい新しい機能を知ることができました。
Googleスライドってとても便利ですね(小並)
これが一番難しかったです。
ストーリーに仕立てることに非常に苦労しました。
スライド間のつなぎの言葉というか、話してるスライドから次のスライドに行くときに、どういう接続をすると綺麗にプレゼンが流れていくのかは試行錯誤しました。
今回に関しては無理矢理繋げていった、という感じは否定できないので、スライドのアウトラインに加えてストーリー構成を考えてからプレゼンを組み立てていくといいのかなと思いました。
ちなみに、自分は普段めちゃくちゃ早口で、今回みたいな緊張したときは、いつも以上にさっさと進んでいくのかと思いきや、なぜか当日登壇してみたらスライドが全く進まず、なんでや!となりました。
(時間計りながら、なんで進まんのや...と自分が一番驚きました。)
不安だらけでしたが、なんとかやり切れたのでよかったです。
2月に社内の技術勉強会へ登壇したときですらとても緊張したのに、それから3ヶ月後に、社外で、かつ現在自分が使用してるソフトウェアの大きなカンファレンスに登壇するなんて考えてもみませんでした。来週誕生日なので28才最後の週末にいい経験ができました(笑)
反省も多かったので Autumn 目指してまた登壇できそうなネタを貯めていきたいと思います。
(次回もまた App Engineネタかなぁ....)
最後に、初登壇で不安だらけだった中、スライドのフィードバックや発表練習などで同僚の皆さんに多大な協力をいただきました。感謝です。
自分一人ではどうすることもできませんでしたmm
令和最初のエントリです。
連休前に教えてもらったことについてまとめました。
このエントリに記載する内容については、github の SliceTricks · golang/go Wiki · GitHub に記載されてる内容になります。
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 にコピーするようなユースケースを考えてみます。
素直にコードを書くと以下のようになると思います。
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]
平成最後のエントリです。(ギリギリ間に合った...)
10連休中の目標であった『進化的アーキテクチャ』を読み切ることができたので備忘録を残します。
※ 僕は別に設計について明るいわけでもマイクロサービスについて明るいわけでもありません。
下記三冊を2年がかりくらいでようやく読み終えました。
そこで考えたことをまとめてみます。
マイクロサービスアーキテクチャって昨今、様々な企業で採用されるケースがとても増えてきてると思うんですよ。
特にモノリスで作られたサービスを分割する、という文脈でのマイクロサービスを採用するケースをよく目にするようになってきました。(個人の観測範囲の話です。)
僕も業務でマイクロサービスアーキテクチャで作られたサービスの運用に関わっています。
ただ、運用をする中で最近、成熟したプロダクトでマイクロサービスアーキテクチャを採用してる際にどう運用していくべきなのか、ということを考えてます。
というのも、組織や企業が拡大するフェーズにおいてマイクロサービスを採用するのは、マイクロサービス本に書かれてる通り嬉しいことが多いと思います。
その一方で、サービスとして成熟してしまって、自動化などもありつつ、開発・運用する人数が限られていく(減っていく)にも関わらず、マイクロサービスの数は変わらず、エンジニア1人が見るべきサービスの守備範囲(ドメインの守備範囲と言ってもいいかもしれません)が広がっていって負荷が上昇していく可能性がある場合に、どう安定的にサービスを運用していくのか、ということに個人的に興味があります。
サービスを拡大する中で、マイクロサービス化して、疎結合で、技術スタックも柔軟に、デプロイ頻度も増やして、プロダクトの改善イテレーションを高速に回していく、というのはプロダクトと組織が拡大する前提があってこそでは?と最近考えるようになりました。プロダクトが現状維持、開発メンバーの工数削減傾向というフェーズにある場合はどうするのか、まだその実践的な活きた情報は多くありません。
先を見据えながら、開発サイドがつらくならない程度でマイクロサービスを少しずつ減らしていく(統合していく)方向もあるでしょうし、さらなる自動化もあるでしょう。(あんまり積極的ではないですけど)ドキュメンテーションで解決できることもあるかなと妄想してます。
マイクロサービスは事実として採用されるケースが増えてきてますが、ある時点で、ナノサービスではない粒度で、適切な粒度に切られていたマイクロサービス達は、プロダクトそのものが成熟し、本当の意味での運用フェーズに入ったときにどういう手法をとるのがベターなのか、その実践的なプラクティスをどんどん知りたいなと思っています。
平成最後の雑エントリ終わり。令和でもアウトプットの継続頑張るぞ。
circleci でのビルド時間の高速化をやったので、その備忘録です。
circleci の高速化 = ビルド時間の短縮になるんですが、そんなことしたことなかったのでまず何をするかを考えました。
circleci の中でやってることは基本的には、手動で叩いてるコマンド群を設定することで自動でゴリゴリタスクを回してる、というだけです。
そのため、このタスクの実行時間を短縮することがそのまま circleci のビルド時間の短縮に繋がります。
npm install した結果の node_modules や パッケージ管理ツールで取得した中身をキャッシュして、キャッシュが存在する場合は、パッケージのインストールプロセスをスキップするようにしました。
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分削除できたのは大きかったです。
普段 gae/go で開発をしているので、GitHub - favclip/testerator: test accelerator for appengine/go を使って appengine を使ったテストを高速化しました。
なぜ高速化できるのかについては以前のエントリでも記載しました。
詳しくは testerator - GoDoc にある通りですが、インスタンスの残機が1つ以上残ってるケースではインスタンスを完全に落とさないので、起動が早くなります。 TestMain をうまく使いながら常に残機が1以上残ってるようにテストを組み立てると恩恵が大きいです。
gofmt などの format や lintツールを使ってる場合 vendor ディレクトリを検査対象から外すことで多少速くなります。
circleci のビルド時間短縮なんてやったことなかったですが、案外やり始めると楽しかったです。
少しでも他の開発者の人が待ち時間少なくなると嬉しいなと思いました。
AppEngine の Go1.11 対応において gin の version を最新の 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)
internal.Main()
が呼ばれている方です。gin 1.1系(~Go1.9)
appengine.Main()
が実行されます。挙動の違いを調査するために 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 } }
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() の中身が違うので、既存のコードを見直してみると良いかもしれません。
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 は含まないことになったらしい。
Google App Engine 2ndGenにてapp.yamlの設定でmain property を指定して、mainで指定した path をプロジェクトの root として静的ファイル( static や template 配下のファイル)が正常に読み込めるかどうか調べました。
以前も静的ファイルのサーブに関連したエントリは書いたのですが、今回はそこからアップデートのあった内容も含めて記載してます。
公式ドキュメントには以下のように記載されています。
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
以下のようなディレクトリ構成に置いて
├── 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 のファイルの中で特定のファイルを開きたい、ようなケースがあった場合も 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)
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) }
最後に 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 の時のようなプロジェクト構成でも静的なファイルをサーブして読み込むことが可能です。
コードはこちらに置いておきました。
カスタム 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
元ネタはこちらの記事です↓
を使ったケースにおいてデータベースのアクセスする実装のテストを書くときにダミーデータをセットアップする方法を記載します。
今回は追加でテストツールとして 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を使ってます。
これがもう一つ考えたのが、テストケース回すごとに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に参加してきたのでさらっとまとめておきます。
https://techconf.cookpad.com/2019
スライドは後日上記公式サイトで公開されるとのことなのでここでは省きます。
箇条書きでまとめます。
まとめると、プロダクト開発における技術の使い方の思想が一貫してると思いました。
雑にまとめましたがこんな感じでした。
ちなみにRubyの会社だと思ってましたがGoも使われてました。Gopherくんが登場していました。
気づいたことまとめておきます。
Google App Engine上でマルチテナントなアプリを作る方法について調査したのでブログにまとめてみます。
IT用語辞典には以下のように記載されています。
マルチテナントとは、SaaSやクラウドコンピューティングなどで、機材やソフトウェア、データベースなどを複数の顧客企業で共有する事業モデル。
マルチテナントとは1つのアプリケーションを複数のクライアントで共有することを意味します。
App Engineは Namespace API を使ったマルチテナントの実装がサポートされています。 具体的には 公式ドキュメント に以下のように記載されています。
複数のクライアント組織に対応する個別のデータ パーティション(テナント)を提供することで、「マルチテナンシー」をサポートできます。これにより、すべてのテナントで同じデータスキーマを保持しながら、各テナントのデータ値をカスタマイズできます。マルチテナンシーではテナントを追加するときにデータ構造を変更する必要がないため、新しいテナントのプロビジョニングが効率的になります。
これにより、1つのアプリケーションをNamespaceで区切られた複数の環境で同じように動作させることが可能で、さらにこのNamespaceで区切られた環境は互いに干渉することはありません。
マルチテナントアプリケーションを実現するために App Engineでは Namespace API が用意されています。
ドキュメントに記載されていますが、現在サポートされてる言語は java、python、goに3言語で、Namespace APIで名前空間を使用するApp Engine(※1)のサービスは以下です。
※1. google.golang.org/appengine のappengine package。cloud.google.comの方ではない。
このAPIを使ってマルチテナントアプリケーションを構築することで、例えば以下のようなことが可能になります。
具体的な実装方法については 公式のドキュメントを参照してください。ここでは名前空間を指定して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を使っているで。)
社内向けではありましたが、エンジニアキャリアの中で初めて大勢の前で技術発表をするという機会を貰い、本日発表をしてきたので、そこで感じた内容をまとめます。
今回は技術についての発表それ自体よりもプレゼンについて勉強したことが多かったです。
案外自分のやってきたことは知らない誰かのためになることがあるんだなぁと実感しました。 自分でハードルを勝手に高くしてた節があるので、少し気が楽になったかなと思います。 (とはいえ、まだちょっと苦手です。)
あと発表後にいくつか質問受けて、うまく答えられなかったり、深く検討してなかったこともあるなーと思ってあとから調べようかなと思ったので、インプットとアウトプットのサイクルってこういうことか!って腹落ちしました。
まぁなんにしても無事に終わってよかったです。
社内のエンジニア向け勉強会で発表してきました。死ぬほど緊張しました。
— ema (@ema_hiro) 2019年2月19日
許可が降りたので当日発表した資料を記載します。
FeedbackはTwitterのDM等でいただけると。
Intellij IDEAをコマンドラインから起動する設定について記載します。
$ which idea
/usr/local/bin/idea
コマンド単体で叩いて Intellij IDEA を起動することもできますが、プロジェクト指定すればプロジェクトまで一気に開いてくれます。
# 単体起動 $ idea # プロジェクト起動 $ idea ./PROJECT_PATH
IDEを使ってるとDockから起動するか、spotlightやalfredから起動することが多いですが、やはりterminalから起動したくなることが多いので、設定方法をまとめてみました。
golang.tokyoが公開しているCodeLabをやってみました。
普段からあまりコードの静的解析をやっているわけではありませんが、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開発にしか触れてないですけど、たまにこういったことに触れておきたいなと思いました。