20191211のGoに関する記事は11件です。

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)

と書いてもコンパイルは通ってしまいます。
苦肉の策として、以下のようにルールづけをして業務を行ってますが、なんとか機械的に検知したいところです。

Screen Shot 2019-12-11 at 19.21.33.png

作成できたもの

まだ作成途中ですが、パイプを使った書き方についてについては検知ができました!以下で公開をしています。今後機能を追加していく予定です。

repo.png

作成するまでの流れ

Analyzerのスケルトンコードを作る

gostaticanalysis/skeletonというツールででベースとなるコード一式を作ることができ、非常に便利です。
今回作成したツールもメインとなるgormchecker.goと、テストデータ以外変更をせずに作成できています。

go get -u github.com/gostaticanalysis/skeleton
skeleton gormchecker 

一番シンプルなチェッカー

以下で、変数名のチェックができています。動作確認はgo testで行えます。これをベースに作っていきます。

gormchecker.go
func 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.go
package a

var Gopher int // want "name of identifier must not be ’Gopher’"
var gopher int // OK

作るまでのアプローチ

実際に意図した記述と、意図していない記述を書いてその構造上の違いをみていくのがわかりやすそうです。

まず以下のようにテストを書きます。パイプを検知できればゴールです。

testdata/src/a/a.go
func check() {  
    db, _ := getConnection()    
    db = db.Where("column_a = xxx") 
    db = db.Where("column_a = xxx").Where("column_b = xxx") 
}

ast.Printを使えばデバッグができますが、大量に出て読みづらいので以下のようにデバッグを入れます。

gormchecker.go
func 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.go
    nodeFilter := []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

ありがとうございます!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Golang + echo】GoとechoでSQLのデータを表示【PostgreSQL】

1.はじめに

記事をご覧いただきありがとうございます。

今までC#とMySQLの接続は行ったことがあったのですが
ほとんど触れたことのないGo言語(+echo)とPostgreSQLとの接続
を行うことになったので備忘録的に手順を記していこうかと思います。

初歩的なところから説明するので適宜読み飛ばしていただければと思います。

2.PostgreSQLの設定

2-1.PostgreSQLのダウンロード

PostgreSQLのHPにアクセスしてダウンロードをクリック

無題.png

Binary packagesの中から自分のOSをクリック

無題.png

ページ内のDownload the installerをクリック(今回はWindowsですが他も同じ手順です。)

無題.png

自分のOSの使いたいバージョンのDownloadをクリック
(今回はWindowsx86-64の12.1バージョンをダウンロードしました。)

無題.png

2-2.PostgreSQLのインストールと環境変数設定(Windows10)

ダウンロードしたpostgresql-(バージョン数)-(OS名)アプリケーションを実行し
このような画面が出たら基本的にNext>を押し続けてください。
passwordだけは自分で設定しますが、portは基本的に干渉しなければ変えなくてよいと思います

image.png

インストールが完了したらシステム環境変数を設定します。
PostgreSQLをインストールした場所の中のbinフォルダがある場所
(変更していなければ、C:\Program Files\PostgreSQL(バージョン数))
にPathを通します。

Windows10の場合、スタートメニューを右クリック>設定で以下の画面が出るので
「システム環境」と入力すると「システム環境変数」が出てきます。

image.png

下のような画面が出てくるので環境変数をクリック

image.png

システム環境変数のPathをクリックして編集>前のパスの間に;(セミコロン)
はさんでPostgreSQLをインストールした場所の中のbinフォルダがある場所のパスを入力

無題.png

image.png

ここまで終わったらコマンドプロンプトを起動し、

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 sample

sampleデータベースに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.go

4-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.実行画面

  • sql.GetPost()
    無題.png

  • sql.GetPosts()
    無題.png

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してからreturn

5-2.実行例

  • sql.GetPost()
    image.png

  • sql.GetPosts()
    image.png

6.終わりに

GoもechoもPostgreSQLも初心者なのでざっとした流れとSQLのデータ表示についてまとめてみました。
まとめてくれているサイトなどがかなり少なかったので同じようなことに迷っている人の
助けになれば幸いです。

何か間違っているところや改善したほうがいいところなどありましたら教えていただけると幸いです。

7.参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Goでの画像生成

この記事はtomowarkar ひとりAdvent Calendar 2019の11日目の記事です。

記事を書く時間が取れず、またストックもないのでどんどんと簡素化していくのがわかりますね笑

今回はGoで640×400のpng画像を生成をしていきます。

ランダム生成

  1. int Slice で描画するフィールドを生成(ランダム)
  2. 1.を元にpngファイル生成
  3. ローカル保存

