Go Conference 2019 Autumnに行ってきたので興味を惹かれたいくつかの発表をまとめます。
メルペイ @zoncoen 資料
webアプリケーションのE2Eテストを行うツールScenarigoを開発した。
Postmanの不満
シナリオをYAMLで
title: echo-service
steps:
- title: POST /echo
protocol: http
request:
method: POST
url: '{{env.ECHO_ADDR}}/echo'
body:
message: hello
expect:
body:
message: '{{request.message}}'
CLIから実行
$ ECHO_ADDR=http://localhost:8080 scenarigo run ./testdata/
=== RUN testdata/scenarios/test.yaml
=== RUN testdata/scenarios/test.yaml/echo-service
=== PAUSE testdata/scenarios/test.yaml/echo-service
=== CONT testdata/scenarios/test.yaml/echo-service
=== RUN testdata/scenarios/test.yaml/echo-service/POST_/echo
--- PASS: testdata/scenarios/test.yaml (0.00s)
--- PASS: testdata/scenarios/test.yaml/echo-service (0.05s)
--- PASS: testdata/scenarios/test.yaml/echo-service/POST_/echo (0.05s)
Goのパッケージになってるのでテストのファイルに書いて実行できる。 > これは便利そう
package main
import (
"testing"
"github.com/zoncoen/scenarigo"
"github.com/zoncoen/scenarigo/context"
)
func TestEchoService(t *testing.T) {
r, err := scenarigo.NewRunner(
scenarigo.WithScenarios("testdata/scenarios"),
)
if err != nil {
t.Fatalf("failed to create a test runner: %s", err)
}
r.Run(context.FromT(t))
}
他のシナリオYAMLファイルをinclude して使い回せる。
title: echo-service
steps:
- title: login
include: './login.yaml' #login処理の使いまわし
bind:
vars:
userToken: '{{vars.userToken}}' # user tokenを変数に受け取るイメージ
- title: POST /echo
protocol: http
request:
method: POST
url: '{{env.ECHO_SERVICE_ADDR}}/echo'
header:
Authorization: 'Bearer {{vars.userToken}}' # 変数に入れたuser token が使える
body:
message: hello
expect:
body:
message: '{{request.message}}'
Goで拡張
title: echo-service
plugins:
gen: gen.so
echo: echo.so
steps:
- title: say
protocol: grpc < Use gRPC
request:
client: '{{plugins.echo.NewClient()}}'
method: Say
body:
id: '{{plugins.gen.UUID()}}' < Call Go function
message: hello
expect:
body:
message: '{{request.message}}'
UUIDをランダム生成する処理をGoで書いておいてYAMLから呼び出せたりする
Go拡張機能のために標準パッケージのPluginパッケージを使った
-> 今回は3の手法
mainのパッケージでコードを書くだけ。 Exported されてる関数と変数を使う側で参照できる
package main
var Variable = "variable"
func Function() string {
return "function"
}
ビルド時にオプションを入れてビルド
$ go build -buildmode=plugin -o gen/plugin.so src/plugin.go
参照側ではバイナリファイルを指定してOpenし、関数名などをLookupして型アサーションして使う。
package main
import (
"fmt"
"plugin"
)
func main() {
p, err := plugin.Open("gen/plugin.so")
if err != nil {
panic(err)
}
symbol, err := p.Lookup("Function")
if err != nil {
panic(err)
}
f := symbol.(func() string)
fmt.Println(f())
}
なぜpluginパッケージを使ったか?
Abema TV @tomiokasyogo 資料
コンテナはホストOS上の独立した実行環境で、 その実態はコンテナランタイムによってホストOSのリソースを隔離、制限されたプロセス
コンテナ関連のプロダクトにはGo言語が多く使われている。 Docker, k8s, Istio...
など
FROM golang:1.13.1-alpine as builder #Baseになるイメージの指定
RUN apk add --nocache ca-certificates git
ENV PROJECT /github.com/tommy-sho/app/app
WORKDIR /go/src/$PROJECT
ENV GO111MODULE on
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /go/bin/app . #ビルドの実行
ENTRYPOINT ["/go/bin/app"] #アプリケーションの実行
参考 至高のDockerイメージ生成を求めて -2019年版-
Podのシャットダウンサイクル
Grace periodを超えた場合、SIGKILLが送られる
Grace period の初期値は30秒(変更可)
// サーバーの起動
go func(){
logger.Info("server start serving")
if err := server.Start(":8080"); err != http.ErrServerClosed {
logger.Fatal("Server closed with err:",zap.Error(err))
}
}()
//シグナルを受け取るチャネルを作成
stopChan := make(chan os.Signal,1)
signal.Notify(stopChan,
os.Interrupt,
syscall.SIGTERM,
syscall.SIGINT,
)
//シグナルを受け取るまでブロック
<-stopChan
// shutdown処理
1.PreStop hookが設定されている場合、最初に実行される
2.SIGTERMがコンテナに送られる(複数の場合、順不同)
ここまでの処理と、
3.ServiceやReplicaSetのPodリストから削除
の処理は非同期なため、Podの削除がPodのServiceからの削除より早いと、該当Podへのリクエストが失敗する可能性がある。
上記の問題を解決するために、 preStopHook
を利用する。
containers:
- name: app
image: app:v1
ports:
- containerPort: 8080
lifecycle:
preStop:
exec:
command: ["sleep","10"]
まずはトラフィックが受けいれられる状態かをチェックするReadinessProbeをおすすめ
通常DB接続先などが異なるので、設定はコードと分離する必要がある -> 環境変数として格納する
ただし、アプリケーション内部の設定は含まない (serverのkeep aliveなど)
環境変数を扱うライブラリ
json tagのように env:"HOME"
アノテーションしておくと、 env.Parse(&cfg)
で環境変数を構造体へマッピングしてくれる
ログのオススメライブラリuber-go/zap
herokuの開発者、設立者のAdam Wigginsによって提唱 Twelve-Factor App
など12の原則が述べられている
さらに、クラウドネイティブアプリ向けに追記された Beyond The Twelve-Factor App もある
DeNA @avvmoto 資料
Goでダイクストラ法を高速に実装する
ダイクストラ法
lengthやcapacityを指定してsliceを作成する
s1 := make([]int,length) // capacity = length
s2 := make([]int,length,capacity)
事前に必要な長さの目安をcapacity に明示しよう (appendを使えば、それ以上に要素を追加してもOK)
メモリアロケートの回数は少なくしよう
s1 := make([]*BigStruct,0,1e5)
構造体のポインタへのスライスだと、ポインタしかアロケートされていない。
s1 := make([]*BigStruct,0,1e5)
for i := 0; i < 1e5; i++{
s1 = append(s1,&BigStruct{}) // 構造体は、都度allocateされている
}
構造体のスライスとして、一回でまるっとアロケートしておこう
s2 := make([]BigStruct,0,1e5)
lengthを延長しアロケート済みの領域を使おう
例:構造体のスライスに要素を1つ追加する
s := make([]BigStruct,0,c)
for i := 0; i < E; i++{
if len(s) + 1 < cap(s){
s = s[:len(s) + 1 ] // sliceの延長
InitBigStruct(&s[len(s)])
} else if cap(s) < len(s) +1{
s = append(s,NewBigStruct()) // capacityが足りなくなったら、appendで規定配列を拡張
}
}
マップのキーはハッシュ値に変換され、ハッシュ値の下位ビットがBucketの選択に用いられる
ベストプラクティス
capacity hint
を指定してmapを作ることで mapの拡張が最小限に抑えられるmake(map[string]int,100)
身も蓋もないことを言うとMapよりSliceのほうが早いので、速度を求めるなら極力Mapを避けて実装する。
mapにもcapacityがあるのは知らなかった。
DeNA @theoden9014 資料
コンパイラで検出できないエラーや構文を静的解析によって検査するツールのこと
Linterを使う利点
var Analyzer = &analysys.Analyzer{
Name: "sample", // Analyzerの名前
Doc: "this is sample" // 説明
Requires: []*analysys.Analyzer{inspect.Analyzer}, // 依存するAnalyzerのリスト
Run: run, // 実際の解析処理をここに記述する(package単位で実行される)
FactTypes: nil, // このAnalyzer内で複数パッケージをまたいだデータ共有等を行いたい場合に利用する
ResultType: nil, // このAnalyzerが解析した結果を他のAnalyzerにも提供したい場合はここで型を宣言する
}
我々にはカスタムする需要はまだなさそう。
一般的なルールはこれを使うといい。
.golangci.yml
をプロジェクトルートに作成これは入れてチェックしてみたい。