- 投稿日:2019-12-11T20:53:56+09:00
gormの間違った記述を静的解析でチェックする
本記事はGo3 Advent Calendar 2019の12日目の記事です。前回の記事は Goで別パッケージの関数呼び出しは禁止すべき でした。
概要
gormは便利ですが、色々な記述の仕方ができたり、エラーの検知がしづらかったりと意図しないコードを作ってしまい、それがまたコンパイルを通ってしまうことがあります。この記事ではそれを静的解析で検知できないかというアプローチをします。
gormで困る事
gormで困る事といえば、色々な書き方ができてしまうという事と、明らかにおかしな処理でもコンパイルを通って動いてしまうということがあります。
例えば、
db = db.Where(“条件A”) db = db.Where(“条件B”) db = db.Find(&someModel)という処理は
db = db.Where(“条件A”).Where(“条件B”).Find(&someModel)ともかけるし、
db = db.Where(“条件A”) db = db.Find(&someModel,“条件B”)とも書くことができます。また、
db = db.Where(“条件A”) db = db.Find(&someModel) db = db.Where(“条件B”)と書いてもコンパイルは通ってしまいます。
苦肉の策として、以下のようにルールづけをして業務を行ってますが、なんとか機械的に検知したいところです。作成できたもの
まだ作成途中ですが、パイプを使った書き方についてについては検知ができました!以下で公開をしています。今後機能を追加していく予定です。
作成するまでの流れ
Analyzerのスケルトンコードを作る
gostaticanalysis/skeletonというツールででベースとなるコード一式を作ることができ、非常に便利です。
今回作成したツールもメインとなるgormchecker.goと、テストデータ以外変更をせずに作成できています。go get -u github.com/gostaticanalysis/skeleton skeleton gormchecker一番シンプルなチェッカー
以下で、変数名のチェックができています。動作確認は
go test
で行えます。これをベースに作っていきます。gormchecker.gofunc run(pass *analysis.Pass) (interface{}, error) { inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) nodeFilter := []ast.Node{ (*ast.Ident)(nil), } inspect.Preorder(nodeFilter, func(n ast.Node) { switch n := n.(type) { case *ast.Ident: if n.Name == "Gopher" { pass.Reportf(n.Pos(), "name of identifier must not be ’Gopher’") } } }) return nil, nil }testdata/src/a/a.gopackage a var Gopher int // want "name of identifier must not be ’Gopher’" var gopher int // OK作るまでのアプローチ
実際に意図した記述と、意図していない記述を書いてその構造上の違いをみていくのがわかりやすそうです。
まず以下のようにテストを書きます。パイプを検知できればゴールです。
testdata/src/a/a.gofunc check() { db, _ := getConnection() db = db.Where("column_a = xxx") db = db.Where("column_a = xxx").Where("column_b = xxx") }ast.Printを使えばデバッグができますが、大量に出て読みづらいので以下のようにデバッグを入れます。
gormchecker.gofunc run(pass *analysis.Pass) (interface{}, error) { inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) nodeFilter := []ast.Node{ (*ast.Ident)(nil), } inspect.Preorder(nodeFilter, func(n ast.Node) { //デバッグ追加ここから fmt.Printf("### pos %v\n", n.Pos()) position := pass.Fset.Position(n.Pos()) fmt.Println(position) ast.Print(pass.Fset, n) //デバッグ追加ここまで switch n := n.(type) { case *ast.Ident: if n.Name == "Gopher" { pass.Reportf(n.Pos(), "name of identifier must not be ’Gopher’") } } }) return nil, nil }これでデバッグしつつ進めることができそうです。
### pos 3480661 /Users/gosagawa/go/src/github.com/gosagawa/gormchecker/testdata/src/a/a.go:77:10 0 *ast.Ident { 1 . NamePos: /Users/gosagawa/go/src/github.com/gosagawa/gormchecker/testdata/src/a/a.go:77:10 2 . Name: "Where" 3 } ### pos 3480685 /Users/gosagawa/go/src/github.com/gosagawa/gormchecker/testdata/src/a/a.go:77:34 0 *ast.Ident { 1 . NamePos: /Users/gosagawa/go/src/github.com/gosagawa/gormchecker/testdata/src/a/a.go:77:34 2 . Name: "Where" 3 }根本に戻って、パイプを検知するためには一行で二回呼んでいる関数を検知すればいけそうです。ast.Identでフィルタリングしているものは変数なので、関数を検知する必要がありそうです。ast.Nodeインターフェースを実装する型をみた際ast.CallExprがそのようです。nodefilterを書き換えます。
gormchecker.gonodeFilter := []ast.Node{ (*ast.CallExpr)(nil), }すると、以下のような構造になっていることがわかります。非常に多くの情報を持っていますが、
(*ast.CallExpr).Fun.X.Nameで呼び出しもと、(*ast.CallExpr).Fun.Sel.Nameで関数名を取れそうです。
ast.SelectorExprとはxxx.yyyというようなセレクターを示すノードであることも知ることができます。/Users/gosagawa/go/src/github.com/gosagawa/gormchecker/testdata/src/a/a.go:77:7 0 *ast.CallExpr { 1 . Fun: *ast.SelectorExpr { 2 . . X: *ast.Ident { 3 . . . NamePos: /Users/gosagawa/go/src/github.com/gosagawa/gormchecker/testdata/src/a/a.go:77:7 4 . . . Name: "db" 5 . . . Obj: *ast.Object { 6 . . . . Kind: var 7 . . . . Name: "db" (中略) 104 . . } 105 . . Sel: *ast.Ident { 106 . . . NamePos: /Users/gosagawa/go/src/github.com/gosagawa/gormchecker/testdata/src/a/a.go:77:10 107 . . . Name: "Where" 108 . . } 109 . } 110 . Lparen: /Users/gosagawa/go/src/github.com/gosagawa/gormchecker/testdata/src/a/a.go:77:15 111 . Args: []ast.Expr (len = 1) { 112 . . 0: *ast.BasicLit { 113 . . . ValuePos: /Users/gosagawa/go/src/github.com/gosagawa/gormchecker/testdata/src/a/a.go:77:16 114 . . . Kind: STRING 115 . . . Value: "\"column_a = xxx\"" 116 . . } 117 . } 118 . Ellipsis: - 119 . Rparen: /Users/gosagawa/go/src/github.com/gosagawa/gormchecker/testdata/src/a/a.go:77:32 120 }あとは以下のようにして、同じ行の関数の数を調べればやりたいことが実現できました。本当はdbという変数がgorm.DBであるかをチェックできれば良いのですが、そこまではできておらず変数名がdbか?という苦肉の策をとってます。
func run(pass *analysis.Pass) (interface{}, error) { inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) nodeFilter := []ast.Node{ (*ast.CallExpr)(nil), } functions := make(map[string]map[int][]string) inspect.Preorder(nodeFilter, func(n ast.Node) { switch n := n.(type) { case *ast.CallExpr: switch f := n.Fun.(type) { case *ast.SelectorExpr: if x, ok := f.X.(*ast.Ident); ok && x.Name != "db" { break } functionName := f.Sel.Name if _, ok := functions[position.Filename]; !ok { functions[position.Filename] = make(map[int][]string) } functions[position.Filename][position.Line] = append(functions[position.Filename][position.Line], functionName) if len(functions[position.Filename][position.Line]) > 1 { pass.Reportf(n.Pos(), "do not use pipe") } } } }) return nil, nil }今後の展開
今できるのはパイプの検知のみですが、以下のケースを関数の位置、パラメータ数などを取得することにより取得できそうです。
- 関数内でfindを複数回呼ばない、findの後にWhere等他の関数を書かない
- findのなかに条件式を入れない
- スライスを取得するものにfirstを使わない(一件しか使わない)
まとめ
静的解析というと非常に難しい印象だったのですが、このようにテンプレートがあった上で、デバッグをしながらやると比較的容易に構文を把握しつつ静的解析ツールが作れてしまうので素晴らしいなと思います。gormchecker自体は業務で使えるレベルにすべくより機能を加えていければと思います。
謝辞
静的解析自体は以下の本を非常に参考にさせていただきました。
この記事では静的解析自体の細かな説明は省きましたが、原理や仕組みが丁寧に解説されています。逆引きGoによる静的解析入門
https://knsh14.booth.pm/items/1319336また、gcpub/zaganeというspannerの問題のあるコードを検知するツールが、やりたいと思う事と近く参考になりました。
https://github.com/gcpug/zaganeありがとうございます!
- 投稿日:2019-12-11T19:03:35+09:00
【Golang + echo】GoとechoでSQLのデータを表示【PostgreSQL】
1.はじめに
記事をご覧いただきありがとうございます。
今までC#とMySQLの接続は行ったことがあったのですが
ほとんど触れたことのないGo言語(+echo)とPostgreSQLとの接続
を行うことになったので備忘録的に手順を記していこうかと思います。初歩的なところから説明するので適宜読み飛ばしていただければと思います。
2.PostgreSQLの設定
2-1.PostgreSQLのダウンロード
PostgreSQLのHPにアクセスしてダウンロードをクリック
Binary packagesの中から自分のOSをクリック
ページ内のDownload the installerをクリック(今回はWindowsですが他も同じ手順です。)
自分のOSの使いたいバージョンのDownloadをクリック
(今回はWindowsx86-64の12.1バージョンをダウンロードしました。)2-2.PostgreSQLのインストールと環境変数設定(Windows10)
ダウンロードしたpostgresql-(バージョン数)-(OS名)アプリケーションを実行し
このような画面が出たら基本的にNext>を押し続けてください。
passwordだけは自分で設定しますが、portは基本的に干渉しなければ変えなくてよいと思いますインストールが完了したらシステム環境変数を設定します。
PostgreSQLをインストールした場所の中のbinフォルダがある場所
(変更していなければ、C:\Program Files\PostgreSQL(バージョン数))
にPathを通します。Windows10の場合、スタートメニューを右クリック>設定で以下の画面が出るので
「システム環境」と入力すると「システム環境変数」が出てきます。下のような画面が出てくるので環境変数をクリック
システム環境変数のPathをクリックして編集>前のパスの間に;(セミコロン)を
はさんでPostgreSQLをインストールした場所の中のbinフォルダがある場所のパスを入力ここまで終わったらコマンドプロンプトを起動し、
psql --varsionと入力してPostgreSQLのバージョン数が出ればインストール完了です。
3.echoのインストール
下のコマンドで一発でインストールできます。
go get -u github.com/labstack/echo...4.SQLのデータをc.Stringでreturnするプログラム
4-1.概要
PostgreSQLの表示パターンを2個作っていきます。
- sql.GetPost()
URLのプレースホルダからidをもらってきて
idが一致するレコードを表示する。
- sql.GetPosts()
postsテーブルのレコードをすべて表示する。
4-2.データベースとテーブル作成
コマンドプロンプトでPostgreSQLにログインする
下のコマンドを打ってパスワードを入力psql -U postgres今回はsampleという名前のDBを作るので
下のコマンドを入力CREATE DATABASE sample先ほど作ったデータベースに接続するコマンドを入力
¥c samplesampleデータベースにpostsテーブルを作る
create table posts ( id integer, content varchar(80), content author(80) );postsテーブルにレコード挿入する(お好きなだけどうぞ)
insert into posts values (1, 'test', 'test1user'); insert into posts values (2, 'testtest', 'test2user'); insert into posts values (3, 'testtesttest', 'test3user');以上でPostgreSQLの操作は完了です。
4-3.フォルダ構成
作業フォルダ--- main.go ∟ sql --- sql.go4-4.main.go
package main import ( "github.com/labstack/echo" "github.com/labstack/echo/middleware" "github.com/~/sql" //SQLについて記述したパッケージのパス ) func main() { // Echoのインスタンス e := echo.New() // ミドルウェア類 e.Use(middleware.Logger()) e.Use(middleware.Recover()) e.Use(interceptor.BasicAuth()) // ルーティング e.GET("/sql/record/:id", sql.GetPost()) //プレースホルダでidをもらってくる e.GET("/sql/table", sql.GetPosts()) // サーバー起動 e.Start(":1323") }【ポイント】
* 基本的にmain.goにはルーティングをだけをしています。4-5.sql.go
package sql import ( "database/sql" "net/http" "github.com/labstack/echo" "github.com/pkg/errors" _ "github.com/lib/pq" "strconv" ) type Post struct { Id int Content string Author string } var content string var Db *sql.DB func init() { var err error Db, err = sql.Open("postgres", "user=postgres dbname=sample password=自分で設定したパスワード sslmode=disable") if err != nil { panic(err) } } func GetPost() echo.HandlerFunc { return func(c echo.Context) error { id := c.Param("id") //プレースホルダのidを取得 post := Post{} if err := Db.QueryRow("select id, content, author from posts where id = $1", id).Scan(&post.Id, &post.Content, &post.Author); err != nil { return errors.Wrapf(err, "connot connect SQL") } return c.String(http.StatusOK, " ID : " + strconv.Itoa(post.Id) + " CONTENT : " + post.Content + " AUTHOR : " + post.Author) // string(Post.Id)だと何故かファイルダウンロードしてしまい、IDも表示されない。 // strconv.Itoa(post.Id) の部分はプレースホルダから取り出したidでも可 } } func GetPosts() echo.HandlerFunc { return func(c echo.Context) error { post := Post{} response := "" rows, err := Db.Query("select id, content, author from posts") if err != nil { return errors.Wrapf(err, "connot connect SQL") } defer rows.Close() for rows.Next(){ if err := rows.Scan(&post.Id, &post.Content, &post.Author); err != nil { return errors.Wrapf(err, "connot connect SQL") } response += "ID : " + strconv.Itoa(post.Id) + " CONTENT : " + post.Content + " AUTHOR : " + post.Author + "\n" } return c.String(http.StatusOK, str) } }【ポイント】
* Db.QueryRowは1つのレコードを取得する関数でDb.Queryは複数のレコードを取得する関数
* post.Idに関してはint型なのでstring(Post.Id)にキャストしようと思うと思いますが
それだと変な挙動をしてしまうので、strconvをインポートしてstrconv.Itoa(post.Id)とすることできちんと動きます。
* GetPosts()のほうは何回もreturnすることができないのでresponseという文字列にすべての結果を格納して表示しています。4-6.実行画面
5.SQLのデータをc.JSONでreturnするプログラム(12.12 追記)
記事を投稿した後にAPIサーバで使うならc.JSONで返すプログラムでなければならないな…
と思いc.JSONでreturnするプログラムに作り変えてみました。main.goは変更していないので4-4をご覧ください。
5-1.sql.go
package sql import ( "database/sql" "net/http" "github.com/labstack/echo" "github.com/pkg/errors" _ "github.com/lib/pq" ) type Post struct { Id int `json:"id"` Content string `json:"content"` Author string `json:"author"` } var content string var Db *sql.DB func init() { var err error Db, err = sql.Open("postgres", "user=postgres dbname=sample password=自分で設定したパスワード sslmode=disable") if err != nil { panic(err) } } func GetPost() echo.HandlerFunc { return func(c echo.Context) error { id := c.Param("id") post := Post{} posts := []*Post{} if err := Db.QueryRow("select id, content, author from posts where id = $1", id).Scan(&post.Id, &post.Content, &post.Author); err != nil { return errors.Wrapf(err, "connot connect SQL") } posts = append(posts, &Post{Id: post.Id, Content: post.Content, Author: post.Author}) return c.JSON(http.StatusOK, posts) } } func GetPosts() echo.HandlerFunc { return func(c echo.Context) error { post := Post{} posts := []*Post{} rows, err := Db.Query("select id, content, author from posts") if err != nil { return errors.Wrapf(err, "connot connect SQL") } defer rows.Close() for rows.Next(){ if err := rows.Scan(&post.Id, &post.Content, &post.Author); err != nil { return errors.Wrapf(err, "connot connect SQL") } posts = append(posts, &Post{Id: post.Id, Content: post.Content, Author: post.Author}) } return c.JSON(http.StatusOK, posts) } }【ポイント】
* structにjson:"○○"
を追加する
* postsにデータをappendしてからreturn5-2.実行例
6.終わりに
GoもechoもPostgreSQLも初心者なのでざっとした流れとSQLのデータ表示についてまとめてみました。
まとめてくれているサイトなどがかなり少なかったので同じようなことに迷っている人の
助けになれば幸いです。何か間違っているところや改善したほうがいいところなどありましたら教えていただけると幸いです。
7.参考文献
- 投稿日:2019-12-11T18:15:20+09:00
Goでの画像生成
この記事はtomowarkar ひとりAdvent Calendar 2019の11日目の記事です。
記事を書く時間が取れず、またストックもないのでどんどんと簡素化していくのがわかりますね笑
今回はGoで640×400のpng画像を生成をしていきます。
ランダム生成
int Slice
で描画するフィールドを生成(ランダム)- 1.を元に
png
ファイル生成- ローカル保存
をしていきます。
コード
package main import ( "image" "image/color" "image/png" "math/rand" "os" ) var ( khaki = color.RGBA{240, 230, 140, 255} darkgoldenrod = color.RGBA{184, 134, 11, 255} royalblue = color.RGBA{65, 105, 225, 255} seagreen = color.RGBA{46, 139, 87, 255} width = 640 height = 400 ) func setField() []int { field := make([]int, width*height) for i := 0; i < len(field); i++ { field[i] = rand.Intn(4) } return field } func main() { field := setField() img := image.NewRGBA(image.Rect(0, 0, width, height)) for x := 0; x < width; x++ { for y := 0; y < height; y++ { switch field[y*height+x] { case 1: img.Set(x, y, khaki) case 2: img.Set(x, y, royalblue) case 3: img.Set(x, y, seagreen) default: img.Set(x, y, darkgoldenrod) } } } f, _ := os.Create("./img/image.png") defer f.Close() png.Encode(f, img) }結果
ランダム生成(倍率指定)
これでは少し描画粒度が細かいので、倍率を指定して描画粒度を荒くしていきます。
差分のみの表示なので詳しく知りたい方はソースコードまでどうぞ。
コード
- width = 640 - height = 400 + width = 16 + height = 10 + scale = 40 - img := image.NewRGBA(image.Rect(0, 0, width, height)) - for x := 0; x < width; x++ { - for y := 0; y < height; y++ { - switch field[y*height+x] { + img := image.NewRGBA(image.Rect(0, 0, width*scale, height*scale)) + for x := 0; x < width*scale; x++ { + for y := 0; y < height*scale; y++ { + switch field[y/scale*height+x/scale] {結果
おまけ
今回紹介したコードを走らせても、同じ画像しか生成されないと思います。
これはフィールド生成の乱数の初期シード値が固定されているためです。なので以下のように
setField
の関数内で時刻をもとにシード値を設定してやります。
これで毎回違ったフィールドが生成されます。rand.Seed(time.Now().UnixNano())画像生成時に指定するファイルパスも固定化してしまうと上書きで更新されてしまうので、タイムスタンプを入れることで上書きを防ぎ新たなファイルを作ることができます。
t := time.Now() timestamp := fmt.Sprintf("%04d%02d%02d%02d%02d%02d", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), ) fpath := fmt.Sprintf("./img/image%s.png", timestamp)どちらも詳細はソースコードで!
おわりに
以上明日も頑張ります!!
tomowarkar ひとりAdvent Calendar Advent Calendar 2019参考
画像生成周り
https://yourbasic.org/golang/create-image/
https://golang.org/pkg/image/Goでの乱数
https://qiita.com/makiuchi-d/items/9c4af327bc8502cdcdce
時間のフォーマット
- 投稿日:2019-12-11T17:48:13+09:00
Go で テストカバレッジを見える化の巻
この記事はAteam Hikkoshi Samurai Inc. & Ateam Connect Inc.(エイチーム引越し侍、エイチームコネクト) Advent Calendar 2019 12日目の記事になります。
はじめに
どーも、まいどおおきにおはこんにちこんばんは。
現在、エイチーム引越し侍では、2019年6月より某サイトのリプレイスに取り掛かっております。
リプレイスにあたりバックエンドの技術選定に関しては開発部全員で話し合いました。
案として挙がってきたのは、【PHPでLaravel】
、【RubyでRuby on Rails】
、【GoでClean Architecture】
あたりです。
そもそも、既存のサイトは【PHPのsymfony】
で作成されており、移行のことを考えるとPHPは有力とされておりましたが最終的な投票の結果、なんと【GoでClean Architecture】
になりました。
しかも次点は、【Ruby on Rails】
という、、、PHPには、苦い思い出があったのでしょうか。(想像はできますが)その選定方法や、選定基準は今回割愛しますが、いずれ発表したいと思います。
さて、その
【GoでClean Architecture】
に決定になったもののGO言語
の有識者が、当時一人しかいない中での選択で、私自身「びつくりした」というほかありません。
私も趣味程度しか触ったことがなく、GO言語
を実際に使う機会があるなんて、夢にも思いませんでした。
ただ、そのたった一人のGO言語
の有識者は2ヶ月ほどでリプレイスメンバーから離れてしまい、内心「大丈夫なのか!?」と思った次第です。しかーーーーし、そんなことはなんのその、クリーンアーキテクチャの思想を取り入み、TDDを駆使しながらメンバーは着実に力をメキメキとつけていき、わたしは置いてけぼりをくらっております。
しかもしかもしかも、そのリプレイスによって「テストカバレッジ80%を達成する」という目標を抱いておりますので、なんとしても達成していきたいと思っています。そんなメンバーに置いてけぼりをくらわない&少しでも力になれるように、ここに備忘録として「
Go
でテストカバレッジ見える化」についてを記載します。[参考] Go 1.13
バックエンドはAPIサーバーとして想定しているので、そのテストパッケージもついでにご紹介。
net/http/httptest パッケージ
net/http/httptest
はHTTPに関するテストをする際に便利なパッケージです。
net/http/httptest
パッケージを使用すると、特定のアドレスとポートをListen
させることなくWEBアプリケーションのテストができます。簡単なAPIサーバーの例
/contact?name=gopher
と問い合わせすると、Hello, gopher
と返ってくるアプリケーションです。これをテストする場合、
go run
して立ち上げ、curl http://localhost:8080/contact?name=gopher
を実行したり、postman で叩いてテストすることも可能ですが、TCPポート:8080
がすでに利用されていたり、毎回毎回postmanで叩くのはめんどうですよね。そんな場合は、
net/http/httptest
を使うと便利です。
ローカルのループバックデバイスで動作するテスト用のサーバを立てることができます。API サーバの例
// server.go package main import ( "fmt" "log" "net/http" ) // Route はこのAPIサーバのルーティング設定をしている func Route() *http.ServeMux { m := http.NewServeMux() m.HandleFunc("/contact", func(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) } fmt.Fprintf(w, "Hello, %s", r.FormValue("name")) }) return m } func main() { m := Route() log.Fatal(http.ListenAndServe(":8080", m)) }
httptest.Server
が便利なのは、http.Handler
インターフェイスを引数に渡せることです。
今回だと、func Route
が*http.ServeMux
を返すようにしました。
*http.ServeMux
はhttp.handler
のインターフェイスを満たしているため、そのままhttptest.NewServer
の引数として渡すことができます。テスト用サーバーを利用したテスト
package main import ( "io/ioutil" "net/http" "net/http/httptest" "testing" ) func TestRoute(t *testing.T) { // テスト用サーバーの立ち上げ ts := httptest.NewServer(Route()) defer ts.Close() // テスト用サーバーのURLは、ts.URL で取得できる res, err := http.Get(ts.URL + "/contact?name=gopher") if err != nil { t.Fatalf("http.Get faild: %s", err) } contact, err := ioutil.ReadAll(res.Body) res.Body.Close() if err != nil { t.Fatalf("read from HTTP Response Body failed: %s", err) } expected := "Hello, gopher" if string(contact) != expected { t.Fatalf("response of /contact?name=gopher returns %s, want %s", string(contact), expected) } }
httptest.Server
はGo
のインターフェイスをうまく利用した使いやすい仕組みであり、テストのための便利なツールセットとなっています。テストカバレッジ
Go
におけるテストカバレッジ計測ツールについてご紹介。
テストカバレッジ見える化をするためには、まず大元であるテストカバレッジ計測ツールや方法が必要ですよね。カバレッジ取得方法
go test -coverこの際、カバレッジを測定するだけでなく、テストも同時に実行されます。
より詳しい結果の閲覧方法
「go test」で作成されたカバレッジプロファイル
go test -coverprofile=c.out注釈付きソースコードを表示するWebブラウザーで確認する場合
go tool cover -html=c.outWebブラウザーを起動する代わりにHTMLファイルを書き出し
go tool cover -html=c.out -o coverage.html※Webブラウザーと同じ
各機能のカバレッジ率を標準出力に表示
go tool cover -func=c.out最後にカバレッジ注釈付きの変更されたソースコードを生成するには
go tool cover -mode=set -var=CoverageVariableName main.goテストカバレッジを得ることで、自身のライブラリのテスト計画を練るだけでなく、そのライブラリのテストの指針についても知ることができます。
よくテストされている箇所とテストのあまりされていない箇所を知ることは、テストの傾向を知ることになり、それは実装の指針を知ることにもなります。
テストのカバレッジによってテストの傾向を知り、アプリケーションで起き得るリスクを察知するために、活用できるかと。テストカバレッジ見える化&CI
やっぱり、チームで運用・保守をやるなら、数字や見える化があった方がモチベーションもあがると思いますので、codecovを使って、カバレッジの見える化も対応したいですね。今回は、travis-ciで試してみました。
やりかたは、codecovを参考に。# .travis.yml language: go go: - 1.13.x - tip before_install: - go get -t -v ./... script: - go test -race -coverprofile=coverage.txt -covermode=atomic after_success: - bash <(curl -s https://codecov.io/bash)緑がテストを回している箇所で、赤の部分がテストコードを書いていない部分です。
このように、図や、テスト箇所がすぐに見れるようになると、とてもわかりやすいです。https://travis-ci.org/andmorefine/go-coveralls
https://codecov.io/gh/andmorefine/go-coveralls
ソースコードきちんと、テストカバレッジを見える化することができました。
カバレッジをうまく使って、ガシガシ、テストの戦略を練っていきましょう!!!!
[参考文献]
golang.org/pkg/testing
golang.org/pkg/net/http
golang.org/cmd/go
Go でコードカバレッジを取得する
Golangのtestify/assert 使えそうな関数まとめ
godoc.org/github.com/stretchr/testify/assert
codecov/example-goお知らせ
エイチームグループでは一緒に活躍してくれる優秀な人材を募集中です。
興味のある方はぜひともエイチームグループ採用ページよりご応募ください!
(Webエンジニア詳細ページ)よりお問い合わせ下さい。明日
明日のエイチーム引越し侍、エイチーム コネクトアドベントカレンダーの担当は、@futa_326さんです!
是非、お楽しみに!
ほなまた!
- 投稿日:2019-12-11T13:53:44+09:00
Goのtestingパッケージの基本を理解する
テックタッチアドベントカレンダー11日目を担当する @taisa831 です。10日目は @mxxxxkxxxx の「Go言語 ElastiCacheの その前に」でした。575 の 5 が Go に掛かっていていい感じですね!もちろん内容も良い!
本記事では、
Go
のtesting
パッケージについて書きます。既存記事を調べてみると、そこそこあるけどそこまで多くはない。重厚な記事もあればあっさりした記事もある。ということで深すぎず浅すぎずを目指そうと思います。「testing パッケージの基本を理解する」なので
Go
の testing パッケージ を参考にしました。テストに関しては最初アサーションがないことに戸惑いましたが、慣れたらない方がよく感じてきました。執筆時点でのバージョンはgo1.13.4
です。
- もっともシンプルなテスト
- Benchmark を取る
- Example テスト
- カバレッジをとる
- テストをスキップする
- サブテストとサブベンチマーク
- 共通処理のエラー箇所を分かりやすく出力する
- 前処理/後処理をする
- その他
- おわりに
もっともシンプルなテスト
Go
でテストを実行するにはいくつかルールがあります。
- ファイル名は
**_test.go
とする- 関数は
TestCamelCase
のようにTest
ではじめ後ろはCamelCase
とするtesting
パッケージを引数で受ける- テストファイルはテスト対象ファイルと同一パッケージとする(※テストだけ例外的に
packagename_test
とすることも可能)そして下記のように期待値と実行時の値が違う場合は
t.Errorf
でエラーを記録します(後述しますが必ずしもt.Errorf
である必要はありません)。func TestAbs(t *testing.T) { got := math.Abs(-1) if got != 1 { t.Errorf("Abs(-1) = %f; want 1", got) } }実行$ go test ok command-line-arguments 4.018s
-v
オプションをつけることでより詳細な情報が見られます。実行go test -v === RUN TestAbs --- PASS: TestAbs (0.00s) PASS ok command-line-arguments 0.005sパッケージ/ファイル単位でテスト対象を指定する
パッケージ単位でテスト対象を指定するには以下のようにコマンドを使い分けます。
# カレントディレクトリ $ go test # カレント配下全て $ go test ./... # testing パッケージ配下 $ go test testing # testing/quick パッケージ配下 $ go test testing/quick # testing 配下全て $ go test testing/... # 同じパッケージがテストが対象の場合 $ go test testing_testing.go testing.go # packgename_test のようにパッケージを分けている場合 $ go test testing_testing.go
Benchmark
を取る
testing
パッケージを使ってBenchmark
を取ることもできます。Benchmark
を取るには*testing.B
を使い、-bench
オプションを指定して実行します。func BenchmarkHello(b *testing.B) { for i := 0; i < b.N; i++ { fmt.Sprintf("hello") } }以下の実行時のベンチマークは
21,764,674 times at a speed of 54.1 ns per loop
となります。実行$ go test -bench=. goos: darwin goarch: amd64 pkg: github.com/taisa831/sandbox-go/testing BenchmarkHello-8 21764674 54.1 ns/op PASS ok github.com/taisa831/sandbox-go/testing 1.241s一部の処理だけ
Benchmark
を取る
Benchmark
にはStartTimer()
、StopTime()
、ResetTimer()
が用意されています。これらを利用して一部の処理だけピンポイントでベンチマークをとることもできます。func BenchmarkBig(b *testing.B) { // 重い処理 Big() // リセット b.ResetTimer() // ここから計測が始まる for i := 0; i < b.N; i++ { fmt.Sprintf("hello") } }重い処理は無視して先ほどと同じような結果となるベンチマークが取れました。
実行go test -bench=. goos: darwin goarch: amd64 pkg: github.com/taisa831/sandbox-go/testing BenchmarkBig-8 22457809 54.2 ns/op PASS ok github.com/taisa831/sandbox-go/testing 14.274s
Example
テスト
testing
パッケージには少し特殊なものとして出力をテストするExample
テスト機能があります。Example
テストは**_test.go
内で実行することができます。出力をチェックするにはOutput:
やUnordered output:
を利用します。Output:
Output: hello
と書くことでテストができます。例えばOutput: helle
に変えるとテストでエラーとなります。func ExampleHello() { fmt.Println("hello") // Output: hello }実行go test -v === RUN ExampleHello --- PASS: ExampleHello (0.00s) PASS ok github.com/taisa831/sandbox-go/testing 0.005s複数行出力した結果のテストも可能です。
func ExampleSalutations() { fmt.Println("hello, and") fmt.Println("goodbye") // Output: // hello, and // goodbye }実行go test -v === RUN ExampleSalutations --- PASS: ExampleSalutations (0.00s) PASS ok github.com/taisa831/sandbox-go/testing 0.005sUnorderd output:
Unordered output
はその名の通りオーダーを無視して出力を検証してくれます。func ExamplePerm() { for _, value := range rand.Perm(5) { fmt.Println(value) } // Unordered output: 4 // 2 // 1 // 3 // 0 }実行go test -v === RUN ExamplePerm --- PASS: ExamplePerm (0.00s) PASS ok github.com/taisa831/sandbox-go/testing 0.005s
Example
テストはGoDoc
にも利用でき、下記のような命名規則があります。
- func Example() { ... }
GoDoc
のPackage
に出る- func ExampleF() { ... }
GoDoc
のFunction
に出る- func ExampleT() { ... }
GoDoc
のType
に出る- func ExampleT_M() { ... }
GoDoc
のType
のMethod
に出る更に上記の単位で細かく
Example
テストを分けて書きたい場合は_suffix
のように小文字で記述します。
- func Example_suffix() { ... }
- func ExampleF_suffix() { ... }
- func ExampleT_suffix() { ... }
- func ExampleT_M_suffix() { ... }
GoDoc
にExample
を出力してみる実際にどのように
GoDoc
に出るかイメージしにくいのでサンプルを作成して確認してみます。ここまではテストファイルしかなかったので、別途サンプル実装とテストを書いて実行してみました。person.go// Package person package person type Person struct { firstName string lastName string } // NewPerson func NewPerson(firstName string, LastName string) *Person { return &Person{firstName: firstName, lastName: LastName} } // GetFirstName func (p *Person) GetFirstName() string { return p.firstName } // GetLastName func (p *Person) GetLastName() string { return p.lastName }person_test.gofunc Example() { person := NewPerson("Taro", "Yamada") fmt.Println(person.GetFirstName()) // output: Taro } func ExampleNewPerson() { person := NewPerson("Taro", "Yamada") fmt.Println(person.GetFirstName()) // output: Taro } func ExamplePerson_GetFirstName() { person := NewPerson("Taro", "Yamada") fmt.Println(person.GetFirstName()) // output: Taro } func ExamplePerson_GetLastName() { person := NewPerson("Taro", "Yamada") fmt.Println(person.GetLastName()) // output: Yamada }上記サンプルコードを
GoDoc
に出力してみます。GoDoc
が入っていない場合はgo get
してからコマンドを実行しlocalhost:6060
にアクセスします。Packages
にアクセスすると自分のパッケージ情報が見られます。$ go get golang.org/x/tools/cmd/godoc $ godoc -http=:6060 # localhost:6060にアクセス
Example()
はPackage
に表示される
ExampleNewPerson()
はType
に表示される
ExamplePerson_GetFirstName()
とExamplePerson_GetLastName
はfunc
に表示されるカバレッジをとる
上記サンプルにテストを追加しカバレッジをとってみます。カバレッジは
-cover
オプションで簡単にみることができます。ちなみにExample
テストだけでもカバレッジが出るようです。実行go test -v -cover === RUN Example --- PASS: Example (0.00s) === RUN ExampleNewPerson --- PASS: ExampleNewPerson (0.00s) === RUN ExamplePerson_GetFirstName --- PASS: ExamplePerson_GetFirstName (0.00s) === RUN ExamplePerson_GetLastName --- PASS: ExamplePerson_GetLastName (0.00s) PASS coverage: 100.0% of statements ok github.com/taisa831/sandbox-go-impl/person 0.005s
Example
テストだけだと中途半端感があるので通常のテストに変えてみます。好みもあると思いますがGoland
やVSCode
の機能を使うと簡単にテストコードの枠組みが出力できるのでかなり楽にテストが作成できます。func TestNewPerson(t *testing.T) { type args struct { firstName string LastName string } tests := []struct { name string args args want *Person }{ { "NewPerson", args{"Taro", "Yamada"}, &Person{firstName: "Taro", lastName: "Yamada"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := NewPerson(tt.args.firstName, tt.args.LastName); !reflect.DeepEqual(got, tt.want) { t.Errorf("NewPerson() = %v, want %v", got, tt.want) } }) } } func TestPerson_GetFirstName(t *testing.T) { type fields struct { FirstName string LastName string } tests := []struct { name string fields fields want string }{ { "GetFirstName", fields{FirstName:"Taro", LastName:"Yamada"}, "Taro", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { p := &Person{ firstName: tt.fields.FirstName, lastName: tt.fields.LastName, } if got := p.GetFirstName(); got != tt.want { t.Errorf("GetFirstName() = %v, want %v", got, tt.want) } }) } } func TestPerson_GetLastName(t *testing.T) { type fields struct { FirstName string LastName string } tests := []struct { name string fields fields want string }{ { "GetLastName", fields{FirstName:"Taro", LastName:"Yamada"}, "Yamada", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { p := &Person{ firstName: tt.fields.FirstName, lastName: tt.fields.LastName, } if got := p.GetLastName(); got != tt.want { t.Errorf("GetLastName() = %v, want %v", got, tt.want) } }) } }同じようにカバレッジが出せました。
実行go test -v -cover === RUN TestNewPerson === RUN TestNewPerson/NewPerson --- PASS: TestNewPerson (0.00s) --- PASS: TestNewPerson/NewPerson (0.00s) === RUN TestPerson_GetFirstName === RUN TestPerson_GetFirstName/GetFirstName --- PASS: TestPerson_GetFirstName (0.00s) --- PASS: TestPerson_GetFirstName/GetFirstName (0.00s) === RUN TestPerson_GetLastName === RUN TestPerson_GetLastName/GetLastName --- PASS: TestPerson_GetLastName (0.00s) --- PASS: TestPerson_GetLastName/GetLastName (0.00s) PASS coverage: 100.0% of statements ok github.com/taisa831/sandbox-go-impl/person 0.006sどこのコードをテストが通っているかを確認したい場合には
html
に変換して確認することももちろんできます。実行$ go test -coverprofile=cover.out PASS coverage: 100.0% of statements ok github.com/taisa831/sandbox-go-impl/person 0.006s $ go tool cover -html=cover.out -o cover.html $ open cover.htmlHTML出力結果
テストをスキップする
下記の例では
--short
オプションと組み合わせて使っていますが、実行するとtesting.Short()
がtrue
となりスキップされます。ショートモードではこのテストは実行不要だという時などに使います。func TestTimeConsuming(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode.") } }実行go test --short -v === RUN TestTimeConsuming --- SKIP: TestTimeConsuming (0.00s) testing_test.go:78: skipping test in short mode. PASS ok github.com/taisa831/sandbox-go/testing 0.005s似た用途のメソッドは
tesing
パッケージのTB
インターフェースに定義されているのであげておきます。基本的にはError
やErrorf
を使うことが多いと思います。
- Error(args ...interface{})
- ログを出力してエラーをマークする
- Errorf(format string, args ...interface{})
- フォーマットログを出力してエラーをマークする
- Fail()
- テストにエラーをマークする
- FailNow()
- テストにエラーをマークしてそのテストを終了する
- Failed() bool
- テストがエラーかをチェックする
- Fatal(args ...interface{})
- エラーを出力してそのテストを終了する
- Fatalf(format string, args ...interface{})
- フォーマットエラーを出力してそのテストを終了する
- Log(args ...interface{})
- ログを出力する
- Logf(format string, args ...interface{})
- フォーマットログを出力する
- Skip(args ...interface{})
- ログを出力してスキップをマークしてそのテストを終了する
- SkipNow()
- スキップをマークしてそのテストを終了する
- Skipf(format string, args ...interface{})
- フォーマットログを出力してスキップをマークしてそのテストを終了する
- Skipped() bool
- テストがスキップかをチェックする
サブテストとサブベンチマーク
1つのテスト関数にサブテストを追加して複数のテストを実行することができます。ベンチマークも同様に可能です。サブテストがすべて終わるまで
t.Logf("%s", "finished")
は呼ばれないのでteardown
処理を入れることも可能です。func TestFoo(t *testing.T) { t.Run("A=1", func(t *testing.T) { got := math.Abs(-1) if got != 1 { t.Errorf("Abs(-1) = %f; want 1", got) } }) t.Run("A=2", func(t *testing.T) { got := math.Abs(-1) if got != 1 { t.Errorf("Abs(-1) = %f; want 1", got) } }) t.Run("B=1", func(t *testing.T) { got := math.Abs(-1) if got != 1 { t.Errorf("Abs(-1) = %f; want 1", got) } }) t.Logf("%s", "finished") // teardown }実行go test -v --run Foo === RUN TestFoo === RUN TestFoo/A=1 === RUN TestFoo/A=2 === RUN TestFoo/B=1 --- PASS: TestFoo (0.00s) --- PASS: TestFoo/A=1 (0.00s) --- PASS: TestFoo/A=2 (0.00s) --- PASS: TestFoo/B=1 (0.00s) testing_test.go:114: finished PASS ok github.com/taisa831/sandbox-go/testing 0.005sサブテストをパラレルに実行する
サブテスト内で
t.Parallel()
を呼び出すことでサブテストをパラレルに実行することができます。パラレル動作を確認する為にA-1
テストにだけSleep
を入れて実行してみます。func TestGroupedParallel(t *testing.T) { tests := []struct { Name string Want bool WantErr bool }{ { Name: "test1", Want: true, WantErr: false, }, } for _, tc := range tests { tc := tc // capture range variable t.Run(tc.Name, func(t *testing.T) { t.Run("A=1", func(t *testing.T) { t.Parallel() got := math.Abs(-1) time.Sleep(2 * time.Second) if got != 1 { t.Errorf("Abs(-1) = %f; want 1", got) } t.Logf("Len=2: %s", time.Now()) }) t.Run("A=2", func(t *testing.T) { t.Parallel() got := math.Abs(-1) if got != 1 { t.Errorf("Abs(-1) = %f; want 1", got) } t.Logf("Len=2: %s", time.Now()) }) t.Run("B=1", func(t *testing.T) { t.Parallel() got := math.Abs(-1) if got != 1 { t.Errorf("Abs(-1) = %f; want 1", got) } t.Logf("Len=2: %s", time.Now()) }) }) } t.Logf("%s", "finished") // teardown }実行結果を見てみると
A-1
のテストが最後まで残りfinished
が呼ばれているのが分かります。パラレルテストでも全てのテストが終わるまでfinished
が呼ばれないのでteardown
処理をすることができます。実行go test -v === RUN TestGroupedParallel === RUN TestGroupedParallel/test1 === RUN TestGroupedParallel/test1/A=1 === PAUSE TestGroupedParallel/test1/A=1 === RUN TestGroupedParallel/test1/A=2 === PAUSE TestGroupedParallel/test1/A=2 === RUN TestGroupedParallel/test1/B=1 === PAUSE TestGroupedParallel/test1/B=1 === CONT TestGroupedParallel/test1/A=1 === CONT TestGroupedParallel/test1/B=1 === CONT TestGroupedParallel/test1/A=2 --- PASS: TestGroupedParallel (2.00s) --- PASS: TestGroupedParallel/test1 (0.00s) --- PASS: TestGroupedParallel/test1/B=1 (0.00s) testing_test.go:149: Len=2: 2019-12-10 19:57:05.678677 +0900 JST m=+0.000551289 --- PASS: TestGroupedParallel/test1/A=2 (0.00s) testing_test.go:141: Len=2: 2019-12-10 19:57:05.678767 +0900 JST m=+0.000641739 --- PASS: TestGroupedParallel/test1/A=1 (2.00s) testing_test.go:133: Len=2: 2019-12-10 19:57:07.681258 +0900 JST m=+2.003074682 testing_test.go:153: finished PASS ok github.com/taisa831/sandbox-go/testing 2.009s
-run
オプションでテスト対象を絞る
-run
オプションを指定することでテストファイル内のテスト対象を絞ることができます。# 全テスト対象 go test -run '' # Fooにマッチするテストが対象 go test -run Foo # FooにマッチかつサブテストA=にマッチするテストが対象 go test -run Foo/A= # サブテストがA=1にマッチするテストが対象 go test -run /A=1下記を実行すると
Foo/A=
にマッチするテストだけ実行されてるのが分かります。実行go test -v -run Foo/A= === RUN TestFoo === RUN TestFoo/A=1 === RUN TestFoo/A=2 --- PASS: TestFoo (0.00s) --- PASS: TestFoo/A=1 (0.00s) --- PASS: TestFoo/A=2 (0.00s) PASS ok github.com/taisa831/sandbox-go/testing 0.005s共通処理のエラー箇所を分かりやすく出力する
ちょっとしたことですが、テストで共通処理を呼び出している時、どのテストでエラーが出たかは共通処理のコード情報しか出力されないので通常だと分かりません。そういう時は
helper()
を使うと呼び出し元のテスト情報を出力してくれます。必ずエラーとなる
PreTest
関数を複数のテスト関数で呼び出して実行して確認してみます。また,T
とB
はtesting
のTB
のインターフェースを実装(正確にはcommon
が実装してそれを両方とも持っている)しているので*testing.T
も*testing.B
も渡すことができます。func PreTest(tb testing.TB) { got := math.Abs(-1) if got != -1 { // t.Helper() tb.Errorf("Abs(-1) = %f; want 1", got) } }通常だと
PreTest
のエラー箇所である行情報しかでませんが、t.Helper()
のコメントアウトを外してもう一回実行してみます。実行go test --- FAIL: TestAbs (0.00s) testing_test.go:155: Abs(-1) = 1.000000; want 1 --- FAIL: TestFoo (0.00s) testing_test.go:155: Abs(-1) = 1.000000; want 1 testing_test.go:97: finished呼び出し元のテスト側の行情報がでるようになりました。
実行go test --- FAIL: TestAbs (0.00s) testing_test.go:12: Abs(-1) = 1.000000; want 1 --- FAIL: TestFoo (0.00s) testing_test.go:76: Abs(-1) = 1.000000; want 1 testing_test.go:97: finished FAIL exit status 1 FAIL github.com/taisa831/sandbox-go/testing 2.009s前処理/後処理をする
*testing.M
を利用すると、対象のテストに対して「前処理」や「後処理」を書くことができます。テスト対象ファイルに下記のように記述することで実行が可能です。また、上記の--run
オプションで対象テストを絞っても「前処理」と」後処理」は実行されます。func TestMain(m *testing.M) { println("前処理") m.Run() println("後処理") }テストの最初と最後に「前処理」と「後処理」がプリントされました。
実行go test -v 前処理 === RUN TestAbs --- PASS: TestAbs (0.00s) === RUN TestAbs2 --- PASS: TestAbs2 (0.00s) === RUN TestTimeConsuming --- PASS: TestTimeConsuming (0.00s) === RUN TestFoo === RUN TestFoo/A=1 === RUN TestFoo/A=2 === RUN TestFoo/B=1 --- PASS: TestFoo (0.00s) --- PASS: TestFoo/A=1 (0.00s) --- PASS: TestFoo/A=2 (0.00s) --- PASS: TestFoo/B=1 (0.00s) === RUN TestGroupedParallel === RUN TestGroupedParallel/test1 === RUN TestGroupedParallel/test1/A=1 === PAUSE TestGroupedParallel/test1/A=1 === RUN TestGroupedParallel/test1/A=2 === PAUSE TestGroupedParallel/test1/A=2 === RUN TestGroupedParallel/test1/B=1 === PAUSE TestGroupedParallel/test1/B=1 === CONT TestGroupedParallel/test1/A=1 === CONT TestGroupedParallel/test1/B=1 === CONT TestGroupedParallel/test1/A=2 --- PASS: TestGroupedParallel (2.00s) --- PASS: TestGroupedParallel/test1 (0.00s) --- PASS: TestGroupedParallel/test1/B=1 (0.00s) testing_test.go:160: Len=2: 2019-12-10 21:48:38.890123 +0900 JST m=+0.000768137 --- PASS: TestGroupedParallel/test1/A=2 (0.00s) testing_test.go:152: Len=2: 2019-12-10 21:48:38.890183 +0900 JST m=+0.000827182 --- PASS: TestGroupedParallel/test1/A=1 (2.00s) testing_test.go:144: Len=2: 2019-12-10 21:48:40.89206 +0900 JST m=+2.002648644 testing_test.go:164: finished === RUN ExampleHello --- PASS: ExampleHello (0.00s) === RUN ExampleSalutations --- PASS: ExampleSalutations (0.00s) === RUN ExamplePerm --- PASS: ExamplePerm (0.00s) PASS 後処理 ok github.com/taisa831/sandbox-go/testing (cached)その他
testing
パッケージには他にもio
テストをしやすくする為のiotest
パッケージや、ブラックボックステスト用であるquick
パッケージ(ただし凍結されている模様)があります。おわりに
Go
は標準でモックライブラリも用意しているので組み合わせれば標準でほとんどのテストができます。またhttptest
もあるのでe2e
テストもできます。これらをうまく使いこなして楽しい開発ライフを送りたいと思います。余談ですが、本記事を書くにあたり
GoDoc
をいろいろみていたら気になる箇所があったのでGo
本体にコミット&レビュー依頼してみたところ、何度かやりとりした後、無事取り込んでくれました。
GitHub
ではなくGerrit
を使っているので最初のセットアップは少し面倒ですが、一度セットアップすると以降は簡単にレビュー依頼をすることができます。下記の本家記事と日本語の記事がとても分かりやすいので是非参考にしてみてください。参考
明日のテックタッチアドベントカレンダーの担当は @takakobem です!
- 投稿日:2019-12-11T13:01:29+09:00
chromedp(golang)で、Googleを開くことができます。
例はありますか?chromedp(golang)で、Googleを開くことができます。複数のキーワードを順次で検索、順次で検索結果をクリック、それで、クリックしたページのタイトルを獲得する。
- 投稿日:2019-12-11T11:39:25+09:00
Go勉強(4) kubernetes client-goでPodのwatcher(TUI)を書いてみる
はじめに
この記事は Go6 Advent Calendar 2019 の12日目の記事です。
※ Goの勉強記録の一部をAdvent Calenderに投稿させて頂いてます。 過去分Kubernetesを操作した時の挙動を観察したくて、ターミナルからコマンドでNode/Podの状態をリアルタイムに表示するツールを書いてみました。
まだ時々おかしな挙動をしてますが 〆切が来たので投稿しちゃいます(後で直します)
Source code
https://gist.github.com/oruharo/11abc53c9ad324522fe5b6bdc6620323
Demo
下のターミナルでツールを起動して、上のターミナルからデプロイコマンドを入れてます。
ツール画面にPodが追加された後ステータスがRunningに変わります。
メイン処理
軽く説明していきます。
監視役・イベント・画面の三種類のオブジェクトで構成しています。
監視役KubeWatcher
が Channelを介してイベントKubeEvent
を画面KubeUI
に投げます。
監視役は非同期に動作します。(メソッドの前にgoって書くだけ。超簡単)
func main() { // // KubeUi <----------- KubeWatcher // kubeEvent eventChan := make(chan *KubeEvent, 10) kubeWatcher := NewKubeWatcher(eventChan) kubeUI := NewKubeUI(eventChan) go kubeWatcher.Run() kubeUI.Run() }監視役
KubeWatcher
client-go
のAPIcache.NewInformer
を使います。
kubernetesの各種オブジェクトに変化(追加・変更・削除)があるとイベントを送られてきます。以下はNodeの監視の設定です。 監視の開始は
nodesController.Run
で行います。(後述)
受け取ったイベントを即チャネルに投げます。func (kw *KubeWatcher) Run() { : 中略 watchNodes := cache.NewListWatchFromClient( clientset.CoreV1().RESTClient(), "nodes", v1.NamespaceAll, fields.Everything()) _, nodesController := cache.NewInformer( watchNodes, &v1.Node{}, 0, cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { kw.Sender <- NewKubeEvent(NodeAdd, obj) }, DeleteFunc: func(obj interface{}) { kw.Sender <- NewKubeEvent(NodeDelete, obj) }, UpdateFunc: func(old, new interface{}) { kw.Sender <- NewKubeEvent(NodeUpdate, new) }, }, )次はPodの監視です。
watchPods := cache.NewListWatchFromClient( clientset.CoreV1().RESTClient(), string(v1.ResourcePods), v1.NamespaceAll, fields.Everything()) _, podsController := cache.NewInformer( watchPods, &v1.Pod{}, 0, cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { kw.Sender <- NewKubeEvent(PodAdd, obj) }, DeleteFunc: func(obj interface{}) { kw.Sender <- NewKubeEvent(PodDelete, obj) }, UpdateFunc: func(old, new interface{}) { kw.Sender <- NewKubeEvent(PodUpdate, new) }, }, )NodeとPodsの監視を非同期で実行します。
実行中は無限ループで待ちます。 それが中断されたらダミーチャネルのclose処理が走って監視が停止します。stop := make(chan struct{}) defer close(stop) go nodesController.Run(stop) go podsController.Run(stop) for { time.Sleep(time.Second) } }イベント
KubeEvent
イベントの種類(Nodeの追加・変更・削除、Podの追加・変更・削除)と変更後のオブジェクトを持ちます。
type KubeEvent struct { eventType EventType newObj interface{} } type EventType int const ( NodeAdd EventType = iota NodeUpdate NodeDelete PodAdd PodUpdate PodDelete )画面
KubeUI
TUI用のライブラリTviewを利用させてもらいました。 超簡単にいい感じになります。
https://github.com/rivo/tview
https://godoc.org/github.com/rivo/tview
画面上の部品の階層構造をyamlっぽく書くとこんな感じです。rootView: # 縦方向のGrid headView: # ヘッダー nodeGrid: # 横方向のGrid - nodeView: # Nodeの箱 - nodeView: # : bottomView: # フッターRunメソッドにUIの大枠を実装しています。 Nodeの箱部分はイベントを受けてから描画します。(後述)
func (ui *KubeUI) Run() { : rootView := tview.NewGrid().SetRows(3, -1, 2) rootView.SetBackgroundColor(tcell.NewHexColor(0xe0e0e0)) headView := tview.NewTextView() headView.SetDynamicColors(true).SetBackgroundColor(tcell.NewHexColor(0x303030)) headView.SetBorderPadding(1, 1, 2, 0) headView.SetText("[#306ee3]⎈ [white]Kubernetes Watcher") ui.nodeGrid = tview.NewGrid() ui.nodeGrid.SetBorderPadding(1, 1, 2, 2) ui.nodeGrid.SetBackgroundColor(tcell.NewHexColor(0xf6f6f4)) ui.nodeGrid.SetGap(0, 2) botomView := tview.NewTextView() botomView.SetDynamicColors(true).SetBackgroundColor(tcell.NewHexColor(0xe0e0e0)) botomView.SetText("[#306ee3] watching... CTRL+C -> Exit") ui.app = tview.NewApplication().SetRoot( rootView. AddItem(headView, 0, 0, 1, 1, 0, 0, false). AddItem(ui.nodeGrid, 1, 0, 1, 1, 0, 0, false). AddItem(botomView, 2, 0, 1, 1, 0, 0, false), true, )イベント受信を非同期で起動しつつ、UIのメインループを実行します。
go ui.EventReciever() if err := ui.app.Run(); err != nil { panic(err) } }Channelからイベントを受け取って、Node/Pod部分を描画するメソッドを呼び出します。
func (ui *KubeUI) EventReciever() { for { kubeEvent := <-ui.Receiver switch kubeEvent.eventType { case NodeAdd: ui.AddNode(kubeEvent.newObj.(*v1.Node)) case NodeUpdate: ui.UpdateNode(kubeEvent.newObj.(*v1.Node)) case NodeDelete: ui.RemoveNode(kubeEvent.newObj.(*v1.Node)) case PodAdd: ui.AddPod(kubeEvent.newObj.(*v1.Pod)) case PodUpdate: ui.UpdatePod(kubeEvent.newObj.(*v1.Pod)) case PodDelete: ui.RemovePod(kubeEvent.newObj.(*v1.Pod)) } } }Nodeを追加する部分はこんな感じです。
非同期に他のgoroutineから処理するためtview.Applicationapp.Draw()メソッドで強制的に描画してます。
https://godoc.org/github.com/rivo/tview#hdr-Concurrency を参考にしました。func (ui *KubeUI) AddNode(v1Node *v1.Node) { nodeView := tview.NewTable() nodeView.SetBackgroundColor(tcell.NewHexColor(0x454545)) nodeView.Select(0, 0).SetFixed(1, 1).SetSelectable(true, false) nodeView.SetBorder(true).SetBorderPadding(1, 1, 1, 1) for _, nodeAddress := range v1Node.Status.Addresses { if nodeAddress.Type == v1.NodeInternalIP { nodeView.SetTitleAlign(tview.AlignLeft).SetTitle(nodeAddress.Address) break } } ui.nodeGrid.AddItem(nodeView, 0, len(ui.Nodes), 1, 1, 0, 0, false) : ui.app.Draw() }Podのステータスを判定する部分です。
こちらはKubectlのソースコードを参考に実装しました。
https://github.com/kubernetes/kubernetes/blob/master/pkg/printers/internalversion/printers.go#printPodfunc podStats(pod *v1.Pod) string { reason := string(pod.Status.Phase) : }ハマったとこ
罫線文字の右横が描画されない
Tviewから別のライブラリtcellが使われており、さらにライブラリrunewidthが使われています。
runewidth v0.0.4以降を使用すると発生するようですが、三つのライブラリどれがマズいのかはわかりません。ターミナルのロケールをLANG=ja-JP.UTF-8からen-US.UTF-8等に変えれば直ります。
ツールでは対処療法的に以下の一文を追加してrunewidthの動作を誤魔化してます。runewidth.DefaultCondition.EastAsianWidth = falseメソッドレシーバ
メソッドレシーバと呼ばれる
(ui *KubeUI)
の部分。
この*
を抜かして(ui KubeUI)
と書いてたため、uiのコピーが渡されて更新してもオリジナルに反映されず。。func (ui *KubeUI) AddNode(v1Node *v1.Node) { ↑ }終わりに
コード書くのに時間かかって、本文が薄めになってしまいました。
作ったツールは前から欲しかった物で、他の人にKubernetesのデモしたりするのにも便利なので、もうちょっと拡張しようと思います。
- 投稿日:2019-12-11T09:29:31+09:00
Goルーチン使ってスクリプト作ったー! / アクセス制限とうまくやる方法
i-plug Advent Calendar 2019 の【11日目】の記事です
あるスプリントで
こんな依頼がありました。
弊社のある部門が、あるサービスでねこさんの日記をつけていた。
しかしこの度、別のサービスに日記を投稿することにしたのでまえのサービスの日記に添付されてるねこさんの画像をすべて抜き取ってきてほしい、という依頼です。(フェイクです)
...その数なんと3万枚
おし!やったるで!
一瞬で終わりそう?
どう取得するかヒアリングしてみるとAPI経由での取得みたいでした。
であるならばGoルーチンとhttp/netをつかって3万のGETリクエストを一気に飛ばしAPIを叩けば、一瞬で取得できそうです。なんや、秒で終わりやん!
API経由で取得できるみたいだけど
公式から公開されているAPIです。やはりアクセス制限がかかっていました。
20request/second です。なん...やて...
Goルーチン と アクセス制限
秒間で 20request しか投げれないからこんな風な実装はどうでしょう?
アクセス制限とうまくやる方法
取ってくるのに必要な
imageID
のリスト(3万件分)のスライスを、20件ずつチャンクをつくり、それをスライスにいれる。20request分を1つのチャンクとして、それを一気にGoルーチンで処理させる
-> time.Sleep(time.Second()) でsleep 1秒 を挟む
-> 以下を全件分ループさせるそれを踏まえて実装はこのようになりました。
ラフにGoルーチンをつかった実装
巨大なスライスを受け取って
imageID
のチャンクのスライスを格納したスライスを返す関数type ImageID struct { Id string } func Chunked(slice []ImageID, size int) [][]ImageID { var chunkedSlice [][]ImageID sliceSize := len(slice) for i := 0; i < sliceSize; i += size { end := i + size if sliceSize < end { end = sliceSize } chunkedSlice = append(chunkedSlice, slice[i:end]) } return chunkedSlice }
1チャンクずつGoルーチンを動かして秒間最高効率をめざす関数(笑)
func main() { // ...他の処理 chunkedSlice := util.Chunked(beforeImageIDList, 20) for i := 0; i < len(chunkedSlice); i++ { imageIDList := chunkedSlice[i] for i := 0; i < len(imageIDList); i++ { go func(i int) { // APIを叩く関数 fetchImage(imageIDList[i].Id, APIkey) }(i) } time.Sleep(1000 * time.Millisecond) fmt.Printf("%v list done\n", i) } }画像を取得できましたが...
取得できたで!
だた、時間が結構かかってしもうた...28分くらいか〜...ん...APIのドキュメントに並列化とか書いたあるで〜なになに??
「複数のアカウントでAPI keyを作成すれば、ひとつにつき20件/秒をまもれば並列化できます」
結論 : ドキュメントは読みましょう。(実際、複数アカウントからAPIkeyを取得して物理的にも並列で叩きました。)
- 投稿日:2019-12-11T09:19:17+09:00
Goのタイマー time.Timer の作法
Goアドベントカレンダーその6の穴埋め投稿です。
忙しい人のためのまとめ
Timer.Stop()
は戻り値を見て<-timer.C
する(下記の作法参照)Timer.Reset()
はTimerが確実に止まってから呼ぶtime.AfterFunc()
は基本的には使わない- Timerは1つのgoroutineでしか触らない
Timer.Stop()の作法if !timer.Stop() { <-timer.C }time.Timer型とは
Goの標準パッケージ
time
にて提供されている機能で、指定時間後に一度だけ発火するイベントを作ることができます。
詳しくはドキュメントをご覧ください。
この記事の内容は、よく読めばドキュメントに書いてある内容ですが、よく読んで試さないとわかりにくかったのでまとめました。Timerを作成する関数は
NewTimer
とAfterFunc
の2種類が提供されていて、次のような違いがあります。timer := time.NewTimer(time.Second * 5) // 5秒後に発火するタイマー <-timer.C // 5秒後にチャネルに通知されるtimer := time.AfterFunc(time.Second * 5, func() { // 5秒後にこの関数が実行される })さらにこのTimerは
Stop()
で途中で停止したり、Reset()
で発火時間を変更することができます。func(t * Timer)Stop()bool func(t * Timer)Reset(d Duration)bool一見シンプルなように見えますが、使うときはいくつか注意すべきポイントがあるので紹介します。
Timer.Stop() の注意点
Timerは並行して動いているため、
Stop()
を呼び出そうとしている間にイベントが発火してしまう可能性があります。ダメな例timer := time.NewTimer(time.Second) time.Sleep(time.Second) timer.Stop() // Stopしたがこの時点ですでに発火している可能性がある select { case <-timer.C: // 発火していた場合、すでにチャネルに通知が投げ込まれているので、こちらが動く fmt.Println("timer") default: }
Stop()
呼び出し時にすでに発火していたかどうかは、戻り値で知ることができます。
後続する処理やTimerを再利用する場合に備えて、チャネルからイベントを取り除くのがおすすめです。正しくStopされるtimer := time.NewTimer(time.Second) time.Sleep(time.Second) if !timer.Stop() { <-timer.C // イベントを取り除いておく } select { case <-timer.C: // チャネルは空なので動かない fmt.Println("timer") default: }ただし、他のgoroutineでもチャネル
C
を待っている場合、この取り出し処理が無限に待たされる可能性が出てきてしまいます。
そもそもtimerを複数goroutineで待つのは、安全なStop()
ができなくなるのでやめたほうが良いです。ここでのポイント
Stop()
したら戻り値を調べてチャネルC
を空にしたほうが良い- チャネル
C
を待つのは1つのgoroutineだけにして、Stop()
も同じgoroutineだけで行うべきTimer.Reset()の注意点
Reset()
はTimerの発火時間を変更することができますが、ドキュメントにも書いてあるように、停止または発火済みのTimerでしか呼んではいけません。
停止していないTimerでReset()
を呼んだ場合、チャネルC
への通知が変更前のものか変更後のものか判断できないからです。動いているTimerの発火時間を変更したい場合は、先に
Stop()
してチャネルをクリアした後でReset()
を呼ぶ必要があります。
(Reset()
にもStop()
と同じ戻り値がありますが、これは後方互換のために残されているだけです)StopしてからResetするtimer := time.NewTimer(time.Second) time.Sleep(time.Second) if !timer.Stop() { // 先にStopしてチャネルへの書き込みを止める <- timer.C // 競合していない状態でチャネルを空にする } timer.Reset(time.Second * 10) // 停止しているので安全にReset <-timer.C fmt.Println("timer")また、ループ中でTimerを再利用する場合、チャネル
C
からイベントを受け取った後のStop()
の戻り値もfalse
なので注意が必要です。
無駄にチャネルC
から取り出そうとするとハングします。Timerの再利用の例d := time.Second timer := time.NewTimer(d) for { select { case <-hoge: // なにかを待つ if !timer.Stop() { // ここでtimerを止めて必要ならチャネルをクリア <-timer.C } // なにかを待っての処理 case <-timer.C: // タイムアウトの処理 // timer.Cはすでに空なので取り出してはいけない } timer.Reset(d) // ここではtimerは停止または発火済みでチャネルも空 }ここでのポイント
- Timerを確実に停止させてから
Reset()
を呼ぶReset()
する前にチャネルC
を空にしなければならない
- ハングしないよう気をつけて空にする
Reset()
の戻り値は使ってはいけないtime.AfterFunc()の注意点
ドキュメントにもあるとおり、
AfterFunc(d, f)
に設定した関数f
は、別のgoroutineで動作します。
このため、Stop()
を呼んだときにすでに動き始めている可能性が常あります。fが動いているかもしれないtimer := time.AfterFunc(time.Second, func() { // 関数f }) select { case <-hoge: // 何かを待つ timer.Stop() // 関数fはすでに動いているかもしれないし動いていないかもしれない // ... }別のイベントと排他的に実行したい処理であれば、
NewTimer
でTimerを作成してチャネルで制御したほうが良いです。
もちろんStop()
する必要がないなら有用です。timer := time.NewTimer(time.Second) select { case <-hoge: // 何か待つ if !timer.Stop() { <-timer.C } // ここではf()が実行されることはない case <-timer.C: f() // timerが発火しかつhogeが来ていないときのみ実行できる }ここでのポイント
Stop()
したい処理はtime.AfterFunc()
を使うべきではない
- 投稿日:2019-12-11T08:57:24+09:00
最速でinitを呼ぶ
Goアドベントカレンダーの穴埋め投稿です。
他の初期化処理に先駆けて、真っ先に何かをやりたいことってないですか?ありますよね?
initの呼び出し順序
Go処理系の実装依存であって、仕様はないのですが、Python同様Goは主要実装が仕様なところがあるのでこれに従って説明します。ググるといろいろ情報が出てきます。
Package initialization and program execution order
- mainパッケージの初期化
- 自身のパッケージの前にimportされているパッケージを初期化
- パッケージは1度にひとつずつ初期化される
- パッケージレベルの変数は宣言順に初期化される
- 最後に自分の中のinitが呼ばれる
- 最後にmain関数を呼ぶ
ここに書いてない情報としては、パッケージ内部の順序というのはファイル名の順序で規定されます。あと、init関数はパッケージ内部に同名関数をいくつも定義できますが、複数ある場合もパッケージ内部の順序(ファイル名順→ファイル内宣言順)で呼ばれます。
最速で呼ぶには
最速のファイル名を定義して、その中からimportして、その中にロジックを書くのが最速ということです。最速なのはゼロドットゴーです。
0.gopackage main import _ "github.com/shibukawa/fastest"これのためにわざわざリポジトリを作るのはあれなので、fastestサブフォルダを作ってgo.modでreplaceでそういうパッケージがあるように見せかけます。
go.modmodule fastest_test go 1.13 replace github.com/shibukawa/fastest => ./fastestこれが最速で呼ばれるコードです。
fastest/fastest.gopackage fastest import "log" func init() { log.Println("最速で呼ばれるinit") }これで最速でなにかやりたいときはバッチリです!まあ実装するのがツールやらライブラリならgo.modのreplaceはやらなくても良くて、そのパッケージのサブパッケージで指定すれば良いですね。今回はコードジェネレータで置き場所が事前に決まらない、みたいなユースケースで解法を考えたのでreplace使っています。
- 投稿日:2019-12-11T04:00:13+09:00
Kafkaメッセージの送信
さてではアプリからKafkaを使っていきたいと思います。
具体的に何をするかというと、POSTメソッドでTODOを保存する処理を非同期化したいと思います。今日はそのイベント送信部分をみていきます。
uw-labs/substrate
メッセージの送信・受信を抽象化するGoライブラリも弊社から公開されています。
Kafka以外のストリームプロセスを使う際でも同じ使い勝手で実装することができます。
今回はこのライブラリの使い方も一緒にみていきましょう。substrate.NewSynchronousMessageSink()
このsubstrateライブラリではメッセージ処理完了のシグナルをデフォルトで非同期化してパフォーマンスを上げているのですが、今回は同期バージョンである
substrate.NewSynchronousMessageSink()
のAPIを利用したいと思います。
main.go
内でメッセージシンクを設定するinitialiseKafkaSink
メソッドを以下のように定義します。main.gofunc initialiseKafkaSink(version, brokers, topic *string, keyFunc func(substrate.Message) []byte) (substrate.SynchronousMessageSink, error) { sink, err := kafka.NewAsyncMessageSink(kafka.AsyncMessageSinkConfig{ Brokers: strings.Split(*brokers, ","), Topic: *topic, KeyFunc: keyFunc, Version: *version, }) if err != nil { return nil, err } return substrate.NewSynchronousMessageSink(sink), nil }呼び出し側は以下のような感じです。
main.gounc main() { ... app.Action = func() { ... gSrv := initialiseGRPCServer(newServer(store)) actionSink, err := initialiseKafkaSink(sinkKafkaVersion, sinkBrokers, actionTopic, actionKeyFunc) if err != nil { log.Fatalln("init action event kafka sink:", err) } defer actionSink.Close() errCh := make(chan error, 2) ... } ... }Kafkaのバージョンなど、必要な環境変数は以下のように定義しています。
main.gosinkKafkaVersion := app.String(cli.StringOpt{ Name: "sink-kafka-version", Desc: "sink kafka version", EnvVar: "SINK_KAFKA_VERSION", }) sinkBrokers := app.String(cli.StringOpt{ Name: "sink-brokers", Desc: "kafka sink brokers", EnvVar: "SINK_BROKERS", Value: "localhost:9092", }) actionTopic := app.String(cli.StringOpt{ Name: "action-topic", Desc: "action topic", EnvVar: "ACTION_TOPIC", Value: "qiita.action", })残るはinitialiseKafkaSinkに渡している、
actionKeyFunc
という関数ですが、後ほど触れたいと思います。イベントの定義
では実際に送信するイベント、
CreateTodoActionEvent
を定義したいと思います。proto/event.protosyntax = "proto3"; package event; message CreateTodoActionEvent { string id = 1; string title = 2; string description = 3; }
proto/service.proto
内でTodo
メッセージを定義しているのでそれを使い回してもよかったのですが、コンパイルが少し厄介なのでリファクタリング要素として残しておきます。
make protos
タスクは以下のようになりました。.PHONY: protos protos: mkdir -pv $(GENERATED_DIR) $(GENERATED_SERVICE_DIR) $(GENERATED_EVENT_DIR) $(GENERATED_ENVELOPE_DIR) protoc \ -I $(PROTO_DIR) \ -I $(GOPATH)/src:$(GOPATH)/src/github.com/gogo/protobuf/protobuf \ --gogoslick_out=plugins=grpc:$(GENERATED_SERVICE_DIR) \ service.proto protoc \ -I $(PROTO_DIR) \ -I $(GOPATH)/src:$(GOPATH)/src/github.com/gogo/protobuf/protobuf \ --gogoslick_out=paths=source_relative:$(GENERATED_EVENT_DIR) \ event.proto protoc \ -I $(PROTO_DIR) \ -I $(GOPATH)/src:$(GOPATH)/src/github.com/gogo/protobuf/protobuf \ --gogoslick_out=paths=source_relative,$(ENVELOPE_PROTO_MAPPINGS):$(GENERATED_ENVELOPE_DIR) \ envelope.protokeyFuncの実装
先ほど後回しにした、
actionKeyFunc
という関数です。
これはイベントのキーを定義する関数です。
Kafkaでは同じキーを共有するメッセージは同じパーティションに入るという大事な性質があるため何をキーとして使うかという問題が無視できません。
先ほどイベント定義の際にid
フィールドを足したので、今回はこれを使いたいと思います。main.gofunc actionKeyFunc(msg substrate.Message) []byte { var env envelope.Event if err := proto.Unmarshal(msg.Data(), &env); err != nil { panic(err) } if types.Is(env.Payload, &event.CreateTodoActionEvent{}) { var ev event.CreateTodoActionEvent if err := types.UnmarshalAny(env.Payload, &ev); err != nil { panic(err) } return []byte(ev.Id) } panic("unknown event") }ここでは
- github.com/gogo/protobuf/proto
- github.com/gogo/protobuf/typesの2つのパッケージを使って、envelopeからイベントを取り出している様子も確認できます。
substrate.Messageの実装
メッセージ送信部分の実装に入りたいのですが、その前に1つ必要なことがあります。
メッセージ送信のAPIPublishMessage()
は引数としてsubstrate.Message
インターフェイスを受け取ります。
ということで、このインターフェイスを実装する型を用意する必要があります。必要なメソッドは1つだけ。
type Message interface { Data() []byte }以下のように型を定義します。
main.gotype message []byte func (m message) Data() []byte { return m }メッセージの送信
よし、じゃあメッセージの送信処理です。
server.CreateTodo()
メソッドは以下のようになります。server.gofunc (s *server) CreateTodo(ctx context.Context, req *service.CreateTodoRequest) (*service.CreateTodoResponse, error) { todoID := uuid.New().String() ev := &event.CreateTodoActionEvent{ Id: todoID, Title: req.Todo.Title, Description: req.Todo.Description, } any, err := types.MarshalAny(ev) if err != nil { return nil, err } env := envelope.Event{ Id: uuid.New().String(), Timestamp: types.TimestampNow(), Payload: any, } b, err := proto.Marshal(&env) if err != nil { return nil, err } if err := s.sink.PublishMessage(ctx, message(b)); err != nil { return nil, err } return &service.CreateTodoResponse{ Success: true, Id: todoID, }, nil }イベントをペイロードとしてenvelopeに包み込む部分、それから全体を
[]byte
にマーシャルする処理に注目してください。サーバー構造体は
substrate.SynchrounousMessageSink
への依存を持ちます。server.gotype ( ... server struct { sink substrate.SynchronousMessageSink } )メッセージを送信するコードのテストも書くべきですが、今回はおサボり。後日触れられたらと思います。
依存関係の注入
main.go
内で以下のように依存関係を注入してあげます。diff --git a/main.go b/main.go index be109e8..907cd41 100644 --- a/main.go +++ b/main.go @@ -104,8 +104,6 @@ func main() { } defer lis.Close() - gSrv := initialiseGRPCServer(newServer(nil)) - actionSink, err := initialiseKafkaSink(sinkKafkaVersion, sinkBrokers, actionTopic, actionKeyFunc) if err != nil { log.Fatalln("init payment account kafka sink:", err) @@ -122,6 +120,9 @@ func main() { }() var wg sync.WaitGroup + + gSrv := initialiseGRPCServer(newServer(actionSink)) + wg.Add(1) go func() { defer wg.Done()実際にはメッセージを送信する前にバリデーションなどを通して本当に保存していいのかをチェックする必要があるかなと思いますが、とりあえずこれでメッセージの送信部分は完了です。
思ったよりやることが多くて飛ばし飛ばしになってしまいました。すいません。
メッセージの送信処理が完了したので、次はこれを受信し、DBに保存する処理をみていきたいと思います。