をしていきます。

コード

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)
}

結果

image1.png

ランダム生成(倍率指定)

これでは少し描画粒度が細かいので、倍率を指定して描画粒度を荒くしていきます。

差分のみの表示なので詳しく知りたい方はソースコードまでどうぞ。

コード

-   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] {

結果

image1.png

おまけ

今回紹介したコードを走らせても、同じ画像しか生成されないと思います。
これはフィールド生成の乱数の初期シード値が固定されているためです。

なので以下のように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

時間のフォーマット

https://stackoverflow.com/questions/20234104/how-to-format-current-time-using-a-yyyymmddhhmmss-format

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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サーバーの例

Web 1920 – 1.png
/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.ServeMuxhttp.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.ServerGoのインターフェイスをうまく利用した使いやすい仕組みであり、テストのための便利なツールセットとなっています。

テストカバレッジ

Goにおけるテストカバレッジ計測ツールについてご紹介。
テストカバレッジ見える化をするためには、まず大元であるテストカバレッジ計測ツールや方法が必要ですよね。

カバレッジ取得方法

go test -cover

この際、カバレッジを測定するだけでなく、テストも同時に実行されます。
スクリーンショット 2019-12-10 2.49.04.png

より詳しい結果の閲覧方法

「go test」で作成されたカバレッジプロファイル

go test -coverprofile=c.out

スクリーンショット 2019-12-10 3.06.12.png

注釈付きソースコードを表示するWebブラウザーで確認する場合

go tool cover -html=c.out

スクリーンショット 2019-12-10 3.06.55.png

Webブラウザーを起動する代わりにHTMLファイルを書き出し

go tool cover -html=c.out -o coverage.html

※Webブラウザーと同じ

各機能のカバレッジ率を標準出力に表示

go tool cover -func=c.out

スクリーンショット 2019-12-10 3.09.13.png

最後にカバレッジ注釈付きの変更されたソースコードを生成するには

go tool cover -mode=set -var=CoverageVariableName main.go

スクリーンショット 2019-12-10 3.10.26.png

テストカバレッジを得ることで、自身のライブラリのテスト計画を練るだけでなく、そのライブラリのテストの指針についても知ることができます。
よくテストされている箇所とテストのあまりされていない箇所を知ることは、テストの傾向を知ることになり、それは実装の指針を知ることにもなります。
テストのカバレッジによってテストの傾向を知り、アプリケーションで起き得るリスクを察知するために、活用できるかと。

テストカバレッジ見える化&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)

スクリーンショット 2019-12-11 3.04.54.png
スクリーンショット 2019-12-11 3.06.17.png
スクリーンショット 2019-12-12 20.47.05.png

緑がテストを回している箇所で、赤の部分がテストコードを書いていない部分です。
このように、図や、テスト箇所がすぐに見れるようになると、とてもわかりやすいです。

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さんです!

是非、お楽しみに!

ほなまた!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Goのtestingパッケージの基本を理解する

テックタッチアドベントカレンダー11日目を担当する @taisa831 です。10日目は @mxxxxkxxxx「Go言語 ElastiCacheの その前に」でした。575 の 5 が Go に掛かっていていい感じですね!もちろん内容も良い!

本記事では、Gotestingパッケージについて書きます。既存記事を調べてみると、そこそこあるけどそこまで多くはない。重厚な記事もあればあっさりした記事もある。ということで深すぎず浅すぎずを目指そうと思います。

「testing パッケージの基本を理解する」なのでGotesting パッケージ を参考にしました。テストに関しては最初アサーションがないことに戸惑いましたが、慣れたらない方がよく感じてきました。執筆時点でのバージョンはgo1.13.4です。

もっともシンプルなテスト

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.005s

Unorderd 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() { ... }
    • GoDocPackageに出る
  • func ExampleF() { ... }
    • GoDocFunctionに出る
  • func ExampleT() { ... }
    • GoDocTypeに出る
  • func ExampleT_M() { ... }
    • GoDocTypeMethodに出る

更に上記の単位で細かくExampleテストを分けて書きたい場合は_suffixのように小文字で記述します。

  • func Example_suffix() { ... }
  • func ExampleF_suffix() { ... }
  • func ExampleT_suffix() { ... }
  • func ExampleT_M_suffix() { ... }

GoDocExampleを出力してみる

実際にどのように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.go
func 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に表示される

スクリーンショット 2019-12-11 11.28.48.png

  • ExampleNewPerson()Typeに表示される

スクリーンショット 2019-12-11 11.29.09.png

  • ExamplePerson_GetFirstName()ExamplePerson_GetLastNamefuncに表示される

スクリーンショット 2019-12-11 11.29.29.png

スクリーンショット 2019-12-11 11.29.35.png

カバレッジをとる

上記サンプルにテストを追加しカバレッジをとってみます。カバレッジは-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テストだけだと中途半端感があるので通常のテストに変えてみます。好みもあると思いますがGolandVSCodeの機能を使うと簡単にテストコードの枠組みが出力できるのでかなり楽にテストが作成できます。

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.html

HTML出力結果

スクリーンショット 2019-12-11 13.32.22.png

テストをスキップする

下記の例では--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インターフェースに定義されているのであげておきます。基本的にはErrorErrorfを使うことが多いと思います。

  • 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関数を複数のテスト関数で呼び出して実行して確認してみます。また,TBtestingTBのインターフェースを実装(正確には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 です!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

chromedp(golang)で、Googleを開くことができます。

例はありますか?chromedp(golang)で、Googleを開くことができます。複数のキーワードを順次で検索、順次で検索結果をクリック、それで、クリックしたページのタイトルを獲得する。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go勉強(4) kubernetes client-goでPodのwatcher(TUI)を書いてみる

はじめに

この記事は Go6 Advent Calendar 2019 の12日目の記事です。
※ Goの勉強記録の一部をAdvent Calenderに投稿させて頂いてます。 過去分

Kubernetesを操作した時の挙動を観察したくて、ターミナルからコマンドでNode/Podの状態をリアルタイムに表示するツールを書いてみました。
まだ時々おかしな挙動をしてますが 〆切が来たので投稿しちゃいます:sweat_smile:(後で直します)

Source code

https://gist.github.com/oruharo/11abc53c9ad324522fe5b6bdc6620323

Demo

下のターミナルでツールを起動して、上のターミナルからデプロイコマンドを入れてます。
ツール画面にPodが追加された後ステータスがRunningに変わります。
zzzz.gif

メイン処理

軽く説明していきます。

監視役・イベント・画面の三種類のオブジェクトで構成しています。
監視役KubeWatcherが Channelを介してイベントKubeEventを画面KubeUIに投げます。
監視役は非同期に動作します。(メソッドの前にgoって書くだけ。超簡単:thumbsup_tone2:

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
スクリーンショット 2019-12-11 7.13.04.png
画面上の部品の階層構造を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#printPod

func podStats(pod *v1.Pod) string {
    reason := string(pod.Status.Phase)
         :
}

ハマったとこ

罫線文字の右横が描画されない

スクリーンショット 2019-12-11 8.04.15.png
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のデモしたりするのにも便利なので、もうちょっと拡張しようと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Goルーチン使ってスクリプト作ったー! / アクセス制限とうまくやる方法

:calendar_spiral: i-plug Advent Calendar 2019 の【11日目】の記事です:santa::tada:

あるスプリントで

こんな依頼がありました。
弊社のある部門が、あるサービスでねこ:cat:さんの日記をつけていた。
しかしこの度、別のサービスに日記を投稿することにしたのでまえのサービスの日記に添付されてるねこ:cat:さんの画像をすべて抜き取ってきてほしい、という依頼です。(フェイクです)

...その数なんと3万枚

おし!やったるで!:nerd:

一瞬で終わりそう?

どう取得するかヒアリングしてみるとAPI経由での取得みたいでした。
であるならばGoルーチンとhttp/netをつかって3万のGETリクエストを一気に飛ばしAPIを叩けば、一瞬で取得できそうです。

なんや、秒で終わりやん!:nerd:

API経由で取得できるみたいだけど

公式から公開されているAPIです。やはりアクセス制限がかかっていました。
20request/second です。

なん...やて...:nerd:

Goルーチン と アクセス制限

秒間で 20request しか投げれないからこんな風な実装はどうでしょう?

アクセス制限とうまくやる方法

取ってくるのに必要なimageIDのリスト(3万件分)のスライスを、20件ずつチャンクをつくり、それをスライスにいれる。

20request分を1つのチャンクとして、それを一気にGoルーチンで処理させる
-> time.Sleep(time.Second()) でsleep 1秒 を挟む
-> 以下を全件分ループさせる

それを踏まえて実装はこのようになりました。

ラフにGoルーチンをつかった実装

:one:巨大なスライスを受け取って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
}

:two: 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)
    }
}

画像を取得できましたが...

取得できたで!:nerd:
だた、時間が結構かかってしもうた...28分くらいか〜... :nerd:

ん...APIのドキュメントに並列化とか書いたあるで〜なになに??:nerd:

「複数のアカウントでAPI keyを作成すれば、ひとつにつき20件/秒をまもれば並列化できます」:nerd:

:joy::joy::joy:

結論 : ドキュメントは読みましょう。(実際、複数アカウントからAPIkeyを取得して物理的にも並列で叩きました。)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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を作成する関数はNewTimerAfterFuncの2種類が提供されていて、次のような違いがあります。

timer := time.NewTimer(time.Second * 5) // 5秒後に発火するタイマー
<-timer.C // 5秒後にチャネルに通知される
timer := time.AfterFunc(time.Second * 5, func() {
    // 5秒後にこの関数が実行される
})

さらにこのTimerはStop()で途中で停止したり、Reset()で発火時間を変更することができます。

funct * TimerStop()bool

funct * TimerResetd Durationbool

一見シンプルなように見えますが、使うときはいくつか注意すべきポイントがあるので紹介します。

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()を使うべきではない
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

最速でinitを呼ぶ

Goアドベントカレンダーの穴埋め投稿です。

他の初期化処理に先駆けて、真っ先に何かをやりたいことってないですか?ありますよね?

initの呼び出し順序

Go処理系の実装依存であって、仕様はないのですが、Python同様Goは主要実装が仕様なところがあるのでこれに従って説明します。ググるといろいろ情報が出てきます。

Package initialization and program execution order

  • mainパッケージの初期化
    • 自身のパッケージの前にimportされているパッケージを初期化
    • パッケージは1度にひとつずつ初期化される
    • パッケージレベルの変数は宣言順に初期化される
    • 最後に自分の中のinitが呼ばれる
  • 最後にmain関数を呼ぶ

ここに書いてない情報としては、パッケージ内部の順序というのはファイル名の順序で規定されます。あと、init関数はパッケージ内部に同名関数をいくつも定義できますが、複数ある場合もパッケージ内部の順序(ファイル名順→ファイル内宣言順)で呼ばれます。

最速で呼ぶには

最速のファイル名を定義して、その中からimportして、その中にロジックを書くのが最速ということです。最速なのはゼロドットゴーです。

0.go
package main

import _ "github.com/shibukawa/fastest"

これのためにわざわざリポジトリを作るのはあれなので、fastestサブフォルダを作ってgo.modでreplaceでそういうパッケージがあるように見せかけます。

go.mod
module fastest_test

go 1.13

replace github.com/shibukawa/fastest => ./fastest

これが最速で呼ばれるコードです。

fastest/fastest.go
package fastest

import "log"

func init() {
    log.Println("最速で呼ばれるinit")
}

これで最速でなにかやりたいときはバッチリです!まあ実装するのがツールやらライブラリならgo.modのreplaceはやらなくても良くて、そのパッケージのサブパッケージで指定すれば良いですね。今回はコードジェネレータで置き場所が事前に決まらない、みたいなユースケースで解法を考えたのでreplace使っています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Kafkaメッセージの送信

さてではアプリからKafkaを使っていきたいと思います。
具体的に何をするかというと、POSTメソッドでTODOを保存する処理を非同期化したいと思います。

今日はそのイベント送信部分をみていきます。

uw-labs/substrate

メッセージの送信・受信を抽象化するGoライブラリも弊社から公開されています。
Kafka以外のストリームプロセスを使う際でも同じ使い勝手で実装することができます。
今回はこのライブラリの使い方も一緒にみていきましょう。

substrate.NewSynchronousMessageSink()

このsubstrateライブラリではメッセージ処理完了のシグナルをデフォルトで非同期化してパフォーマンスを上げているのですが、今回は同期バージョンであるsubstrate.NewSynchronousMessageSink()のAPIを利用したいと思います。
main.go内でメッセージシンクを設定するinitialiseKafkaSinkメソッドを以下のように定義します。

main.go
func 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.go
unc 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.go
sinkKafkaVersion := 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.proto
syntax = "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.proto

keyFuncの実装

先ほど後回しにした、actionKeyFuncという関数です。
これはイベントのキーを定義する関数です。
Kafkaでは同じキーを共有するメッセージは同じパーティションに入るという大事な性質があるため何をキーとして使うかという問題が無視できません。
先ほどイベント定義の際にidフィールドを足したので、今回はこれを使いたいと思います。

main.go
func 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.go
type message []byte

func (m message) Data() []byte {
    return m
}

メッセージの送信

よし、じゃあメッセージの送信処理です。
server.CreateTodo()メソッドは以下のようになります。

server.go
func (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.go
type (
    ...

    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に保存する処理をみていきたいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む