20191201のGoに関する記事は17件です。

golang/oauth2を使ってgithubのOAuth認証用APIサーバを作ってみる(WIP)

はじめに

この記事は、Go4 アドベントカレンダーの2日目になります。
OAuthの認証ってどうやって実装するのだろう。
golang/oauth2: Go OAuth2があることは知っていましたが、実際に触ったことがなかったので、今回認証用のAPIサーバを作ってみようと思いました。
時間がなくてまだWIPです。。。
hirano00o/github-oauth-sample

golang/oauth2の使い方

oauth2/example_test.go at master · golang/oauth2 を見ると、使い方がわかります。

簡単にまとめるとこんな感じ。

  1. conf := &oauth2.Config{} にGithubのCLIENT IDやSECRET、エンドポイントを詰める。
    エンドポイントは、oauth2/github.go at master · golang/oauth2importして利用する。
    このconfは、トークンを取得するときや、APIにアクセスするときにも利用する。
  2. conf.AuthCodeURL()でCSRF対策用のstate等を入れて、GithubにログインするためのURLを作成する。
    このURLにアクセスすると、Githubのログイン画面が出力され、ログインするとコールバック用のURLにリダイレクトされる。
    コールバック用のURLは、CLIENT IDやSECRETを作成したときに設定したもの。
  3. コールバック用URLにリダイレクトされたリクエストには、codestateがformに設定されている。
    このstateが、AUthCodeURL()で設定したstateとイコールかを確認し、イコールであればcodeを基にconf.Exchange()でトークンを発行する。
    Exchange()で発行したトークンは、構造体であり、access tokentoken type, refresh token, expiryが入っている。
  4. このトークンとコンテキストでconf.Client()からhttpClientを作り、GetやPost等を行う。
    ちなみに、conf.Client()したとき、TokenSource()が呼ばれ、トークンのリフレッシュが必要なときは、自動的にリフレッシュされる。

フロー

登場人物は

  • ユーザと
  • フロント用サーバ
  • 認証用APIサーバ(今回作っているもの)

です。
コールバック先は、フロント用サーバとしています。

  1. ユーザがフロント用サーバにアクセスする。フロント用サーバはセッションIDを払い出しCookieに詰める。
  2. ユーザがログインボタンを押すと、フロント用サーバは、セッションIDを取得し、認証用APIサーバにセッションIDを渡す。
  3. 認証用サーバは、ランダム値のstateを作成、githubログイン用URLを発行する。stateとセッションIDを紐付けてDBに入れる。
  4. フロント用サーバは、ユーザを返却されたログイン用URLにリダイレクトさせる。
  5. ユーザがログインに成功するとコールバックされ、フロント用サーバはcodestateを取得する。
  6. フロント用サーバは、codestate、セッションIDを認証用APIサーバに渡す。
  7. 認証用APIサーバは、stateがURL発行時のものとイコールか確認し、イコールであれば、codeからgithub用のトークンを作成する。
    また、ユーザ用のトークンを作成し、これとgithub用のトークンを紐付けてDBに保存し、ユーザ用のトークンのみ返却する。
  8. フロント用サーバは、トークンをCookieに詰めてユーザに返す。

(例えば)この後に、リポジトリ一覧を取得する場合は、

  1. フロント用サーバがCookieからトークンを取得し、認証用APIサーバに渡す。
  2. 認証用APIサーバは、トークンの期限を確認し、トークンと紐づくgithub用のトークンを返す。
  3. フロント用サーバは、返却されたgithub用のトークンでリポジトリ一覧を取得する

ユーザ用のトークンとか無しで、セッションIDとgithub用のトークンを紐付ければよかったか...

コード(WIP)

Clean Architectureを意識して作ってみています。
hirano00o/github-oauth-sample

ディレクトリ構成
.
├── domain
│   └── domain.go
├── go.mod
├── go.sum
├── infrastructure
│   ├── config.go
│   ├── dbhandler.go
│   └── router.go
├── interfaces
│   ├── controllers
│   │   ├── context.go
│   │   ├── controller.go
│   │   └── error.go
│   ├── database
│   │   ├── database.go
│   │   └── repository.go
│   └── handler.go
├── main.go
├── mysql
│   └── init.sql
├── swagger.yaml
└── usecases
    └── usecase.go

よく利用するoauth2.Configは、infrastructure層のconfig.goで環境変数を読み込むときに一緒に作り、コンフィグとして読み込んでいます。

infrastructure/config.go
func getGithubConf() (github oauth2.Config) {
        scopes := []string{"repo"}
        github = oauth2.Config{
                ClientID:     os.Getenv("GITHUB_CLIENT_ID"),
                ClientSecret: os.Getenv("GITHUB_CLIENT_SECRET"),
                RedirectURL:  os.Getenv("SERVER_HOST"),
                Scopes:       scopes,
                Endpoint:     oauth2github.Endpoint,
        }
        return
}

そして読み込んだコンフィグは、interface層controllerに渡して利用しています。
正直どこで作るべきか悩みましたが、毎回どこかで作るよりかは...ってことでコンフィグ読み込み時に一緒にしました。
usecase層まで足を伸ばしているのが、微妙に感じるけど問題ないのか? state入りのURL作ったり、トークン発行するためには必要だけども。

infrastructure/router.go
func Router() *gin.Engine {
        conf := NewConf()
        controller := controllers.NewController(NewDB(conf.DBConf.Database, conf.DBConf.DSN))
        router := gin.Default()
        v1 := router.Group("/v1")
        auth := v1.Group("/auth")
        github := auth.Group("/github")
        github.GET("/login", func(c *gin.Context) { controller.Login(c, conf) })
        github.GET("/callback", func(c *gin.Context) { controller.Callback(c, conf) })
        github.GET("/token", func(c *gin.Context) { controller.Auth(c) })
        zap.S().Info("running")
        return router
}

おわりに

なるべく早く完成させます。
DBは、Redisに変更したいし。テスト作らないと。。。

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

golangにおける継承

導入

golangのオブジェクト指向言語ぽい機能を調べてみました。

エイリアス

type myInt int

こういうやつですね。これはもともとの型のエイリアスです。
元の型とは別の型ですが、以下のように相互に変換することができます。

var i int = 1
var j myInt = myInt(i)
var k int = int(j)

エイリアス化すれば、自由に機能(メソッド)を追加することができます。
JavaなどではBuilt-in(java.lang類)型は継承も機能追加もできないのと比較すると、Goらしい自由さを感じるところです。

var myInt int
func (i myInt) String() string {
   return fmt.Sprintf("myInt --- %d", i)
}

この機能、enumでよく使われますね。

type MyType int
const (
   A MyType = iota
   B MyType
   C MyType
)

ちなみに、superが欲しければ、

func (i MyType) String() string {
   j := OrigType(i)
   return "MyType" + j.String()
}

のような感じで取得できます。少し面倒ですが。

埋め込み型

type MyInt struct {
   int
}

MyIntはintを要素に持ちながら、intの機能を持ちます。
エイリアスとの違いとしては、

  • 多重継承できる
  • superにアクセスするのが簡単
  • 継承元以外の要素を含める

といったところかと思います。
多重継承によってメソッドが衝突した場合、衝突を解消しない限りコンパイルエラーになります。

type A struct {}
type B struct {}
func (*A) Hoge() string { return "A" }
func (*B) Hoge() string { return "B" }
// CはAとBを継承(埋め込み)
type C struct {
   A
   B
}
func (c *C) Hoge() string { return c.A.Hoge() + c.B.Hoge() }

例えばこんな風に衝突を解消することができるでしょう。
こんなコードが推奨されるか?というと疑問ですが。

どう使うか

個人的には以下の順序で考えます。

  1. structの要素か?(内部ロジックで使うのみなら)
  2. エイリアスか?(単純な型のコピーもしくは拡張であれば)
  3. 埋め込み型?(型を拡張しつつかつ、superや他の要素が必要な場合)

まずはhas_aでいいのではないかと考えます。
型の拡張の場合はまずエイリアスを検討します。
埋め込み型は上記を超える要件の場合のみとしたい所です。

aliasはわりとカジュアルに使えると思っていますが、埋め込み型はオブジェクト指向での継承の是非の議論と同じように使用に慎重であるべきかと思います。

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

Go言語のVueライクなライブラリ、Vuguを利用したWebAssembly入門(GitHubAPIを利用したサンプル実装)

概要

VueやReactにインスパイアされ、Go言語で書かれたWebAssemblyのライブラリ、Vuguが登場しました。Vuguでは、Vueのような単一コンポーネントファイルを利用して、簡単にWebAssemblyのアプリケーションを作ることができます。

ちなみに、WebAssemblyについて知りたい方は、こちらの記事などをご参照ください。

本記事では、Vuguを使ってWebAssemblyを利用したサンプルアプリケーションを作っていきます。
具体的には、GitHubのAPIを利用して、ユーザのリポジトリの一覧を表示するアプリケーションを作ります。

本記事で作成するアプリケーションの挙動を確認したい方は、以下から閲覧することができます。

Screen Shot 2019-12-01 at 21.57.00.png

また、ソースコードを確認したい方は、以下から閲覧することができます。

想定読者・解説内容

本記事では、VueおよびGoの文法について基本的な知識のある方を対象としています。
また、Vueを知っていればある程度予測できる内容部分に関する解説はスキップし、Vueとは異なる部分にフォーカスして解説をします。

開発

はじめに

筆者の開発環境は以下の通りです。
VuguがGoのバージョン1.12以上を求めているので、それ以上のバージョンとしてください。

go version go1.13.3 darwin/amd64

まずはじめに、作業用のディレクトリを作成します。
名前はなんでも良いですが、VuguGitHubClientという名前で進めていきます。

mkdir VuguGitHubClient
cd VuguGitHubClient

Goのパッケージ管理をする仕組みとして、今回はGo Modulesを利用します。
以下のコマンドで初期化します。
ユーザ名などは適宜置き換えてください。

go mod init github.com/solt9029/VuguGitHubClient

コンポーネント実装

さてここからが本題です。
Vueの単一コンポーネントのようなファイルとして、root.vuguを作成します。
内容は以下の通りです。

root.vugu
<div class="root">
    <div class="container">
        <div class="row">
            <h1>VuguGitHubClient</h1>
            <div class="input-group block">
                <input type="text" placeholder="user" id="user" class="form-control" @change="data.HandleChange(event)">
                <div class="input-group-append">
                    <button class="btn btn-primary" @click="data.HandleClick(event)">find repos</button>
                </div>
            </div>
            <div class="block">
                <div vg-if="data.IsLoading">isLoading...</div>
                <div vg-if='data.Error != ""' vg-html="data.Error"></div>
                <div vg-if="len(data.Repos) > 0">
                    <ul class="list-group">
                        <repo-item vg-for="_, repo := range data.Repos" :name="repo.Name" :html-url="repo.HtmlUrl"></repo-item>
                    </ul>
                </div>
            </div>
        </div>
    </div>

    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
</div>

<style>
.block {
    margin-bottom: 30px;
}
</style>

<script type="application/x-go">
import (
    "encoding/json"
    "log"
    "net/http"
)

type RootData struct {
    Repos     []Repo
    IsLoading bool
    User      string
    Error     string
}

type Repo struct {
    Name    string `json:"name"`
    HtmlUrl string `json:"html_url"`
}

func (data *RootData) HandleChange(event *vugu.DOMEvent) {
    data.User = event.JSEvent().Get("target").Get("value").String()
    log.Printf("user: %v", data.User)
}

func (data *RootData) HandleClick(event *vugu.DOMEvent) {
    data.Repos = []Repo{}
    eventEnv := event.EventEnv()
    data.IsLoading = true
    data.Error = ""

    go func() {
        eventEnv.Lock()
        defer func() {
            eventEnv.UnlockRender()
            data.IsLoading = false
        }()

        res, err := http.Get("https://api.github.com/users/" + data.User + "/repos")
        if err != nil {
            log.Printf("Error fetching: %v", err)
            data.Error = "Error fetching."
            return
        }
        defer res.Body.Close()

        var newRepos []Repo
        err = json.NewDecoder(res.Body).Decode(&newRepos)
        if err != nil {
            log.Printf("Error JSON decoding: %v", err)
            data.Error = "Error JSON decoding."
            return
        }

        data.Repos = newRepos
    }()
}
</script>

デフォルトでは、root.vuguという名前のコンポーネントが一番親の要素ということになります。

さて、Vuguファイルの仕様についてもう少し詳しく解説していきます。
Vuguファイルは以下のように構成します。
基本的には、Vueと同じだと考えて問題ないでしょう。

// html
<div></div>

// style
<style></style>

// script
<script type="application/x-go"></script>

大きくVueと異なる点は、当たり前ではありますが、script内にGoのコードを書くというところです。

まずはscript内のコードから見ていきましょう。

type RootData struct {
    Repos     []Repo
    IsLoading bool
    User      string
    Error     string
}

RootDataで定義されているものは、HTML内でdata.Userのように参照できたりします。
コンポーネント内で状態を持つデータを表しています。
RootDataという名前になっていますが、こちらは命名規則に従って名前がつけられています。
コンポーネントの名前に対してアッパーキャメルケースで名付けます。

//  例
root.vugu -> RootData
example-component.vugu -> ExampleComponentData

APIから値を取得し、データを更新する処理については、非同期処理になります。
WebAssembly内で非同期処理を行う場合、デッドロックを避けるために、新しいgoroutineを作成します。
細かい解説はコード内部に記します。

    go func() {
        eventEnv.Lock() // 排他ロック取得
        defer func() {
            eventEnv.UnlockRender() // この関数終了時にロック解除
            data.IsLoading = false // この関数終了時にdata.isLoadingをfalseにする
        }()

        res, err := http.Get("https://api.github.com/users/" + data.User + "/repos")
        if err != nil {
            log.Printf("Error fetching: %v", err)
            data.Error = "Error fetching."
            return
        }
        defer res.Body.Close()

        var newRepos []Repo
        err = json.NewDecoder(res.Body).Decode(&newRepos) // 取得したjsonをパースする
        if err != nil {
            log.Printf("Error JSON decoding: %v", err)
            data.Error = "Error JSON decoding."
            return
        }

        data.Repos = newRepos // data.Reposを更新する
    }()

お気づきかもしれませんが、root.vuguのHTML内で<repo-item>といったHTMLタグが登場しています。
こちらはカスタムコンポーネントなので、今から作成していきましょう。
repo-item.vuguを作成します。
内容は以下の通りです。

repo-item.vugu
<li class="list-group-item">
    <a :href="data.HtmlUrl" vg-html="data.Name"></a>
</li>

<script type="application/x-go">
type RepoItemData struct {
    Name    string
    HtmlUrl string
}

func (component *RepoItem) NewData(props vugu.Props) (interface{}, error) {
    ret := &RepoItemData{}
    ret.Name, _ = props["name"].(string)
    ret.HtmlUrl, _ = props["html-url"].(string)
    return ret, nil
}
</script>

propsとして値を受け取るためには、NewDataという関数を利用して、dataに受け渡してあげる必要があります。
propsをそのまま使うといったことは基本的にはできません。(できないはず。違ったら教えてください。)

さて、コアとなる必要なコンポーネントを作ることができました!

サーバ実装

次に、サーバのコードをちょろっと書きます。
server.goを作成しましょう。
こちらはVuguが提供しているsimplehttpを利用しており、hotreloadとはいきませんが、vuguファイルを書きなおすたびにその変更が反映されます。(ブラウザのリロードは手動)

server.go
// +build !wasm

package main

//go:generate vugugen .

import (
    "flag"
    "log"
    "net/http"
    "os"
    "path/filepath"

    "github.com/vugu/vugu/simplehttp"
)

func main() {
    dev := flag.Bool("dev", false, "Enable development features")
    dir := flag.String("dir", ".", "Project directory")
    httpl := flag.String("http", "127.0.0.1:8877", "Listen for HTTP on this host:port")
    flag.Parse()
    wd, _ := filepath.Abs(*dir)
    os.Chdir(wd)
    log.Printf("Starting HTTP Server at %q", *httpl)
    h := simplehttp.New(wd, *dev)
    log.Fatal(http.ListenAndServe(*httpl, h))
}

上記のコードは、Vugu公式のドキュメント内のソースコードを利用しています。

さて、準備完了です!
以下のコマンドを打ってみてください。

go run server.go -dev

http://localhost:8877 にてVuguがWebAssemblyを利用して動いていることを確認できるでしょう。

実際にserver.goを利用して、Vuguのアプリケーションが動いていることを確認できました。
できれば、staticなファイルとしてビルドできると嬉しいですよね?
以下のようなdist.goを作成し、distディレクトリにビルドしましょう!

dist.go
// +build ignore

package main

import (
    "flag"
    "fmt"
    "log"
    "net/http"
    "os"
    "os/exec"
    "path/filepath"
    "text/template"
    "time"

    "github.com/vugu/vugu/distutil"
    "github.com/vugu/vugu/simplehttp"
)

func main() {

    clean := flag.Bool("clean", false, "Remove dist dir before starting")
    dist := flag.String("dist", "dist", "Directory to put distribution files in")
    flag.Parse()

    start := time.Now()

    if *clean {
        os.RemoveAll(*dist)
    }

    os.MkdirAll(*dist, 0755) // create dist dir if not there

    // copy static files
    distutil.MustCopyDirFiltered(".", *dist, nil)

    // find and copy wasm_exec.js
    distutil.MustCopyFile(distutil.MustWasmExecJsPath(), filepath.Join(*dist, "wasm_exec.js"))

    // check for vugugen and go get if not there
    if _, err := exec.LookPath("vugugen"); err != nil {
        fmt.Print(distutil.MustExec("go", "get", "github.com/vugu/vugu/cmd/vugugen"))
    }

    // run go generate
    fmt.Print(distutil.MustExec("go", "generate", "."))

    // run go build for wasm binary
    fmt.Print(distutil.MustEnvExec([]string{"GOOS=js", "GOARCH=wasm"}, "go", "build", "-o", filepath.Join(*dist, "main.wasm"), "."))

    // STATIC INDEX FILE:
    // if you are hosting with a static file server or CDN, you can write out the default index.html from simplehttp
    req, _ := http.NewRequest("GET", "/index.html", nil)
    outf, err := os.OpenFile(filepath.Join(*dist, "index.html"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
    distutil.Must(err)
    defer outf.Close()
    template.Must(template.New("_page_").Parse(simplehttp.DefaultPageTemplateSource)).Execute(outf, map[string]interface{}{"Request": req})

    // BUILD GO SERVER:
    // or if you are deploying a Go server (yay!) you can build that binary here
    // fmt.Print(distutil.MustExec("go", "build", "-o", filepath.Join(*dist, "server"), "."))

    log.Printf("dist.go complete in %v", time.Since(start))
}

上記のコードもVugu公式のドキュメント内のソースコードを利用しています。

以下のコマンドを打ってみてください。

go run dist.go

新しくdistディレクトリが作成され、index.htmlmain.wasmwasm_exec.jsが作られていることが確認できます。

以上で開発は終了です!
お疲れ様でした。

Vueと比較してVuguでやりにくいこと

筆者がVuguを使ってみて、やりにくいと感じたことを書きます。
なお、調べきれていないと思うので、何か間違いなどあればご指摘ください。

  • propsを一旦dataにいれなければならない
  • エディタのSyntaxHighlightが存在しない(はず)
  • ライフサイクルに対応した関数がほとんど存在しない(こちらのPRによれば、BeforeBuild()というライフサイクルは現在新しいバージョンで実装されているらしい。例えばコンポーネントがunmountするときのようなライフサイクルイベントは現状存在しなそう。)

最後に

Vuguを利用して、簡単にWebAssemblyのアプリケーションを書くことができました。
WebAssemblyは敷居の高いものだと思われていますが、基本的なVueとGoの知識さえあれば何も抵抗なく開発を行えるということがわかりました。
最後まで本記事を読んでいただきありがとうございました。

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

Go言語のVueライクなライブラリ、Vuguを利用したWebAssembly入門

概要

VueやReactにインスパイアされ、Go言語で書かれたWebAssemblyのライブラリ、Vuguが登場しました。Vuguでは、Vueのような単一コンポーネントファイルを利用して、簡単にWebAssemblyのアプリケーションを作ることができます。

ちなみに、WebAssemblyについて知りたい方は、こちらの記事などをご参照ください。

本記事では、Vuguを使ってWebAssemblyを利用したサンプルアプリケーションを作っていきます。
具体的には、GitHubのAPIを利用して、ユーザのリポジトリの一覧を表示するアプリケーションを作ります。

本記事で作成するアプリケーションの挙動を確認したい方は、以下から閲覧することができます。

Screen Shot 2019-12-01 at 21.57.00.png

また、ソースコードを確認したい方は、以下から閲覧することができます。

想定読者・解説内容

本記事では、VueおよびGoの文法について基本的な知識のある方を対象としています。
また、Vueを知っていればある程度予測できる内容部分に関する解説はスキップし、Vueとは異なる部分にフォーカスして解説をします。

開発

はじめに

筆者の開発環境は以下の通りです。
VuguがGoのバージョン1.12以上を求めているので、それ以上のバージョンとしてください。

go version go1.13.3 darwin/amd64

まずはじめに、作業用のディレクトリを作成します。
名前はなんでも良いですが、VuguGitHubClientという名前で進めていきます。

mkdir VuguGitHubClient
cd VuguGitHubClient

Goのパッケージ管理をする仕組みとして、今回はGo Modulesを利用します。
以下のコマンドで初期化します。
ユーザ名などは適宜置き換えてください。

go mod init github.com/solt9029/VuguGitHubClient

コンポーネント実装

さてここからが本題です。
Vueの単一コンポーネントのようなファイルとして、root.vuguを作成します。
内容は以下の通りです。

root.vugu
<div class="root">
    <div class="container">
        <div class="row">
            <h1>VuguGitHubClient</h1>
            <div class="input-group block">
                <input type="text" placeholder="user" id="user" class="form-control" @change="data.HandleChange(event)">
                <div class="input-group-append">
                    <button class="btn btn-primary" @click="data.HandleClick(event)">find repos</button>
                </div>
            </div>
            <div class="block">
                <div vg-if="data.IsLoading">isLoading...</div>
                <div vg-if='data.Error != ""' vg-html="data.Error"></div>
                <div vg-if="len(data.Repos) > 0">
                    <ul class="list-group">
                        <repo-item vg-for="_, repo := range data.Repos" :name="repo.Name" :html-url="repo.HtmlUrl"></repo-item>
                    </ul>
                </div>
            </div>
        </div>
    </div>

    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
</div>

<style>
.block {
    margin-bottom: 30px;
}
</style>

<script type="application/x-go">
import (
    "encoding/json"
    "log"
    "net/http"
)

type RootData struct {
    Repos     []Repo
    IsLoading bool
    User      string
    Error     string
}

type Repo struct {
    Name    string `json:"name"`
    HtmlUrl string `json:"html_url"`
}

func (data *RootData) HandleChange(event *vugu.DOMEvent) {
    data.User = event.JSEvent().Get("target").Get("value").String()
    log.Printf("user: %v", data.User)
}

func (data *RootData) HandleClick(event *vugu.DOMEvent) {
    data.Repos = []Repo{}
    eventEnv := event.EventEnv()
    data.IsLoading = true
    data.Error = ""

    go func() {
        eventEnv.Lock()
        defer func() {
            eventEnv.UnlockRender()
            data.IsLoading = false
        }()

        res, err := http.Get("https://api.github.com/users/" + data.User + "/repos")
        if err != nil {
            log.Printf("Error fetching: %v", err)
            data.Error = "Error fetching."
            return
        }
        defer res.Body.Close()

        var newRepos []Repo
        err = json.NewDecoder(res.Body).Decode(&newRepos)
        if err != nil {
            log.Printf("Error JSON decoding: %v", err)
            data.Error = "Error JSON decoding."
            return
        }

        data.Repos = newRepos
    }()
}
</script>

デフォルトでは、root.vuguという名前のコンポーネントが一番親の要素ということになります。

さて、Vuguファイルの仕様についてもう少し詳しく解説していきます。
Vuguファイルは以下のように構成します。
基本的には、Vueと同じだと考えて問題ないでしょう。

// html
<div></div>

// style
<style></style>

// script
<script type="application/x-go"></script>

大きくVueと異なる点は、当たり前ではありますが、script内にGoのコードを書くというところです。

まずはscript内のコードから見ていきましょう。

type RootData struct {
    Repos     []Repo
    IsLoading bool
    User      string
    Error     string
}

RootDataで定義されているものは、HTML内でdata.Userのように参照できたりします。
コンポーネント内で状態を持つデータを表しています。
RootDataという名前になっていますが、こちらは命名規則に従って名前がつけられています。
コンポーネントの名前に対してアッパーキャメルケースで名付けます。

//  例
root.vugu -> RootData
example-component.vugu -> ExampleComponentData

APIから値を取得し、データを更新する処理については、非同期処理になります。
WebAssembly内で非同期処理を行う場合、デッドロックを避けるために、新しいgoroutineを作成します。
細かい解説はコード内部に記します。

    go func() {
        eventEnv.Lock() // 排他ロック取得
        defer func() {
            eventEnv.UnlockRender() // この関数終了時にロック解除
            data.IsLoading = false // この関数終了時にdata.isLoadingをfalseにする
        }()

        res, err := http.Get("https://api.github.com/users/" + data.User + "/repos")
        if err != nil {
            log.Printf("Error fetching: %v", err)
            data.Error = "Error fetching."
            return
        }
        defer res.Body.Close()

        var newRepos []Repo
        err = json.NewDecoder(res.Body).Decode(&newRepos) // 取得したjsonをパースする
        if err != nil {
            log.Printf("Error JSON decoding: %v", err)
            data.Error = "Error JSON decoding."
            return
        }

        data.Repos = newRepos // data.Reposを更新する
    }()

お気づきかもしれませんが、root.vuguのHTML内で<repo-item>といったHTMLタグが登場しています。
こちらはカスタムコンポーネントなので、今から作成していきましょう。
repo-item.vuguを作成します。
内容は以下の通りです。

repo-item.vugu
<li class="list-group-item">
    <a :href="data.HtmlUrl" vg-html="data.Name"></a>
</li>

<script type="application/x-go">
type RepoItemData struct {
    Name    string
    HtmlUrl string
}

func (component *RepoItem) NewData(props vugu.Props) (interface{}, error) {
    ret := &RepoItemData{}
    ret.Name, _ = props["name"].(string)
    ret.HtmlUrl, _ = props["html-url"].(string)
    return ret, nil
}
</script>

propsとして値を受け取るためには、NewDataという関数を利用して、dataに受け渡してあげる必要があります。
propsをそのまま使うといったことは基本的にはできません。(できないはず。違ったら教えてください。)

さて、コアとなる必要なコンポーネントを作ることができました!

サーバ実装

次に、サーバのコードをちょろっと書きます。
server.goを作成しましょう。
こちらはVuguが提供しているsimplehttpを利用しており、hotreloadとはいきませんが、vuguファイルを書きなおすたびにその変更が反映されます。(ブラウザのリロードは手動)

server.go
// +build !wasm

package main

//go:generate vugugen .

import (
    "flag"
    "log"
    "net/http"
    "os"
    "path/filepath"

    "github.com/vugu/vugu/simplehttp"
)

func main() {
    dev := flag.Bool("dev", false, "Enable development features")
    dir := flag.String("dir", ".", "Project directory")
    httpl := flag.String("http", "127.0.0.1:8877", "Listen for HTTP on this host:port")
    flag.Parse()
    wd, _ := filepath.Abs(*dir)
    os.Chdir(wd)
    log.Printf("Starting HTTP Server at %q", *httpl)
    h := simplehttp.New(wd, *dev)
    log.Fatal(http.ListenAndServe(*httpl, h))
}

上記のコードは、Vugu公式のドキュメント内のソースコードを利用しています。

さて、準備完了です!
以下のコマンドを打ってみてください。

go run server.go -dev

http://localhost:8877 にてVuguがWebAssemblyを利用して動いていることを確認できるでしょう。

実際にserver.goを利用して、Vuguのアプリケーションが動いていることを確認できました。
できれば、staticなファイルとしてビルドできると嬉しいですよね?
以下のようなdist.goを作成し、distディレクトリにビルドしましょう!

dist.go
// +build ignore

package main

import (
    "flag"
    "fmt"
    "log"
    "net/http"
    "os"
    "os/exec"
    "path/filepath"
    "text/template"
    "time"

    "github.com/vugu/vugu/distutil"
    "github.com/vugu/vugu/simplehttp"
)

func main() {

    clean := flag.Bool("clean", false, "Remove dist dir before starting")
    dist := flag.String("dist", "dist", "Directory to put distribution files in")
    flag.Parse()

    start := time.Now()

    if *clean {
        os.RemoveAll(*dist)
    }

    os.MkdirAll(*dist, 0755) // create dist dir if not there

    // copy static files
    distutil.MustCopyDirFiltered(".", *dist, nil)

    // find and copy wasm_exec.js
    distutil.MustCopyFile(distutil.MustWasmExecJsPath(), filepath.Join(*dist, "wasm_exec.js"))

    // check for vugugen and go get if not there
    if _, err := exec.LookPath("vugugen"); err != nil {
        fmt.Print(distutil.MustExec("go", "get", "github.com/vugu/vugu/cmd/vugugen"))
    }

    // run go generate
    fmt.Print(distutil.MustExec("go", "generate", "."))

    // run go build for wasm binary
    fmt.Print(distutil.MustEnvExec([]string{"GOOS=js", "GOARCH=wasm"}, "go", "build", "-o", filepath.Join(*dist, "main.wasm"), "."))

    // STATIC INDEX FILE:
    // if you are hosting with a static file server or CDN, you can write out the default index.html from simplehttp
    req, _ := http.NewRequest("GET", "/index.html", nil)
    outf, err := os.OpenFile(filepath.Join(*dist, "index.html"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
    distutil.Must(err)
    defer outf.Close()
    template.Must(template.New("_page_").Parse(simplehttp.DefaultPageTemplateSource)).Execute(outf, map[string]interface{}{"Request": req})

    // BUILD GO SERVER:
    // or if you are deploying a Go server (yay!) you can build that binary here
    // fmt.Print(distutil.MustExec("go", "build", "-o", filepath.Join(*dist, "server"), "."))

    log.Printf("dist.go complete in %v", time.Since(start))
}

上記のコードもVugu公式のドキュメント内のソースコードを利用しています。

以下のコマンドを打ってみてください。

go run dist.go

新しくdistディレクトリが作成され、index.htmlmain.wasmwasm_exec.jsが作られていることが確認できます。

以上で開発は終了です!
お疲れ様でした。

Vueと比較してVuguでやりにくいこと

筆者がVuguを使ってみて、やりにくいと感じたことを書きます。
なお、調べきれていないと思うので、何か間違いなどあればご指摘ください。

  • propsを一旦dataにいれなければならない
  • エディタのSyntaxHighlightが存在しない(はず)
  • ライフサイクルに対応した関数がほとんど存在しない(こちらのPRによれば、BeforeBuild()というライフサイクルは現在新しいバージョンで実装されているらしい。例えばコンポーネントがunmountするときのようなライフサイクルイベントは現状存在しなそう。)

最後に

Vuguを利用して、簡単にWebAssemblyのアプリケーションを書くことができました。
WebAssemblyは敷居の高いものだと思われていますが、基本的なVueとGoの知識さえあれば何も抵抗なく開発を行えるということがわかりました。
最後まで本記事を読んでいただきありがとうございました。

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

【Go】S3のファイルを取得する方法

環境

  • Go - 1.13

事前準備

aws-sdk-goパッケージを使用するため、go getする。

$ go get -u github.com/aws/aws-sdk-go

サンプルコード

package main

import (
    "bytes"
    "fmt"

    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/s3"
)

func main() {
    bucket := "bucket"     // バケット名
    path := "path/to/file" // ファイルパス

    /*
     * アクセスキーID・シークレットアクセスキーを直接指定する場合
     */
    // svc := s3.New(session.New(), &aws.Config{
    //  Region: aws.String("ap-northeast-1"),
    //  Credentials: credentials.NewStaticCredentialsFromCreds(credentials.Value{
    //      // NOTE: GitHub等で公開する場合は環境変数に入れましょう
    //      AccessKeyID:     "xxxxxxxxxxxx",
    //      SecretAccessKey: "xxxxxxxxxxxx",
    //  }),
    // })

    /*
     * ~.aws/credentialsに認証情報が設定済の場合、
     * または、環境変数`AWS_ACCESS_KEY_ID`と`AWS_SECRET_ACCESS_KEY`に値が設定されている場合は、
     * ここではアクセスキーID・シークレットアクセスキーを指定しなくても大丈夫
     */
    svc := s3.New(session.New(), &aws.Config{
        Region: aws.String("ap-northeast-1"),
    })

    obj, _ := svc.GetObject(&s3.GetObjectInput{
        Bucket: aws.String(bucket),
        Key:    aws.String(path),
    })
    defer obj.Body.Close()

    buf := new(bytes.Buffer)
    buf.ReadFrom(obj.Body)
    rslt := buf.String()

    fmt.Println(rslt)
    // => "ファイルの内容"
}

参考

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

脱初心者Gopherのための言語仕様Tips

はじめに

Go歴=GOPATHを通してからの年月(独断と偏見による定義)はおよそ1年半になった。
GoはRubyの次に学習したい言語、Googleが開発しているモダンな言語(2009年に設計された。Go歴が10年を超えたとかいってる輩がいたら鼻で笑ってあげましょう)、MercariやCyberAgentなど国内のメガベンチャーを筆頭に使われている言語であったりと、1年半前は非常に話題性に富む言語だった。
一方で、GoでHello Worldをすることは以下の5行で済むが、Effective Goを初めから読むには難易度が高かったり、Tour of goはやってみたがしっくりこない感じがする人も多いのではないか。特に初心者のフェーズの次の段階で知りたい知識を体系的にまとめた文献はない気がする。

package main

import "fmt"

func main() {
    fmt.Println("Hello World")
}

自分自身は、実務でコードを書きながらレビューをされることで知見を得て、Goの書き方を理解してきた。
また、プログラミングを始めてからおよそ1年半の間にRuby,Python,Java,Node×Typescriptを書いてきたが、他言語と比較をすることでもGo独自の書き方や思想について考えてきた。
今回は、主に僕が約1年のGoでの実務の中でレビューされたことを中心に、初心者を抜けて自分的には動くコードだがGoの先輩は許してくれない、レビュー時に指摘されるであろう点についてまとめた。
まだまだ完全に地雷を踏み切ってはいないと思っているので、指摘点があれば編集リクエストをお願いします。

Goroutine

Goを語る上で、外せないのがGoroutineである。
Goの良さを聞かれた時に、並行処理が書きやすいと非Gopherエンジニアに言っていたが、ここ最近まであまり理解していなかった。
CyberAgentのインターンに行った際に、Goroutineを意地でも書かないといけない機会に遭遇し、メンターさんに教えてもらいながら理解を深めていった。
そもそも並行処理と並列処理の違いもわかっていなかったので、詳細をまとめた記事が以下
並行処理と並列処理の違い

基本的にコードは上から下に向かってコードが実行される。
しかし、各行での実行にかかる時間には全く異なる。
例えば、

num++
client.DB.CreateUser(user)

変数の加算とDBへのデータの永続化にかかる時間が異なるのは明確だろう。

例えば、DBへの処理が複数ある場合、各々の処理が完了するまでコードの実行が止まってしまうと、全体としてのコードの実行時間は長くなってしまう。
独立した永続化の処理を並行して行うことができれば、実行時間を短縮できる。
一方で、同じメモリを複数のプロセスで扱うには、デッドロックや値の同期に関しての問題が発生する。

Goでは、書き方さえミスしなければ、並行処理が比較的容易に書けて、並列に処理が実行されることが期待できる。

単純な並行処理のコードサンプル

func main() {
    // 無名関数。又の名をクロージャ
    go func() {
        time.Sleep(3 * time.Second)
        fmt.Println("実行おわた!!")
    }()
    fmt.Println("実行はよ")
    time.Sleep(10 * time.Second)
}
実行結果
実行はよ
実行おわた!!

実行される順番
無名関数の実行開始

実行はよ出力

time.Sleep(10 * time.Second)実行開始

実行おわた!!出力

無名関数の実行終了

time.Sleep(10 * time.Second)実行終了

main関数終了
である。まあ、有名な話だ。

今回は一つのGroutineだが、複数のGoroutineを思うがままに使いたい時がある。
週2ぐらいである。
その際にどうしたら良いか。
実行中のGoroutineの数をカウントして、全てのGoroutineの実行が終了したら次のコードに進めるようにしたい。
そんな時は、"sync" パッケージのwaitGroupを使う。

func main() {
    wg := &sync.WaitGroup{}
    for i := 0; i < 10; i++ { 
        wg.Add(1)  // wgをインクリメント Goroutineの動かす前にするのが大事
        go func() {
            fmt.Println(i)
            wg.Done()  // wgをデクリメント
        }()
    }
    wg.Wait() //  wgがゼロになるまで待つ。
}

余談

余談なのだが
上のコードではiの値を使ってないから良いが、
無名関数の中でiを出力したら面白いことがおきる。

func main() {
    wg := &sync.WaitGroup{}
    for i := 0; i < 10; i++ { 
        wg.Add(1)  // wgをインクリメント Goroutineの動かす前にするのが大事
        go func() {
            fmt.Println(i)
            wg.Done()  // wgをデクリメント
        }()
    }
    wg.Wait() //  wgがゼロになるまで待つ。
}
./prog.go:19:16: loop variable i captured by func literal
Go vet exited.

10
10
10
10
10
10
10
10
10
10

出力したい値が全部10になってしまう。

なぜこのような結果になるかというと、goroutineの実行より先に、forのループが回ってしまうからだ。
対応策は何個かあるが、自分が一番綺麗だと思うのが以下。

func main() {
    wg := &sync.WaitGroup{}
    for i := 0; i < 10; i++ { 
        wg.Add(1)  // wgをインクリメント Goroutineを動かす前にするのが大事
        go func(i int) {
            fmt.Println(i)
            wg.Done()  // wgをデクリメント
        }(i)
    }
    wg.Wait() //  wgがゼロになるまで待つ。
}
実行結果
9
0
1
2
3
4
5
6
7
8

無名関数に引数を追加することで、forのループごとのiで無名関数を実行できるようにしたことで、実行より先にforのloopが終わることを防ぐことができる。
1~9の順番にならないのは、Goroutineの処理が完了する順番はこちらでユーザ側で制御できないからお愛想。特に今回は問題ないので。

関連し合う複数の処理単位で、Waitすることで、独立した処理の実行を並行に書くことができる。
他の関数でも使われるデータを扱う関数を上に書くべきである。

var user *User
var room *Room
var message *Message

user = getUser()
wg := &sync.WaitGroup{}
wg.Add(1)
go func() {
    defer wg.Done()
    room = getRoomFromUserID(user.ID)
}
wg.Add(1)
go func() {
    defer wg.Done()
    message = getMessageFromRoom(room.ID)
}
wg.Wait() 
UserRoomUpdate(user, room, message) // ユーザとルームとメッセージを使う処理

syncMapや、デッドロックについての知見も共有したいが、長くなりそうなので別の機会に記事を書こうと思う。

引数

before

func hoge(hoge string, fuga string, num int) error

after

func hoge(hoge, fuga string, num int) error

確かに。こっちの方がわかりやすい。(これはなんとかしてPRのレビューで先輩のアラを探そうとした時に、「引数名抜けてまっせ」と指摘したが返り討ちにあった苦い思い出。ダサい。)

before

// これ以降も使う構造体
type User struct {
    ID string
}

interface (
    func hoge(ctx context.Context, user *User) error   
)

after

interface (
    func hoge(context.Context, *User) error   
)

インターフェイス内の引数がプリミティブな型(intとかstringとか)を含まなかったら、引数名を省略しても良い。
命名に関して追加の情報はないので。

戻り値

before

func hoge() error {
    _, err := fuga()
    if err != nil {
        return 
    }
}

after

func hoge() (err error) {
    _, err = fuga()
    if err != nil {
        return 
    }
}

戻り値を命名しないこともできるが、命名することで
return err→ return
だけで書くことでき、関数内のerrハンドリングが多くなった時にコード量が減って嬉しい。

余談

Effective Go曰く
関数の戻り値が複数ってのはGo言語の特徴で、使われ方としては正常値とエラーが返されることが多い。
最近、TypeScript×Nodeでコードを書く機会があり、try-throw-catchを書くことがあったが、Goには例外機構がない。多値返却の理由と例外機構がないことは密接に関わり合っている。

↓の記事がなぜGoにtry-throw-catchがないのか、Go言語の仕様を元にかなり詳細に記載してあるので、興味がある人はぜひ読んで欲しい。
「例外」がないからGo言語はイケてないとかって言ってるヤツが本当にイケてない件

スライス

プログラミングを初心者を抜け出したら、メモリ領域とか気にしなくてはならない。
扱うデータ量が多くなればなるほど、使用するメモリは想像以上に大きくなる。
以下は自分が踏んだ地雷。

type AdminUser struct {
    ID int
    UserID string
}
func (users []*User) {
    // サンプル用の意味のない処理
    au := make([]*AdminUser, 0) // 容量を0にしてsliceを定義
    for i, user := range {
        au = append(au, &AdminUser{
            ID: i, // 急にIDがintで謎だがスルーしてくれ
            UserID: user.ID,
        })
    }
    fmt.Println(au)
}

おわかりいただけただろうか。
goではarrayとsliceが存在し、arrayの容量は固定で、sliceの容量は動的だ。
sliceには、要素数と容量が存在し、要素数はsliceに実際に入っている値の個数で、容量は確保できる要素数である。
image.png

上のコードでは、makeを行っているが、この際に容量のパラメータを設定していない。その場合にどうなるかと言うとauの容量は初めてのappendが行われるまで0になっている状態だ。
容量が0のsliceに要素を一つ追加するので、容量を超えたappendになる。
容量を確保するために、auを別のメモリにコピーすることを容量を超えたappendが行われ度に繰り返される。
容量が2以上になると、既存の容量の倍の容量を確保するようにgoは書かれているぽい。
一応以下が確認のためのコードである。

func main() {
    sl := make([]string, 0)
    fmt.Println(cap(sl)) // capでsliceの容量がわかる
    sl = append(sl, "1")
    fmt.Println(cap(sl))
    sl = append(sl, "2")
    fmt.Println(cap(sl))
    sl = append(sl, "3")
    fmt.Println(cap(sl))
    sl = append(sl, "4")
    fmt.Println(cap(sl))
    sl = append(sl, "5")
    fmt.Println(cap(sl))
}
実行結果
0
1
2
4
4
8

何がいいたいかというと、sliceは初期化のタイミングで長さがわかっていたら、容量を書きましょうということ。
これによって、コピーにかかる時間とメモリ量が少なくなり実行が早くなる。
自分自身そこまで、メモリ量は大切ではないと思っていたが、pythonのpandasのDataFrameに数十万件のデータをappendする際に、書き方を間違えて変数がコピーされまくりGCの回収が間に合わなくなりループの実行が絶望的に遅くなってしまった?ので、メモリ量を気にするようにしている。
GoのGCについてMediumに簡単にまとめたので、よかったらこちらもどうぞ。
GoのGCについて

sliceの容量を確保したコードは以下の通り!!!

func (users []*User) {
    au := make([]*AdminUser, 0, len(users)) // 要素数を0、容量をusersの要素数に!
    ...
}

マップ

ヌルポは恥。しかし実行して見ないとわからないものである。
Goのmapでは、空のkeyに値をいれる可能性があるコードでもbuildが通ってしまう。

サンプルコード

var hogeMap map[int]string

func hoge() {
    hogeMap[1] = "hoge"
    fmt.Println(hogeMap[1])
}
実行結果
panic: assignment to entry in nil map

mapでのruntimeエラーの予防策は以下である。

var hogeMap map[int]string

func hoge() {
    if _, ok := hogeMap[1]; ok {
        hogeMap[1] = "hoge"
        fmt.Println(hogeMap[1])
    }
}

nilチェックを代入前にすることでruntimeエラーを防ぐことができる。
上記のようなシンプルなコードだと、nilチェックをしないとという気持ちになるのだが、複雑なコードになってくると忘れてしまうこともあるので、気を付けないといけぬ。

ポインタ

引数に構造体を含める場合、以下の書き方が使われる。
書き方①

func hoge(user User) error {}

書き方②

func hoge(user *User) error {}

書き方①②の違いは、Userに*がつくかつかないかである。
①の場合は、Userの値が引数で渡される(値渡し)
②の場合は、Userのメモリのアドレスが引数で渡される(ポインタ渡し)
主な違いは、①の場合は、引数の値を関数内で変更した場合に、呼び出し元の関数でのUserの値は変わることはないが、②の場合は、引数の値を関数内で変更した場合に、呼び出し元の関数でのUserの値は変わる。
以下例である。

書き方①

type User struct {
    ID string
}

func hoge() {
    user := User{
        ID: "id", 
    }
    fuga(user)
    fmt.Println(user)
}

func fuga(user User) {
    user.ID = "idddd"
}

実行結果
&{id} 

書き方②

type User struct {
    ID string
}

func hoge() {
    user := &User{
        ID: "id", 
    }
    fuga(user)
    fmt.Println(user)
}

func fuga(user *User) {
    user.ID = "idddd"
}
実行結果
&{idddd}

経験則だが、構造体には特別な理由がない限り、ポインタ渡しで書くべきだと思う。
(理由があり、納得させられる自信があるならok)

構造体とメソッド

GoにはRubyやJavaなどのオブジェクト指向言語とは異なりクラスが予約語にはない。
しかし、構造体とメソッドに関係性を持たせることはできる。

type User struct {
    ID string
    Name string
}

func (u *User) Name() string {
    return u.Name + "さん"
}

関数名の前に紐付けたい構造体を書くことで、構造体にフィールドにアクセスすることができる。

まとめ

かなり長くなってしまったが、PRで指摘された点から始まり、Goの書き方について、1年前の自分に教えたいことを詰め込んだ。
かなり厳選しているので、まだまだ書きたいことはあるが、これくらいにしておこうと思う。
まだまだ絶賛参加受付中なので、気になった方はぜひ参加して欲しい。
千葉大学 Advent Calender

参考

https://qiita.com/Maki-Daisuke/items/80cbc26ca43cca3de4e4
https://qiita.com/tenntenn/items/e04441a40aeb9c31dbaf
https://qiita.com/sudix/items/67d4cad08fe88dcb9a6dhttps://golang.org/doc/effective_go.html
https://qiita.com/ruiu/items/dba58f7b03a9a2ffad65

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

TUI版インベーダーゲームをGo言語で作る

この記事は千 Advent Calendar 2019の4日目の記事です。

はじめに

GoのTUIツールに興味が沸いたので、いろいろ調べている時にブロック崩しを見つけました。
これがシンプルな作りで理解しやすかったので、参考にさせてもらって別のゲームを作ってみた!
という記事です。
termbox-goというライブラリの解説はブロック崩しを見てもらった方がいいです ^^;
注)インベーダーゲームってタイトルにしてますがルールは全く違うもどきです。
  ちゃんと再現したGo実装は、こことか見た方がいいです ∑(゚Д゚) 完成度すごい

どんなゲーム?

特徴は、統率がとれていない&武器がないので体当たりしかできない侵略者w、を撃ち落とす
インベーダーゲームです
↓こんな感じです

fvahq-pmxqk.gif

  • ルール
    • インベーダーに3回当たるとゲームオーバー
      • 体当たりしてくるので十字キー(←,→)で避けてください
    • インベーダーに3発当てると撃墜
      • スペースキーで弾を発射
      • 1発当たる度にインベーダーの色が変わります
      • 弾を連続で当てるとコンボポイントでスコアが伸びます
    • 全てのインベーダーを撃墜すればゲームクリア
    • ESCキーでゲーム終了

環境

開発環境

  • Go v1.13
  • macOS
    • Terminal.appよりiTerm2(&標準フォントMonaco)の方がキレイに表示されます

動作環境

  • OSはmacOS,Linux,Windowsで確認済み
  • 等幅フォント
    • Windowsは、標準設定のコマンドプロンプトだと表示崩れます。
      いくつかの等幅フォント試したけどやっぱり崩れます(誰か崩れない設定探してw)
      Windows10で表示が崩れないことを確認しました!
      (@mattn さんにPR頂きました。ありがとうございます!)

実行方法

git clone https://github.com/miyaz/invaders.git
cd invaders
go run main.go

OR
こちらからバイナリダウンロードして実行

開発の思い出

開発過程でこんなんあったなぁということを思い出しながらやったことを説明します。
なお、それぞれのフェーズは下記のように実行できます。

git checkout phase1  # phase1から6まであります
go run main.go

フェーズ1

ブロック崩し...フェーズ1差分

ブロック要素で作るインベーダー

マルチOS対応を考えるとターミナルで画像を使うのは難しいので、ブロック要素のみを使ってインベーダーを作りました。
termboxでセルを描画するSetCell関数は、1文字ずつ座標(x,y)と文字(rune型)を渡す必要があります。
キャラクター文字列を改行で区切り、rune配列に変換して1文字ずつSetCellで描画しています。

該当コード
//インベーダーを描画
func drawInvader(x, y int) {
  invader := `
 ▚▄▄▞
▟█▟▙█▙
▘▝▖▗▘▝
`
  scanner := bufio.NewScanner(strings.NewReader(invader))
  j := 0
  for scanner.Scan() {
    line := scanner.Text()
    runes := []rune(line)
    for i := 0; i < len(runes); i++ {
      termbox.SetCell(x+i-3, y+j-2, runes[i], termbox.ColorDefault, termbox.ColorDefault)
    }
    j++
  }
}

フェーズ2

フェーズ1...2差分

一定周期で動作させるためのタイマーをSleepからTickerに変更

複数インベーダーをgoroutineで登場させてみました↓ うじゃうじゃいるw
1ujbm-tiwg6.gif

ブロック崩しはボールの移動をtime.Sleepを使って一定周期で動くように実装されていました。
今回はインベーダを撃ち落とすというゲームにしたかったので、goroutineにchannelで通知してリアルタイムに
処理ができるようにtickerを使ったタイマーにし、for-select-caseを使いました。

変更差分
 //タイマーイベント
-func invaderTimerLoop(itch chan bool) {
+func moveLoop(moveCh chan int, mover, ticker int) {
+       t := time.NewTicker(time.Duration(ticker) * time.Millisecond)
        for {
-               itch <- true
-               time.Sleep(time.Duration(_invaderTimeSpan) * time.Millisecond)
+               select {
+               case <-t.C: //タイマーイベント
+                       moveCh <- mover
+                       break
+               }
        }
+       t.Stop()
 }

フェーズ3

フェーズ2...3差分

インベーダー同士の衝突で跳ね返る

インベーダーオブジェクトを配列で持たせて、個々のインベーダーを配列のインデックスで識別するようにしました。
衝突判定関数に配列のインデックスを渡し、そのインベーダーと座標が重なった物体(壁、他のインベーダー、戦闘機)
がないかをチェックし、あれば方向を反転させます。

9fsql-t60ax.gif

差分
 //衝突判定
-func checkCollision(st state) state {
+func checkCollision(st state, i int) state {
+   //左の壁
+   if st.Invaders[i].Pos.X <= 0 {
+       st.Invaders[i].Pos.X = 1
+       st.Invaders[i].Vec.X = 1
以降判定処理が続く

フェーズ4

フェーズ3...4差分

弾を発射してインベーダーを撃墜

スペースキーで戦闘機から弾を発射できるようにしました。
(ちなみに戦闘機のデザインは娘(小4)にやってもらいました!)

弾の発射ごとにgoroutineを生成して、それぞれがtickerで刻み、それをcontroller側でchannel経由で
通知を受け取ってY座標をインクリメントし描画する=前に飛んでいく、という作りです。
(言葉で書くとわかりづらいな)

ここで、撃墜されたインベーダーを消す場合に配列だと扱いづらいのでmapに変更しました。
変更前) []invader
変更後) map[int]*invader

配列(正確にはスライス)の要素を消すのは下記のような関数でやればいいですが、インデックスがズレます(当然
配列インデックスでインベーダーを識別しているとバグになりやすい、というのがmapにする理由です。

配列要素削除関数の例
func remove(s []invader, i int) []invader {
  s = s[:i+copy(s[i:], s[i+1:])]
  return s
}

また、mapを使えばキーを指定してGo標準のdelete関数で消せるので簡単。
指定キーに紐づいたインベーダーを削除、ということができるのでコードもシンプルになるし、キーも連番ではなく
自由に作れるのもメリットですね。

フェーズ5

フェーズ4...5差分

消えた弾丸goroutineをちゃんとクローズする

フェーズ4では、弾をうつほどCPU負荷が増えていきました。
インベーダーの体当たり以外にPCがフリーズするかも、という緊張感も楽しめるゲームに仕上がりました!(違う

原因は簡単で、弾の発射ごとに生成されるgoroutineが、外れる(画面外)or命中して消えた後も動き続けていました。
使い終わったgoroutineは止めてあげる必要があります(お作法

対処としては、弾(bullet)というstructにCloseCh chan boolを持たせて、生成したgoroutineにこのCloseChを
渡してcloseしたことを外から通知できるようにしました。
弾が外れる(画面外)or命中して消える際にgoroutineを止めるには、close(CloseCh) を呼び出せばいいだけです。

 //タイマーイベント
-func moveLoop(moveCh chan int, mover, ticker int) {
+func moveLoop(moveCh chan int, closeCh chan bool, mover, ticker int) {
    t := time.NewTicker(time.Duration(ticker) * time.Millisecond)
+   defer t.Stop()
    for {
        select {
        case <-t.C: //タイマーイベント
            moveCh <- mover
            break
+       case <-closeCh:
+           return
        }
     }
-    t.Stop()
 }

フェーズ6(完成)

フェーズ5...6差分

ゲームとして整えた

  • 画面上部にスコアやライフを表示してゲームっぽく
    • おまけで、goroutine数をリアルタイムで表示するNumGoroutine欄をつけました
  • インベーダーは3回命中で撃墜に変更(難易度・ゲーム性UP

おわりに

TUIでも動きのあるアプリが簡単に作れるtermbox-go、使いやすい!!と思いました。
また、非同期でタイマー/キーイベントで動いている物体(インベーダー、弾丸,戦闘機)同士の衝突判定や処理が
簡単にできるのも並列処理を書きやすいGoだからこそだな、と実感しました。

最後に、実はこのゲームにはバグがあって簡単に最高得点をとることができます。
興味があれば探してみてください!

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

GoでIteratorパターンを実装する

概要

デザインパターンの1つであるIteratorパターンをGoで実装してみます。

Iteratorパターンとは

Iteratorパターンは、任意のオブジェクトを集約して列挙しながら参照する時によく用いられるデザインパターンです。

forで回せないような複雑なリストを列挙したい場合などに、列挙処理を内部に隠蔽して順次に参照できるようになる利点があります。

Goでイテレーターが使われている処理

例えばdatabase/sqlの内部実装でイテレーターが使われています。
sql.Drivers.Rowsにイテレーターが実装されていて、上流レイヤであるsql.Rowsでmutexなどを使って処理をラップしているようです。

sql.Rows
https://golang.org/src/database/sql/sql.go?s=75693:76415#L2672

sql.Drivers.Rows
https://golang.org/src/database/sql/sql.go?s=75693:76415#L2672

実装例

Iteratorパターンでは、以下の概念のオブジェクトを実装します。

  • Items(アイテムの集合体)
  • Item(アイテム単体)
  • Iterator

一般的にIteratorは以下の2つのメソッドを持ちます。

  • HasNext() -> 次の要素が存在するか。
  • Next() -> インデックスをインクリメントする。

さて、以上のことを踏まえてここからは音楽プレーヤを例に解説します。
プレイリストをMusics、中身の単体のアイテムをMusicとしつつ、
Musicsを管理するイテレータ構造体を作成して、Musicsの中に処理を隠蔽する構成で作っていこうと思います。

まずはMusicの定義。

package main

type Music struct {
    Id     int64
    Title  string
    Artist string
}

func (m Music) String() string {
    return fmt.Sprintf("ID: %d, Title: %s, Artist: %s", m.Id, m.Title, m.Artist)
}

次に、Musicの集合体を定義する前にMusicsが保持するIteratorを先に作成します。

type MusicsIterator struct {
        // MusicのAggregate
    Musics *Musics

        // 現在のインデックス
    Idx    int64
}

func (m *MusicsIterator) HasNext() bool {
    if m.Idx < m.Musics.GetSize() {
        return true
    }
    return false
}

func (m *MusicsIterator) Next() *Music {
    item := m.Musics.GetItemAt(m.Idx)
    m.Idx += 1
    return item
}

最後に、Musicの具象AggragateクラスであるMusicsを定義します。

type Musics struct {
        // 外に解放しない
    items    []*Music
    iterator *MusicsIterator
}

func NewMusics() *Musics {
    m := &Musics{
        iterator: &MusicsIterator{},
    }
    m.iterator.Musics = m
    return m
}

func (m *Musics) GetItemAt(idx int64) *Music {
    if m.GetSize() <= idx {
        return nil
    }
    return m.items[idx]
}

// AggregatorのアクションとしてIterationさせたいので
// Iteratorの処理をラップ。
func (m *Musics) HasNext() bool {
    return m.iterator.HasNext()
}

// ここも同じ
func (m *Musics) Next() *Music {
    return m.iterator.Next()
}

// わかりやすくするためにsliceにappendしていますが
// リストの場合などはポインタで連結して再代入。
func (m *Musics) Append(music *Music) {
    m.items = append(m.items, music)
}

// ここもわかりやすくするために単純にlen(xx)で済ませていますが
// リストの場合などはノードをたどる処理が入ったりするかも。
func (m *Musics) GetSize() int64 {
    return int64(len(m.items))
}

さて、準備が整ったので実際にイテレーションの動作を確かめてみましょう。

package main

import (
    "fmt"
)

func main() {
        // 要素をappendして出力するだけ。
    musics := NewMusics()
    musics.Append(&Music{
        Id:     1,
        Title:  "MusicA",
        Artist: "ArtistA",
    })
    musics.Append(&Music{
        Id:     2,
        Title:  "MusicB",
        Artist: "ArtistB",
    })

    for musics.HasNext() {
        m := musics.Next()
        fmt.Println(m)
    }
}

はい、見事に集約された構造体の走査処理を隠蔽することができました。
出力はこのようになるはず。

ID: 1, Title: MusicA, Artist: ArtistA
ID: 2, Title: MusicB, Artist: ArtistB

実際に書いたコードは https://github.com/OdaDaisuke/go-algorithm/blob/master/dp/iterator/main.go

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

golangのAtCoder向けデバック方法

golangでAtCoder(競技プログラミング)を始めたところVSCodeでのデバッグでつまづいたのでメモ.

TL;DR

  • VSCodeのDebug機能では標準入力を受け付けられない
  • testファイル内でos.Stdinをスタブし, bufio.NewScannerを再定義して解決した

Sample Code

回答用コード.
globalでscannerを定義している.

main.go
package main

import (
    "bufio"
    "fmt"
    "os"
)

var scanner = bufio.NewScanner(os.Stdin)

func nextLine() string {
    scanner.Scan()
    return scanner.Text()
}

func main() {
    N := nextLine()
    fmt.Println(N)
}

テストコード.
stubしたStdinを使ってscannerを上書きし, main()を実行.

main_test.go
package main

import (
    "bufio"
    "io/ioutil"
    "os"
    "testing"
)

func TestAnswer(t *testing.T) {
    inbuf := readFile("./stdin.txt")
    stubStdin(inbuf, func() {
        main()
    })
}

func stubStdin(inbuf string, fn func()) {
    // stub os.Stdin with os.Pipe
    inr, inw, _ := os.Pipe()
    inw.Write([]byte(inbuf))
    inw.Close()
    os.Stdin = inr
    // overwrite scanner
    scanner = bufio.NewScanner(os.Stdin)
    fn()
}

func readFile(fileName string) string {
    bytes, err := ioutil.ReadFile(fileName)
    if err != nil {
        panic(err)
    }
    return string(bytes)
}

./stdin.txtにテスト用の入力を書く形にした.

参考

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

GolangでDBアクセスがあるユニットテストのやり方を考える

こんにちは、こちらはLinc'wellアドベントカレンダーの1日目です。

皆さんはDBに対して書き込みが発生する関数のテストをどのように行われているでしょうか?
私はgolang初心者どころかサーバサイド初心者なので、最適解が全くもって分からなかったのですが、いろいろ調べた末「こんな感じで用意すると良さそう」というテスト環境・書き方に落ち着いたのでそれをここに記します。

同じ悩みを持った方に判断基準を提供できるような記事になっていると嬉しいです。(あわよくば強い人からのフィードバック待ってます)

はじめに: どんなテストを書きたいのか

はじめに要件をクリアにするためにどんなテストを書きたいのかの具体例を書きます。

ただ今私はECサイトの開発をしていて、その中で定期購入の仕組みを作っています。
定期購入はユーザが定期購入を申し込んだ際に「次にどの日付にどんな商品を購入するのか」というデータを持った Subscription というデータを作り、日次でその日の日付が指定された定期購入に応じて注文を作るバッチ処理を行う、という仕組みで作ります。

なので、テストの擬似コードを書くとこんな感じです。

func TestCreateSubscriptionOrders(t *testing.T) {
    // Given: 今日発送予定のSubscriptionがある

    // When: Batchの関数を実行する

    // Then: そのSubscriptionに応じたOrderが作られる
}

今回テストを考えたいのはこのように「関数を実行し」「その関数を実行したことによってDBのデータが正しく変わったこと」を確認したいケースです。なので、関数に対してテストコード内から参照できるDBインスタンスを渡してあげる必要があります。

このため、この記事では次の3つの事柄について考えていきます。

  1. DBインスタンスをどう用意するか
  2. テストデータをどう用意するか
    • 上記例のようにテストを実行するために前提として特定のテストデータが多々あるでしょう。これをどう用意すると良さそうかを考えます
  3. テストデータのクリーンナップをどうやって行うか
    • ユニットテストで前提条件やテスト対象の関数内でINSERTされたデータは他のテストに影響を与えないよう消しておきたいです
    • これをどのように行うかを考えます

DBインスタンスをどう用意するか

大方針としては mock を用意するかテスト用にDBを立てるかのどっちかになると思います。

mock

試しに go-sqlmock のサンプルコードを見てみます。

GitHub - DATA-DOG/go-sqlmock: Sql mock driver for golang to test database interactions

func TestShouldUpdateStats(t *testing.T) {
    db, mock, err := sqlmock.New()
    if err != nil {
        t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
    }
    defer db.Close()

    mock.ExpectBegin()
    mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1))
    mock.ExpectExec("INSERT INTO product_viewers").WithArgs(2, 3).WillReturnResult(sqlmock.NewResult(1, 1))
    mock.ExpectCommit()

    // now we execute our method
    if err = recordStats(db, 2, 3); err != nil {
        t.Errorf("error was not expected while updating stats: %s", err)
    }

    // we make sure that all expectations were met
    if err := mock.ExpectationsWereMet(); err != nil {
        t.Errorf("there were unfulfilled expectations: %s", err)
    }
}

mock では mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1))
のように「こういうSQLが来た時はこういう結果を返す」という指定の仕方ができます。
要するに「期待するSQLが来たこと」は確認できますが「想定した値がDBから返ってくること」は担保できません。
個人的にはこれだけで今回の要件には合わないかなと感じました。(あと弊社のケースで言うと、sqlboilerというORマッパーを使っていて、それのSQLを再現するのがめんどくさいと言う事情もありました。ちゃんと探せば生SQL発行するメソッドとかあるのでかもしれないですが)

test用のDBを立てる

次にテスト用のDBを立てる方法について、色々あるとは思いますが、探したところ次のどっちかっぽいです。

  1. 開発用のものと同じコンテナ内にテスト用のDBを作る
  2. dockertestを使って専用のイメージ作る

今の自分たちの開発環境だとすでに開発用の postgres プロセスがあって、そこにテスト用のDB作っちゃうので簡単に済ませられそうだったのでそうすることにしました。ちょっとユニットテストがDocker立てていることに一抹の気持ち悪さがないわけではないですが、開発中は基本的に常に動かしているものなので今のところ困っていないです。

CI環境ではどうしているのか

ただ今GitHub Actionsを使っているのでそれを使っての例になりますが、CI環境ではdockerも使わないで postgres立てて migration (goose)を流すというのをやっています。

name: ci-backend
on: [push]
jobs:
  build:
    name: setup
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:10
        env:
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: postgres
        ports:
          - 5432:5432
        options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
    steps:
      - uses: actions/checkout@master
      - uses: actions/setup-go@v1
        with:
          go-version: '1.12'
      - name: setup bash_profile
        run: |
          echo 'export GOPATH="$HOME/go"' >>~/.bash_profile
          echo 'PATH="$GOPATH/bin:$PATH"' >>~/.bash_profile
      - name: run migration
        working-directory: server
        shell: bash -l {0}
        run: |
          go get -u github.com/pressly/goose/cmd/goose
          goose --dir=internal/db/migrate postgres "user=postgres port=5432 password=postgres host=localhost dbname=postgres sslmode=disable" up
      - name: test server
        working-directory: server
        env:
          DB_PORT: ${{ job.services.postgres.ports[5432] }}
          DB_USER: postgres
          DB_PASSWORD: postgres
          DB_HOST_TEST: localhost
          DB_NAME_TEST: postgres
        run: |
          go test ./...

テストデータの準備

考えたい観点は二つあって

  1. いかに簡単にテストデータを用意するか
  2. いかに事前に用意するテストデータが他のテストに影響しないようにするか

例えば冒頭の定期購入のケースについて考えてみると、「注文を作るためにアクティブな定期購入があること」を前提としています。
なので「注文が作られること」をテストしたい場合は前提条件である定期購入のデータを入れなければいけません。テストデータを用意するのは非常に詰まらない作業なのでなるべく簡略化したいところです。

ただ、ではグローバルな seed データのようなものに定期購入のデータを入れて最初に入れればいいと言うものでもなく、例えば上記以外に「定期購読がない場合に注文が作られないこと」もテストしたいとなった場合に不可能になってしまいます。(それかなんらかデータが入っていることを前提として「そのテストの前に定期購読のデータ消す」という余計な知識が入ってしまいます)

なので「テストデータを簡単に用意すること」と「テスト間で扱うデータが干渉しない」ために考えたいのは次の二つです

  • どのタイミングでデータをINSERTするのがいいか
  • テストデータを簡単に用意する実装の仕方

どのタイミングでデータをINSERTするのがいいか

ざっくり言うと事前に seed みたいなのを入れるか各ユニットテストで前提条件として入れるかです。(正確には二者択一というより、テスト固有のデータは登場するに決まっているので seedを用意するかどうかという問いの方が正しいですが)

今後は何かしら作るかもしれないですが一旦 seed のようなものは作らないことにしました。
理由としてはあるユニットテストがグローバルに作られたテストデータを前提としている時に、暗黙的な知識を持っているのがテスト自体の保守性下げてなんか気持ち悪いと感じたからです。そのテストだけ見ればそのテストの前提条件は何で見たいことは何か分かるようにしたいと感じました。なので多少面倒は増えるかもしれないですがきっちり一つ一つのユニットテストにデータの準備を書いていこうと思います。

ただ、いくつかテスト書いてみて思ったのは User みたいなどんなシステムでもコアとなる概念はもう全てのテストの前提条件にしてしまっても問題ないのではという気はしてきています。こういったものは漸進的に改善していきたいなと思います。

テストデータを簡単に用意する実装の仕方

月並みですがファクトリ関数を作っていこうと思います。
「思います」というのはこの記事執筆時点では大して書いてないので、何も書くことが無いということを指しています。頑張ります。

普通に自作で良さそうだけどこういうライブラリ使った方が楽なのかな
GitHub - bluele/factory-go: A test fixtures replacement inspired by factory_boy and factory_girl.

3.テストデータのクリーンナップをどうやって行うか

大きくは次の二つかなと思います。

  • テスト内の処理はトランザクションとして持っておいて、テストが終わったらロールバック。
  • 毎回データを消す

結論から述べると1個目の毎回トランザクション貼るようにします。理由はそちらの方が脳死で書けて、特にデメリットも思い当たらないからです。

具体的には次のような感じで書くようにします。

func TestCreateAppSubscriptionOrders(t *testing.T) {
  // トランザクションはる
    tx := db.GetTestTransaction()

    // テスト

  // 終わったらロールバック
    tx.Rollback()
}

めっちゃシンプルですね。シンプルですが毎回書くのもう一段楽にできないかと思ったので、beforeEach/afterEachした方がいいかというのも考えてみました。

なぜ beforeEach/afterEach したいのか

分解すると次の二つのモチベーションがあります。

  • うっかり書き忘れを無くしたい
    • ただ、これを実現したい場合は全パッケージのテストに対して適用するのが必要となるができないっぽいです(できるなら教えて欲しい)
    • そうでない場合、DBアクセスが必要なテストのパッケージ内で書くことになるが、それであればこの目的は満たせないので、このモチベーションはどう転んでも実現できない
  • 手間を減らしたい
    • 同一パッケージ内にかなりの数のDBを参照するテストがある場合

ただ、残念なことに golang 標準の testing パッケージには beforeEach/afterEach は存在しません。
実現しようとする場合は ginkgo などのテスティングフレームワークを使ったりする必要があるので、それを入れるほどのモチベーションではないかなーと思いました。

結論

頑張ってそれぞれのテストケース内でトランザクションの開始とRollbackを書こう。
ただ関数としては次のように書いておく。

package db

func GetTestTransaction() *sql.Tx {
    psqlInfo := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s",
        os.Getenv("DB_HOST_TEST"), os.Getenv("DB_PORT"), os.Getenv("DB_USER"), os.Getenv("DB_PASSWORD"), os.Getenv("DB_NAME_TEST"), os.Getenv("DB_SSL"))

    db, err := sql.Open("postgres", psqlInfo)

    if err != nil {
        panic(err.Error())
    }

    err = db.Ping()
    if err != nil {
        panic(err)
    }

    tx, _ := db.Begin()

    return tx
}

// 使う側
func TestCreateAppSubscriptionOrders(t *testing.T) {
  tx = db.GetTestTransaction()

  /**********
  テストの処理
  **********/

  tx.Rollback()
}

まとめ

私のプロジェクトでは次のように用意するようにしました。

  • DBインスタンスはそれ専用のコンテナを立ち上げる
  • テストデータはSeedは用意せずファクトリ関数のみで一旦行く
  • テストのクリーンナップはトランザクションを用いて行う

もちろんプロダクトの性質によって方針も変わるとは思いますが、一旦これで進んでいきます。
お読みいただきありがとうございました。

追記

早速フィードバックいただけて激しく感謝なのですが、テストケース毎に db create, migration などを行うことによってクリーンナップを実現するというのが良さそうです。
私のトランザクションを用いてやる方法は、全てのテストケースに対して一回だけDB立てることを前提とした時に一々テストに使ったデータを指定してDELETEするのが面倒だったからで、この方法なら脳死で書けるし「transactionが出来ていることを試すテスト」も他のテストと違う書き方せず実現できるのでいいなと感じました。

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

Go Benchmark Toolingで実行結果の可視化機能を利用する

はじめに

フューチャーAdvent Calendar 2019 の1日目です。

この記事ではGoのベンチマーク機能について書いていきます。

普段わたしは業務系アプリのサーバサイド開発でGoを利用しています。いままでの経験では性能的なボトルネックはGoで書いたアプリケーションロジック部分ではなく、外部のデータストア(RDBやらAWS DynamoDB)の問い合わせ部分であることが多かったのですが、何度か非常に低速なコードを書いてしまったことがあります1。そういった場合にも対応できるよう、コードのどの部分が処理負荷が高いのか切り分けるようにGoのベンチマーク機能を調査した結果を残していきます。

Go Benchmark 概要

Goにはtestingというパッケージやユーティリティツール(CPUなどのプロファイリングなど)が標準機能として提供されています。基本形のテストは func TestXxx(*testing.T) という形式でテストコードを書いて、go test コマンドで実行できます。

ベンチマークのテストも同様に、 func BenchmarkXxx(*testing.B) という形式でテストコードを書いて、 go test -bench=. で実行できます。概略は公式ドキュメントのPackage testingに書いています。-bench 時に利用できるオプションは Testing flags にまとまっています。

オプションを見ると、なにげに多くの機能があることがわかりますが、今回はベンチマークの結果可視化部分を中心に見ていきます。

Benchmarkコードの準備

まずは何でも良いですが、ベンチマーク対象とする関数を用意します。

ベンチマーク対象のコード(どんな処理でも良いですが、今回は画像をリサイズする処理にしました)
import (
    "github.com/nfnt/resize"
    "image/jpeg"
    "os"
)

func ResizeJPEG(src, dest string, quality int) error {
    f, err := os.Open(src)
    if err != nil {
        return err
    }

    img, err := jpeg.Decode(f)
    if err != nil {
        return err
    }
    f.Close()

    out, err := os.Create(dest)
    if err != nil {
        return err
    }
    defer out.Close()

    m := resize.Resize(1000, 0, img, resize.Lanczos3)
    return jpeg.Encode(out, m, &jpeg.Options{
        Quality: quality,
    })
}

これに対応するベンチマークコードを準備します。比較のためJPEGのQualityを75, 100の2種類分用意しました。

import "testing"

func BenchmarkResizeJPEG_q75(t *testing.B) {
    err := ResizeJPEG("src.jpg", "resize_q75.jpg", 75)
    if err != nil {
        t.Fatal("resize src.jpg: ", err)
    }
}

func BenchmarkResizeJPEG_q100(t *testing.B) {
    err := ResizeJPEG("src.jpg", "resize_q100.jpg", 100)
    if err != nil {
        t.Fatal("resize src.jpg: ", err)
    }
}

このコードに対して go test -bench=. で実行します。

$ go test -bench=.
goos: windows
goarch: amd64
pkg: github.com/laqiiz/go-tooling-sample/thumbnail
BenchmarkResizeJPEG_q75-4       1000000000               0.0797 ns/op
BenchmarkResizeJPEG_q100-4      1000000000               0.138 ns/op
PASS

用意したベンチマーク関数ごとの、 実行回数1回あたりの実行にかかった時間(ns/op) が取得できます。

ベンチマーク結果の可視化

さて、 go test -bench=. は非常に実行が簡単ですし、これだけで事足りることも多いと思います。一方でベンチマーク対象の関数そのものをチューニングしたい時には、ボトルネックが関数内のどこにあるか分かりにくいです。

そのため、逐次処理のトレースを行ってみます。トレース方法はまず -trace オプションで実行トレースを出力し、 go toolでビジュアライズします。

$ go test -bench=. -trace a.trace

これで a.trace というファイルが出力されていると思います。もし出力されていなければベンチマークテストが失敗している可能性があります。 a.trace にはゴルーチンの作成/ブロック/ブロック解除、システムコールの開始/終了/ブロック、GC関連のイベント、ヒープサイズの変更、プロセッサの開始/停止などの情報がバイナリ形式で書き込まれているそうです。詳細はPackage trace を参考ください。

この a.trace ファイルを引数にして、go tool traceを実行します。--http で起動するサーバのポートを指定する必要があります。

$ go tool trace --http localhost:6060 a.trace
2019/12/01 00:12:52 Parsing trace...
2019/12/01 00:12:52 Splitting trace...
2019/12/01 00:12:52 Opening browser. Trace viewer is listening on http://127.0.0.1:6060

そうすると、以下のようなページがブラウザで表示されます。

Trace viewer top page

ここから1番トップの View traceを見ると下記のようなグラフを表示できます。

view trace page

内容は横軸は時間で、GCやコアごとの利用率がわかります。ボックスをドラッグすると拡大・縮小できるため気になるところをピンポイントで拡大できるので、ボトルネックになっていそうなところはこちらで確認します。

view trace page zoom in/out

Zoom In/Outでベンチマーク対象の関数が呼び出している resizeパッケージの実行時間などがわかります。今回は自分で書いたコードが殆ど無いですが、他にも複数のパッケージ呼び出しがあるような関数をベンチマークした時は処理時間の内訳が分かるのではないでしょうか。

resize部分.png

CPUプロファイリング

続いて、同じベンチマークコードに対してCPUプロファイルを行います。CPUプロファイリングは、比較的ランタイム負荷が小さい処理で、実行中のgorutineのコールトレースを収集することで分析を行います。

trace の場合と同様に、-cpuprofile オプションで同様に.prof ファイルが出力されますのでまずベンチマークのテストを実行します。

$ go test -bench=. -cpuprofile a.prof
goos: windows
goarch: amd64
pkg: github.com/laqiiz/go-tooling-sample/thumbnail
BenchmarkResizeJPEG_q75-4       1000000000               0.0779 ns/op
BenchmarkResizeJPEG_q100-4      1000000000               0.0920 ns/op
PASS
ok      github.com/laqiiz/go-tooling-sample/thumbnail   3.141s

出力された a.prof に対して、 go tool pprof を実行して可視化します。

$ go tool pprof -http :6060 a.prof
Serving web UI on http://localhost:6060

※可視化には Graphviz が必要です。未インストールの方はこちらから環境構築ください

Graphvizがインストールされた端末のブラウザで http://localhost:6060/ui/ を確認すると、以下のようなコールグラフが出力されています。

cpu_profile.png

処理が重そうな部分は強調して表示されているようなのでわかりやすいですね。グラフをみると、resizeの処理に隠れていますがjpegのDecodeや、jpegのEncode(書き込み)部分も処理が重いことがわかります。

Viewタブから 「Frame Graph」 を選択すると以下のようなフレームグラフも表示できます。
人によってはこちらのほうが理解しやすいかも知れません

framegraph.png

これらのグラフによって、もしこのベンチマーク対象の関数をチューニングしようとする時は、resize処理だけではなく、DecodeやEncodeを別処理として非同期に切り出すような選択や、上手くバッチ的に処理できないかといったアプローチを検討する判断材料にもなると思います。

まとめ

Goのベンチマークといえば、関数のマイクロベンチマークのためのツールで、関数内部のプロファイルを行う機能は無いと思っていましたが、既存のToolセットと合わせることが可視化分析も可能でした。

「ボトルネックになっていない箇所を最適化しようとするのは自転車置き場議論だ!」 とGoogle Cloud Next ’19 in Tokyoのあるセッションで聞いたのですが、Go Toolingも活用して推測ではなく事実をベースにチューニングしていきたいですね。


  1. 例えば、GPSの緯度経度から地域メッシュコードを特定する処理などです 

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

jqより便利そうなTUIツールtsonが良さげな件

ども、ゴリラです。

みなさんjqを使っていますか?とても便利なのでおそらく多くの方はjqを使っていると思います。
ぼくもその一人ですが、最近JSONをツリー状にして編集、フィルタリング、保存できたら便利では?と思い立ってtsonっていうTUIツールを作りました。
意外と便利だったので、紹介していきます。

どんな感じ?

ウホウホウ
tson-demo.gif

対応OS

Mac/Linuxのみになります。(Windowsだと画面が崩れます)
将来的にはWindowsにも対応するつもりです。

機能

便利だろうと思った機能を実装しました。次になります。

  • ファイル、URLからJSONを読み込み、ツリー化
  • フィルタリング
  • 編集、追加、削除
  • ファイルに保存
  • 外部エディタを使ってJSONを編集する

使い方

とてもシンプルです。以下の3つの方法があります。

  • ファイルから直接読み込む:tson < file.json
  • パイプ|で標準出力から読み込む:curl http://gorilla/api | tson
  • -url引数を使ってURLのレスポンスから読み込む:tson -url http://gorilla/api

URLから読み込みについて

-urlで指定したURLからJSONを読み込むことができますが、これは簡易なHTTP GETなため複雑なことはできません。
GET以外のPOSTやヘッダーをつけてリクエストを発行したいときはcurlといったコマンドを使用して|でtsonに流し込むと良いでしょう。

具体的な使い方について

tsonの便利なところはインタラクティブにJSONを編集できるところです。TUIツールならではですね。
個人的におすすめなキーバインドをいくつか紹介します。より詳細なキーバインドはこちらを参照してください。

絞り込み

/で検索ダイアログが表示されるので、検索したい文字を入力してください。
入力するたび結果が即反映されます。特に長いJSONの場合キー名を入力して絞り込みするときに便利でしょう。

image.png

折りたたみ

Hでvalueノードをすべて折りたたみ、Keyノードだけ残します。これは長いJSONの全体像を把握するのに役に立ちます。

  • 折りたたむ前
    image.png

  • 折りたたんだ後
    image.png

現在のkeyノードのみ折りたたみたい場合はhを使用します。
valueを知りたいときはlで現在のノードを展開できます。また全体を展開したいときはLを使用します。

外部エディタを使ってJSONを編集

e$EDITORに設定されているエディタを使用してJSONを編集することができます。
エディタで編集したJSONを保存して終了すると、制御はtsonに戻り編集した結果も反映されます。

  • 編集前
    image.png

  • エディタを使って編集
    image.png

  • 編集後
    image.png

ただ、エディタを使用してtsonも戻ったあとに1キーストロークが反応しなくなります。
つまり1回目に入力したキーが効かないです。これはtviewが使用しているtcellのバグのようです。
現在まだ修正されていないのとtcell作者があんまり直す気がないので、修正されるのを待つよりもこちらが直したほう早いと思いますが、tcellナニモワカラナイので、時間はかかりますが自分が頑張って治そうと思います。

ノード間ジャンプ

ctrl-j/ctrl-kでノード間のジャンプができます。子ノードの数が多く、次のノードに移動したいときに便利です。
例えば、次のようにrangeノードからindexノードにジャンプするときに使用したりします。

  • 移動前

  • 移動後

ファイルに保存

sで現在のツリー状態をファイルに保存できます。もちろん出力もJSONです。
例えばtson -url https://gorilla/apiでレスポンスを確認したあとにそれを出力したいときに役たちます。

  • 出力前

  • 出力後

実装

すこし実装について書いていきます。実際ソースコードを読んでもらった方がわかりやすいのかもしれません。
まず、JSONは以下の型が定義されています。

false/null/true/object/array/number/string

これはRFC 8259で詳しく定義されているので、興味ある方は読んでみてください。
ちなみに、numberはintfloatが含まれています。

JSONの読み込みからツリー生成

JSONからツリーを作成するときの大まかの処理の流れは次になります。

  1. JSONをGoの型にパース(json.Unmarshal()を使用)
  2. objectかarrayかliteralかで処理を分岐し、objectとarrayの中身を再帰的に処理していく

objectとarrayはarray、object、literalを持つことができるので、
どのように型を判定して適切な処理を行うのか、この再帰処理がかなり難航しました。

JSONの読み込み

JSONを読み込むときに、空インターフェイスを用意します。なぜならJSONがどんな形でも対応できるようにするためです。

b, err := ioutil.ReadAll(in)
if err != nil {
    log.Println(err)
    return nil, err
}

var i interface{}
if err := json.Unmarshal(b, &i); err != nil {
    return nil, err
}

あとはjson.UnmarshalがJSONをよしなにGoの型にしてくれます。
JSONの型に対応するGoの型は次になります。

JSON Go
string string
number int/float64
object []map[string]interface{}
array []interface{}
true true
false false
null nil

ちなみに、ツリーを作成するときにどのノードがどの型なのか、という情報を持たせる必要があります。
なぜなら、この情報はツリーをJSONに書き出すときに使用するからです。

ツリー生成

パース後のinterfaceをtypeを使って型ごとに処理を行います

switch node := node.(type) {
case map[string]interface{}:
    // objectの場合の処理
case []interface{}:
    // arrayの場合の処理
default:
    // literalの場合の処理
}

objectとarrayは基本的にt.AddNode()を使って再帰処理を行います。
それと同時に親ノードがarrayなのかobjectなのかといった情報をReferrenceというオブジェクトにセットしています。
なぜノードの型情報が必要かというと、JSONに書き出すときも再帰的にノードを辿っていきますが、
arrayobjectかによって処理が変わるので、その判定に使用します。

ちなみに、次のコードはarrayの場合の処理です。

case []interface{}:
    for _, v := range node {
        id := uuid.Must(uuid.NewV4()).String()
        switch v.(type) {
        case map[string]interface{}:
            objectNode := tview.NewTreeNode("{object}").
                SetChildren(t.AddNode(v)).SetReference(Reference{ID: id, JSONType: Object})
            nodes = append(nodes, objectNode)
        case []interface{}:
            arrayNode := tview.NewTreeNode("{array}").
                SetChildren(t.AddNode(v)).SetReference(Reference{ID: id, JSONType: Array})
            nodes = append(nodes, arrayNode)
        default:
            nodes = append(nodes, t.AddNode(v)...)
        }
    }

literalの場合、型チェックはreflectパッケージを使用して次のようにチェックします。
ValueTypeはJSONを書き出すときにnullで書き出すのか"null"で書き出すのかといった判定に使用します。

JSONの書き出しはこうった情報がないと正しく書き出せないので、ここがキモになります。

default:
    ref := reflect.ValueOf(node)
    var valueType ValueType
    switch ref.Kind() {
    case reflect.Int:
        valueType = Int
    case reflect.Float64:
        valueType = Float
    case reflect.Bool:
        valueType = Boolean
    default:
        if node == nil {
            valueType = Null
        } else {
            valueType = String
        }
    }

    id := uuid.Must(uuid.NewV4()).String()
    nodes = append(nodes, t.NewNodeWithLiteral(node).
        SetReference(Reference{ID: id, JSONType: Value, ValueType: valueType}))

ちなみに、ValueTypeの定義は以下のようになっています。

type ValueType int

const (
    Int ValueType = iota + 1
    String
    Float
    Boolean
    Null
)

JSONの書き出し

JSONの書き出しですが、こちらはとてもシンプルです。
JSONTypearrayobjectかそれ以外かで処理を分けます。
こちらも再帰的にノードを辿りながらJSONを組み上げていきます。

parseValueValueTypeを見てintなのかstringなのかといった値の種類別で変換処理を行います。

func (g *Gui) makeJSON(node *tview.TreeNode) interface{} {
    ref := node.GetReference().(Reference)
    children := node.GetChildren()

    switch ref.JSONType {
    case Object:
        i := make(map[string]interface{})
        for _, n := range children {
            i[n.GetText()] = g.makeJSON(n)
        }
        return i
    case Array:
        var i []interface{}
        for _, n := range children {
            i = append(i, g.makeJSON(n))
        }
        return i
    case Key:
        v := node.GetChildren()[0]
        if v.GetReference().(Reference).JSONType == Value {
            return g.parseValue(v)
        }
        return map[string]interface{}{
            node.GetText(): g.makeJSON(v),
        }
    }

    return g.parseValue(node)
}

func (g *Gui) parseValue(node *tview.TreeNode) interface{} {
    v := node.GetText()
    ref := node.GetReference().(Reference)

    switch ref.ValueType {
    case Int:
        i, _ := strconv.Atoi(v)
        return i
    case Float:
        f, _ := strconv.ParseFloat(v, 64)
        return f
    case Boolean:
        b, _ := strconv.ParseBool(v)
        return b
    case Null:
        return nil
    }

    return v
}

ざっくりですが、以上がtsonの処理の一部になります。

今後について

tsonは今後も機能を追加していきます。
具体的に言うとPostmanのような、ヘッダーやリクエストボディなどを自由に編集してHTTPリクエストを発行できる機能を追加する予定です。

おまけ

デモの中でgjoというツールを使いましたが、これは画像のようにkey=valueの組み合わせで簡単にJSON文字列を生成できるツールです。

image.png

curl -X POST -H "Content-Type: application/json" https://gorilla/api -d $(gjo name=gorilla)
って感じでcurlと組み合わせて使用できるので、よかったら使ってみてください。

最後に

tsonはまだまだ使いにくいところがあります。
まだまだ改善の余地があるので、この記事を見てGo詳しくなくても、
OSSを作るのをチャレンジしてみたい方や一緒に作ってみたい方はぜひ声かけてください。お待ちしています。

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

画像処理パッケージから学ぶ!Goによる画像処理のテスト実装パターンまとめ

cover.png

こんにちは pon です。僕はよくGoで画像処理をやるのですが、実は画像のテストに関してネットにあまり情報がありません(というかほぼない)。そこで、僕が他の画像処理パッケージのコードリーディングをしてまとめた画像処理テストの実装パターンを紹介します。実はこの内容は Go Conference'19 Summer in Fukuokaで発表したことがあるのですが、それをより詳細な解説にしています。

画像のテストパターン

1ピクセルずつ愚直にテスト

標準パッケージやOSSなどの様々なパッケージではRGBAの値を1ピクセルごと調べています。

// in go/src/image/draw/draw_test.go
func eq(c0, c1 color.Color) bool {
    r0, g0, b0, a0 := c0.RGBA()
    r1, g1, b1, a1 := c1.RGBA()
    return r0 == r1 && g0 == g1 && b0 == b1 && a0 == a1
}

func TestDraw(t *testing.T) {
    // ...

    // 画像が処理されているかを1ピクセルごと調べる
    for y := b.Min.Y; y < b.Max.Y; y++ {
        for x := b.Min.X; x < b.Max.X; x++ {
            if !eq(dst.At(x, y), golden.At(x, y)) {
                // test fail
            }
        }
    }
}

しかし愚直に1ピクセルごと調べていくと時間がかかるので、標準パッケージでは、その前に画像の大きさが一致するかや、1ピクセルだけチェックしてRGBAがあっているかを確認しています。下記はimage/drawパッケージのテスト実装です。

// 画像の大きさが一致するか
if !b.Eq(golden.Bounds()) {
    // fail
}

// (8,8)のRGBAがあっているかだけをチェック
if image.Pt(8, 8).In(r) {
    if !eq(dst.At(8, 8), test.expected) {
        t.Errorf("draw %v %s: at (8, 8) %v versus %v", r, test.desc, dst.At(8, 8), test.expected)
        continue
    }
}

上の例で1ピクセルだけチェックするのは、そもそも全く画像処理できてなかったパターン(全く予期せぬ画像ができてしまう時など)を前もって弾く為です。これである程度重いテストの前にテストを失敗させることができます。

ストライドを使った効率化

さらに面白いテストのパターンもあります。下記は画像処理アルゴリズムのコレクションパッケージ github.com/anthonynsimon/bild のテストで使われているequal関数です。

func RGBAImageEqual(a, b *image.RGBA) bool {
    if !a.Rect.Eq(b.Rect) {
        return false
    }

    for y := 0; y < a.Bounds().Dy(); y++ {
        for x := 0; x < a.Bounds().Dx(); x++ {
            pos := y*a.Stride + x*4
            if a.Pix[pos+0] != b.Pix[pos+0] {
                return false
            }
            if a.Pix[pos+1] != b.Pix[pos+1] {
                return false
            }
            if a.Pix[pos+2] != b.Pix[pos+2] {
                return false
            }
            if a.Pix[pos+3] != b.Pix[pos+3] {
                return false
            }
        }
    }
    return true
}

画像データの構造に着目してストライドの値を使ってRGBAを保持するスライスに直でアクセスしています。もちろん先ほどのテストパターンより高速です。画像のデータ構造とストライドに関しては下記の記事が大変勉強になります。

画像データの構造

bytes.Equalでテスト

一方で image.RGBA.Pix[]uint8 なので bytes.Equal で一発でテストできます。画像処理フィルタパッケージ「disintegration/gift」ではこのように画像のテストがされています。

func checkBoundsAndPix(b1, b2 image.Rectangle, pix1, pix2 []uint8) bool {
    if !b1.Eq(b2) {
        return false
    }
    if !bytes.Equal(pix1, pix2) {
        return false
    }
    return true
}

実はここまでに紹介した例よりもこちらの方が高速です。一方で、この方法だとどのピクセルが間違っているのかの情報が失われてしまいます。。その為、「パフォーマンス」と「テスト失敗時の情報の詳細度」のシーソーゲームです。テスト失敗時の情報の詳細度を全く気にしないのであれば reflect.DeepEqual でもいけます。速度はほとんど bytes.Equal を使ったテストと同じです。

カラーモードに適したテスト

当然、画像のカラーモード次第で更に最適化したテスト実装があります。下記はグレースケールの画像のequal関数です。グレースケールならこれで十分でしょう。

func GrayImageEqual(a, b *image.Gray) bool {
    if !a.Rect.Eq(b.Rect) {
        return false
    }

    for i := 0; i < len(a.Pix); i++ {
        if a.Pix[i] != b.Pix[i] {
            return false
        }
    }
    return true
}

テストパターン別のベンチマーク

先ほどパフォーマンスの話が出たので先ほど紹介したパターンをそれぞれベンチマークしてみましょう。ベンチマークは460px × 460px の同じ画像かをチェックするテストです。コードは こちら にあるので興味のある方はどうぞ。

go test -bench=. ./...
goos: darwin
goarch: amd64
pkg: github.com/po3rin/try-img-test
BenchmarkEqNormal-12              84      13948999 ns/op   //1ピクセルずつ愚直にテスト
BenchmarkEqWithStride-12         686       1717456 ns/op   //ストライドを使った効率化
BenchmarkEqWithBytes-12         1070       1113299 ns/op   //bytes,Equalを使ったテスト
BenchmarkEqWithReflect-12       1059       1115034 ns/op   //reflect.DeepEqualを使ったテスト
PASS

1ピクセルずつ愚直に検査が当然遅いですね。ストライドを使った効率化したテストはパフォーマンスと失敗時の情報の詳細度のバランス的に良さそうです。テスト失敗時にどの程度の詳細度で情報が欲しいかで選んでいくと良いでしょう。

テストケースの準備

画像をテストする方法を決めたらあとは期待するデータを準備するだけです。github.com/disintegration/gift では下記のようにテストデータを準備しています。

testData := []struct {
    desc           string
    w, h           int
    r              Resampling
    srcb, dstb     image.Rectangle
    srcPix, dstPix []uint8
}{
    {
        "resize to fit (1, 1, nearest)",
        1, 1, NearestNeighborResampling,
        image.Rect(-1, -1, 4, 4),
        image.Rect(0, 0, 1, 1),
        []uint8{
            0x00, 0x01, 0x02, 0x03, 0x04,
            0x05, 0x06, 0x07, 0x08, 0x09,
            0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
            0x0f, 0x10, 0x11, 0x12, 0x13,
            0x14, 0x15, 0x16, 0x17, 0x18,
        },
        []uint8{0x0c},
    },
    // ...
}

上のように []uint8 を準備しても良いですが、もっと大きな画像を扱う場合はどうしましょう。また、欲しい画像がコロコロ変わる場合にいちいち []uint8 をテストケースに詰め直すのも面倒な作業です。その為、場合によっては下記のように goldenfile を準備したテストを行うのが便利です。その際には goldenfile を生成するフラグを準備しておくと良いでしょう。

var genGoldenFiles = flag.Bool("gen_golden_files", false, "whether to generate the TestXxx golden files.")

func TestResizePNG(t *testing.T) {
    tests := []struct {
        name           string
        goldenFilename string
    }{
        {
            name:           "x1.0",
            goldenFilename: "testdata/resize_golden_1.png",
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {

            // 何かしら面白い画像生成
            got := Convert()

            // goldenfile 生成フラッグが有効だったら goldenfileを生成して終了
            if *genGoldenFiles {
                goldenFile, _ := os.Create(tt.goldenFilename)
                defer goldenFile.Close()
                _ = png.Encode(goldenFile, got)
                return
            }

            // goldenfileから期待する画像を取得
            f, _ = os.Open(tt.goldenFilename)
            defer f.Close()
            want, _, _ := image.Decode(f)

            // 欲しい画像ができているかテスト
            if !reflect.DeepEqual(convertRGBA(got), convertRGBA(want)) {
                t.Errorf("actual image differs from golden image")
                return
            }
        })
    }
}

このテストは下記のようにフラグを使うことでテストではなくgoldenfile生成を行ってくれるようになります。

go test -gen_golden_files ./...

実際に github.com/golang/image パッケージではこのように goldenfile と生成フラグを使ったテストが実装されています。goldenfile生成には独自でフラグを用意する他にもビルドフラグで切り替えるパターンもあります。

また、画像処理のテストにおけるgoldenfileはPNGであることが望まれます。JPEG自体がlossy(情報が欠落する)な非可逆(元に戻せない)圧縮方式なので、一度image.ImageをJPEGに変換してしまうと、元の画像に復元することはできないからです。

png.png

まとめ

いろんな画像処理パッケージのテストをのぞいて、画像処理のテストのパターンをまとめました。意外にもたくさんのパターンがあって驚きました。僕がまだ考えついていないテスト実装パターンがあると思うので、もっと良いテスト方法があったらぜひ教えてください!

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

Goによる画像処理のテスト実装パターンまとめ

cover.png

この記事は Go2 Advent Calendar 2019の1日目の記事です。

こんにちは pon です。

僕はよくGoで画像処理をやるのですが、実は画像のテストに関してネットにあまり情報がありません(というかほぼない)。そこで、僕が他の画像処理パッケージのコードリーディングをしてまとめた画像処理テストの実装パターンを紹介します。

実はこの内容は Go Conference'19 Summer in Fukuokaで発表したことがあるのですが、それをより詳細な解説にしています。

画像のテストパターン

1ピクセルずつ愚直にテスト

標準パッケージやOSSなどの様々なパッケージではRGBAの値を1ピクセルごと調べています。

// in go/src/image/draw/draw_test.go
func eq(c0, c1 color.Color) bool {
    r0, g0, b0, a0 := c0.RGBA()
    r1, g1, b1, a1 := c1.RGBA()
    return r0 == r1 && g0 == g1 && b0 == b1 && a0 == a1
}

func TestDraw(t *testing.T) {
    // ...

    // 画像が処理されているかを1ピクセルごと調べる
    for y := b.Min.Y; y < b.Max.Y; y++ {
        for x := b.Min.X; x < b.Max.X; x++ {
            if !eq(dst.At(x, y), golden.At(x, y)) {
                // test fail
            }
        }
    }
}

しかし愚直に1ピクセルごと調べていくと時間がかかるので、標準パッケージでは、その前に画像の大きさが一致するかや、1ピクセルだけチェックしてRGBAがあっているかを確認しています。下記はimage/drawパッケージのテスト実装です。

// 画像の大きさが一致するか
if !b.Eq(golden.Bounds()) {
    // fail
}

// (8,8)のRGBAがあっているかだけをチェック
if image.Pt(8, 8).In(r) {
    if !eq(dst.At(8, 8), test.expected) {
        t.Errorf("draw %v %s: at (8, 8) %v versus %v", r, test.desc, dst.At(8, 8), test.expected)
        continue
    }
}

上の例で1ピクセルだけチェックするのは、そもそも全く画像処理できてなかったパターン(全く予期せぬ画像ができてしまう時など)を前もって弾く為です。これである程度重いテストの前にテストを失敗させることができます。

ストライドを使った効率化

さらに面白いテストのパターンもあります。下記は画像処理アルゴリズムのコレクションパッケージ github.com/anthonynsimon/bild のテストで使われているequal関数です。

func RGBAImageEqual(a, b *image.RGBA) bool {
    if !a.Rect.Eq(b.Rect) {
        return false
    }

    for y := 0; y < a.Bounds().Dy(); y++ {
        for x := 0; x < a.Bounds().Dx(); x++ {
            pos := y*a.Stride + x*4
            if a.Pix[pos+0] != b.Pix[pos+0] {
                return false
            }
            if a.Pix[pos+1] != b.Pix[pos+1] {
                return false
            }
            if a.Pix[pos+2] != b.Pix[pos+2] {
                return false
            }
            if a.Pix[pos+3] != b.Pix[pos+3] {
                return false
            }
        }
    }
    return true
}

画像データの構造に着目してストライドの値を使ってRGBAを保持するスライスに直でアクセスしています。もちろん先ほどのテストパターンより高速です。画像のデータ構造とストライドに関しては下記の記事が大変勉強になります。

画像データの構造

bytes.Equalでテスト

一方で image.RGBA.Pix[]uint8 なので bytes.Equal で一発でテストできます。画像処理フィルタパッケージ「disintegration/gift」ではこのように画像のテストがされています。

func checkBoundsAndPix(b1, b2 image.Rectangle, pix1, pix2 []uint8) bool {
    if !b1.Eq(b2) {
        return false
    }
    if !bytes.Equal(pix1, pix2) {
        return false
    }
    return true
}

実はここまでに紹介した例よりもこちらの方が高速です。一方で、この方法だとどのピクセルが間違っているのかの情報が失われてしまいます。。その為、「パフォーマンス」と「テスト失敗時の情報の詳細度」のシーソーゲームです。テスト失敗時の情報の詳細度を全く気にしないのであれば reflect.DeepEqual でもいけます。速度はほとんど bytes.Equal を使ったテストと同じです。

カラーモードに適したテスト

当然、画像のカラーモード次第で更に最適化したテスト実装があります。下記はグレースケールの画像のequal関数です。グレースケールならこれで十分でしょう。

func GrayImageEqual(a, b *image.Gray) bool {
    if !a.Rect.Eq(b.Rect) {
        return false
    }

    for i := 0; i < len(a.Pix); i++ {
        if a.Pix[i] != b.Pix[i] {
            return false
        }
    }
    return true
}

テストパターン別のベンチマーク

先ほどパフォーマンスの話が出たので先ほど紹介したパターンをそれぞれベンチマークしてみましょう。ベンチマークは460px × 460px の同じ画像かをチェックするテストです。コードは こちら にあるので興味のある方はどうぞ。

go test -bench=. ./...
goos: darwin
goarch: amd64
pkg: github.com/po3rin/try-img-test
BenchmarkEqNormal-12              84      13948999 ns/op   //1ピクセルずつ愚直にテスト
BenchmarkEqWithStride-12         686       1717456 ns/op   //ストライドを使った効率化
BenchmarkEqWithBytes-12         1070       1113299 ns/op   //bytes,Equalを使ったテスト
BenchmarkEqWithReflect-12       1059       1115034 ns/op   //reflect.DeepEqualを使ったテスト
PASS

1ピクセルずつ愚直に検査が当然遅いですね。ストライドを使った効率化したテストはパフォーマンスと失敗時の情報の詳細度のバランス的に良さそうです。テスト失敗時にどの程度の詳細度で情報が欲しいかで選んでいくと良いでしょう。

テストケースの準備

画像をテストする方法を決めたらあとは期待するデータを準備するだけです。github.com/disintegration/gift では下記のようにテストデータを準備しています。

testData := []struct {
    desc           string
    w, h           int
    r              Resampling
    srcb, dstb     image.Rectangle
    srcPix, dstPix []uint8
}{
    {
        "resize to fit (1, 1, nearest)",
        1, 1, NearestNeighborResampling,
        image.Rect(-1, -1, 4, 4),
        image.Rect(0, 0, 1, 1),
        []uint8{
            0x00, 0x01, 0x02, 0x03, 0x04,
            0x05, 0x06, 0x07, 0x08, 0x09,
            0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
            0x0f, 0x10, 0x11, 0x12, 0x13,
            0x14, 0x15, 0x16, 0x17, 0x18,
        },
        []uint8{0x0c},
    },
    // ...
}

上のように []uint8 を準備しても良いですが、もっと大きな画像を扱う場合はどうしましょう。また、欲しい画像がコロコロ変わる場合にいちいち []uint8 をテストケースに詰め直すのも面倒な作業です。その為、場合によっては下記のように goldenfile を準備したテストを行うのが便利です。その際には goldenfile を生成するフラグを準備しておくと良いでしょう。

var genGoldenFiles = flag.Bool("gen_golden_files", false, "whether to generate the TestXxx golden files.")

func TestResizePNG(t *testing.T) {
    tests := []struct {
        name           string
        goldenFilename string
    }{
        {
            name:           "x1.0",
            goldenFilename: "testdata/resize_golden_1.png",
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {

            // 何かしら面白い画像生成
            got := Convert()

            // goldenfile 生成フラッグが有効だったら goldenfileを生成して終了
            if *genGoldenFiles {
                goldenFile, _ := os.Create(tt.goldenFilename)
                defer goldenFile.Close()
                _ = png.Encode(goldenFile, got)
                return
            }

            // goldenfileから期待する画像を取得
            f, _ = os.Open(tt.goldenFilename)
            defer f.Close()
            want, _, _ := image.Decode(f)

            // 欲しい画像ができているかテスト
            if !reflect.DeepEqual(convertRGBA(got), convertRGBA(want)) {
                t.Errorf("actual image differs from golden image")
                return
            }
        })
    }
}

このテストは下記のようにフラグを使うことでテストではなくgoldenfile生成を行ってくれるようになります。

go test -gen_golden_files ./...

実際に github.com/golang/image パッケージではこのように goldenfile と生成フラグを使ったテストが実装されています。goldenfile生成には独自でフラグを用意する他にもビルドフラグで切り替えるパターンもあります。

また、画像処理のテストにおけるgoldenfileはPNGであることが望まれます。JPEG自体がlossy(情報が欠落する)な非可逆(元に戻せない)圧縮方式なので、一度image.ImageをJPEGに変換してしまうと、元の画像に復元することはできないからです。

png.png

まとめ

いろんな画像処理パッケージのテストをのぞいて、画像処理のテストのパターンをまとめました。意外にもたくさんのパターンがあって驚きました。僕がまだ考えついていないテスト実装パターンがあると思うので、もっと良いテスト方法があったらぜひ教えてください!

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

Goによる画像処理テストパターンの考察とまとめ

cover.png

この記事は Go2 Advent Calendar 2019の1日目の記事です。

こんにちは pon です。

僕はよくGoで画像処理をやるのですが、実は画像のテストに関してネットにあまり情報がありません(というかほぼない)。そこで、僕が他の画像処理パッケージのコードリーディングをしてまとめた画像処理テストの実装パターンを紹介します。

実はこの内容は Go Conference'19 Summer in Fukuokaで発表したことがあるのですが、それをより詳細な解説にしています。

画像のテストパターン

1ピクセルずつ愚直にテスト

標準パッケージやOSSなどの様々なパッケージではRGBAの値を1ピクセルごと調べています。

// in go/src/image/draw/draw_test.go
func eq(c0, c1 color.Color) bool {
    r0, g0, b0, a0 := c0.RGBA()
    r1, g1, b1, a1 := c1.RGBA()
    return r0 == r1 && g0 == g1 && b0 == b1 && a0 == a1
}

func TestDraw(t *testing.T) {
    // ...

    // 画像が処理されているかを1ピクセルごと調べる
    for y := b.Min.Y; y < b.Max.Y; y++ {
        for x := b.Min.X; x < b.Max.X; x++ {
            if !eq(dst.At(x, y), golden.At(x, y)) {
                // test fail
            }
        }
    }
}

しかし愚直に1ピクセルごと調べていくと時間がかかるので、標準パッケージでは、その前に画像の大きさが一致するかや、1ピクセルだけチェックしてRGBAがあっているかを確認しています。下記はimage/drawパッケージのテスト実装です。

// 画像の大きさが一致するか
if !b.Eq(golden.Bounds()) {
    // fail
}

// (8,8)のRGBAがあっているかだけをチェック
if image.Pt(8, 8).In(r) {
    if !eq(dst.At(8, 8), test.expected) {
        t.Errorf("draw %v %s: at (8, 8) %v versus %v", r, test.desc, dst.At(8, 8), test.expected)
        continue
    }
}

上の例で1ピクセルだけチェックするのは、そもそも全く画像処理できてなかったパターン(全く予期せぬ画像ができてしまう時など)を前もって弾く為です。これである程度重いテストの前にテストを失敗させることができます。

ストライドを使った効率化

さらに面白いテストのパターンもあります。下記は画像処理アルゴリズムのコレクションパッケージ github.com/anthonynsimon/bild のテストで使われているequal関数です。

func RGBAImageEqual(a, b *image.RGBA) bool {
    if !a.Rect.Eq(b.Rect) {
        return false
    }

    for y := 0; y < a.Bounds().Dy(); y++ {
        for x := 0; x < a.Bounds().Dx(); x++ {
            pos := y*a.Stride + x*4
            if a.Pix[pos+0] != b.Pix[pos+0] {
                return false
            }
            if a.Pix[pos+1] != b.Pix[pos+1] {
                return false
            }
            if a.Pix[pos+2] != b.Pix[pos+2] {
                return false
            }
            if a.Pix[pos+3] != b.Pix[pos+3] {
                return false
            }
        }
    }
    return true
}

画像データの構造に着目してストライドの値を使ってRGBAを保持するスライスに直でアクセスしています。もちろん先ほどのテストパターンより高速です。画像のデータ構造とストライドに関しては下記の記事が大変勉強になります。

画像データの構造

bytes.Equalでテスト

一方で image.RGBA.Pix[]uint8 なので bytes.Equal で一発でテストできます。画像処理フィルタパッケージ「disintegration/gift」ではこのように画像のテストがされています。

func checkBoundsAndPix(b1, b2 image.Rectangle, pix1, pix2 []uint8) bool {
    if !b1.Eq(b2) {
        return false
    }
    if !bytes.Equal(pix1, pix2) {
        return false
    }
    return true
}

実はここまでに紹介した例よりもこちらの方が高速です。一方で、この方法だとどのピクセルが間違っているのかの情報が失われてしまいます。。その為、「パフォーマンス」と「テスト失敗時の情報の詳細度」のシーソーゲームです。テスト失敗時の情報の詳細度を全く気にしないのであれば reflect.DeepEqual でもいけます。速度はほとんど bytes.Equal を使ったテストと同じです。

カラーモードに適したテスト

当然、画像のカラーモード次第で更に最適化したテスト実装があります。下記はグレースケールの画像のequal関数です。グレースケールならこれで十分でしょう。

func GrayImageEqual(a, b *image.Gray) bool {
    if !a.Rect.Eq(b.Rect) {
        return false
    }

    for i := 0; i < len(a.Pix); i++ {
        if a.Pix[i] != b.Pix[i] {
            return false
        }
    }
    return true
}

テストパターン別のベンチマーク

先ほどパフォーマンスの話が出たので先ほど紹介したパターンをそれぞれベンチマークしてみましょう。ベンチマークは460px × 460px の同じ画像かをチェックするテストです。コードは こちら にあるので興味のある方はどうぞ。

go test -bench=. ./...
goos: darwin
goarch: amd64
pkg: github.com/po3rin/try-img-test
BenchmarkEqNormal-12              84      13948999 ns/op   //1ピクセルずつ愚直にテスト
BenchmarkEqWithStride-12         686       1717456 ns/op   //ストライドを使った効率化
BenchmarkEqWithBytes-12         1070       1113299 ns/op   //bytes,Equalを使ったテスト
BenchmarkEqWithReflect-12       1059       1115034 ns/op   //reflect.DeepEqualを使ったテスト
PASS

1ピクセルずつ愚直に検査が当然遅いですね。ストライドを使った効率化したテストはパフォーマンスと失敗時の情報の詳細度のバランス的に良さそうです。テスト失敗時にどの程度の詳細度で情報が欲しいかで選んでいくと良いでしょう。

テストケースの準備

画像をテストする方法を決めたらあとは期待するデータを準備するだけです。github.com/disintegration/gift では下記のようにテストデータを準備しています。

testData := []struct {
    desc           string
    w, h           int
    r              Resampling
    srcb, dstb     image.Rectangle
    srcPix, dstPix []uint8
}{
    {
        "resize to fit (1, 1, nearest)",
        1, 1, NearestNeighborResampling,
        image.Rect(-1, -1, 4, 4),
        image.Rect(0, 0, 1, 1),
        []uint8{
            0x00, 0x01, 0x02, 0x03, 0x04,
            0x05, 0x06, 0x07, 0x08, 0x09,
            0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
            0x0f, 0x10, 0x11, 0x12, 0x13,
            0x14, 0x15, 0x16, 0x17, 0x18,
        },
        []uint8{0x0c},
    },
    // ...
}

上のように []uint8 を準備しても良いですが、もっと大きな画像を扱う場合はどうしましょう。また、欲しい画像がコロコロ変わる場合にいちいち []uint8 をテストケースに詰め直すのも面倒な作業です。その為、場合によっては下記のように goldenfile を準備したテストを行うのが便利です。その際には goldenfile を生成するフラグを準備しておくと良いでしょう。

var genGoldenFiles = flag.Bool("gen_golden_files", false, "whether to generate the TestXxx golden files.")

func TestResizePNG(t *testing.T) {
    tests := []struct {
        name           string
        goldenFilename string
    }{
        {
            name:           "x1.0",
            goldenFilename: "testdata/resize_golden_1.png",
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {

            // 何かしら面白い画像生成
            got := Convert()

            // goldenfile 生成フラッグが有効だったら goldenfileを生成して終了
            if *genGoldenFiles {
                goldenFile, _ := os.Create(tt.goldenFilename)
                defer goldenFile.Close()
                _ = png.Encode(goldenFile, got)
                return
            }

            // goldenfileから期待する画像を取得
            f, _ = os.Open(tt.goldenFilename)
            defer f.Close()
            want, _, _ := image.Decode(f)

            // 欲しい画像ができているかテスト
            if !reflect.DeepEqual(convertRGBA(got), convertRGBA(want)) {
                t.Errorf("actual image differs from golden image")
                return
            }
        })
    }
}

このテストは下記のようにフラグを使うことでテストではなくgoldenfile生成を行ってくれるようになります。

go test -gen_golden_files ./...

実際に github.com/golang/image パッケージではこのように goldenfile と生成フラグを使ったテストが実装されています。goldenfile生成には独自でフラグを用意する他にもビルドフラグで切り替えるパターンもあります。

また、画像処理のテストにおけるgoldenfileはPNGであることが望まれます。JPEG自体がlossy(情報が欠落する)な非可逆(元に戻せない)圧縮方式なので、一度image.ImageをJPEGに変換してしまうと、元の画像に復元することはできないからです。

png.png

まとめ

いろんな画像処理パッケージのテストをのぞいて、画像処理のテストのパターンをまとめました。意外にもたくさんのパターンがあって驚きました。僕がまだ考えついていないテスト実装パターンがあると思うので、もっと良いテスト方法があったらぜひ教えてください!

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

GoでBMI計算機を作ってみた

作成したもの

https://github.com/usk81/go-pi

設計

BMIだけの計算するものはあったので、実際の使われ方に合わせて、年齢ごとに使用する体格指数を変えるようにしてみました

仕様に関して

今回は一般的になものを使いましたが、体格指数はいろいろな種類があるので、Interfaceを用意しています

type PonderalIndex interface {
    Calc(weight, height float64) (result float64, err error)
}

測定結果

// Result is a ponderal index calculation result
type Result struct {
    Classification string
    Index          float64
    IndexType      string
    Status         string
}

Classification (分類) / Status

Classification ステータス 分類
Severe thinness Fatal 痩せすぎ
Underweight Warning 痩せぎみ
Normal Normal 通常体重
Overweight Warning 太り気味
Obesity Fatal 肥満
Severe obesity Fatal 太り過ぎ

IndexType (インデックスの種類)

年齢 種類 参照
5歳未満 カウプ指数 https://www.benricho.org/bmi/02youji.html
5歳以上、15歳未満 ローレル指数 https://www.benricho.org/bmi/03jidou.html
15歳以上 BMI       https://www.wikiwand.com/en/Body_mass_index

使い方

インストール

git clone git@github.com/go-shway/shway.git
cd shway
go build -ldflags '-w -s' -o go-pi

基本的な使い方

# go-pi height(cm2) weight(kg) age 
e.g. go-pi 180 70 20
> Classification: Normal
> Index: 21.604938
> IndexType: Body Mass Index
> Status: Normal
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

gRPCから見たHTTP/2

HTTP/3も出てきて今更感があるが、改めてHTTP/2についてまとめてみました。

HTTP1.1とその問題点

HTTP/2誕生前から使用されているHTTP/1.1では基本的には1つのリクエストが完了しレスポンスが返ってくるするまで、次のリクエストを送ることができません。

http11.png

HTTPパイプラインという仕組みを使えばHTTP/1.1でも完了を待たずに、複数のリクエストを送信することが可能ですが
サーバーはリクエストの順番通りにレスポンスを返さなければならないという制約があります。

http11_pipeline2.png

3つのリクエストを送信して、
1つめのリクエストのレスポンスが最も重い場合は
2つめ以降のレスポンスが待たされる結果となります。
これをHTTP HOL Blockingといいます。

HTTP/1.1で速度を上げ場合には

TCP接続を多重化するしかないです。では多重化する場合どこに問題があるのでしょうか?
クライアントがWebブラウザであったりする場合は1ドメインにおける最大同時接続数に制限があります。
あとWebブラウザに関係なく、多重化して使用する場合TCPコネクションを行うための負荷がかかります。

多重化して使用する場合の問題点

  • Webブラウザ上からの接続の場合は1ドメインにおける同時接続数に規制があったりする
    • 現在使用されている主要なWebブラウザの場合でも最大6まで
  • TCPコネクションのバーヘッド

TCPコネクション

3Wayハンドシェイク

TCPコネクションが確立されるまでに何が実施されているかというと、接続エンドポイント間で3Wayハンドシェイクが実施されています。

TCPコネクションフロー

アプリケーションから見たとき、TCPコネクション確立時に何を実施されているかというと
簡単には下記のフローの通りsocketの設定、実施を実施しております。各処理においてシステムコールが発生してます。

  • サーバー

    • socket作成
    • IPアドレス、Portの設定
    • IPアドレス、Portとsocketをbindする
    • 接続待ち
  • クライアント

    • socket作成
    • 接続相手を設定する
    • 接続

TCP接続

以上のことによりコネクション数はなるべく少なくて済むものなら少なく済ませたいです。

HTTP/2では

HTTP2では一つにTCPコネクションに内において複数のストリームというデータ(フレーム)の送受信シーケンスをつくることができるようになりました。

また、HTTP/1.1まではヘッダー部、ボティー部は改行で分割されているだけで1つの送信・受信のデータ単位でした。(ヘッダー、ボティを一括して送受信)
HTTP/2ではフレームというものをデータ最小単位として、ヘッダーフレーム、データフレームと独立して送受信をできるようになりました。

これらの機能を駆使してより柔軟な送受信に対応可能になりました。

  • ストリームの多重化
  • ストリームの優先度
  • フレーム
  • ヘッダー圧縮
  • フロー制御

1TCPコネクション内に複数ストリームが存在するイメージ
http2_muli_streams.png

コネクション単位で見るとこういう感じ
http2_in_connection.png

ストリーム

  • フレームのやり取りをするためのもの
  • 1TCPコネクション内に複数のストリームを持つことができる(多重化)
  • 複数ストリーム間で優先度や依存関係を持てる(依存関係)

ストリームの多重化

HTTP/1.1ではリクエストとレスポンスの組を1つずつしか同時に送受信できないことが制限となり、プロトコルレベルでボトルネックになっていました。HTTP/2では1つの接続上にストリームと呼ばれる仮想的な双方向シーケンスを作ることで問題を克服しています。

HTTP/2では1つのコネクション上で複数並列に扱うことができます。そのためHTTP/1.1時代で問題となっていたHOLブロッキング問題を解決します。

ストリームの優先度

ストリームの多重化によりブロックされることは無くなりました。しかし例えばレンダリングに関係ないリソースは後回しにするとういうようなケースはストリームの優先度というものが必要があります。HTTP/2ではPRIORITYフレームを用いてストリームに優先度を付けることが可能となりました。

優先順位は、「重み付け」と「依存関係」の2つがあり、ストリームAを他のストリームより優先させることや、BとCのストリームをそれぞれ2:5の重み付けを付ける事などが可能になります。

動画サイトの表示なので動画再生中にPauseされた瞬間に動画ダウンロードの優先度を下げて、別の通信の優先度が上がるような使用法が考えられます。

※しかし PRIORITY関連のパラメーターは全てサーバーへの優先度提案であり、必ずしもパラーメーター通りに配信されるとは限りません。

ストリームの構成

多重化、優先度を扱うための一意なIDと送信、受信エンドポイントから様々なストリームの操作を行うため、もしくはネットワークやエンドポイントの状況よりストリーム制御を行うために状態管理をしています。

  • ID
  • 状態

ストリームのID

ストリームには一意のIDが存在し、ストリーム生成時に採番します。

  • クライアントが生成したストリームIDは奇数
  • サーバーが生成したストリームIDは偶数

https://summerwind.jp/docs/rfc7540/#section5-1-1

ストリームの状態

stream_状態.png

idle状態にあるストリームはHEADERSフレームを受け取ることによりopen状態となります。
一般的な使われ方としてはopen状態にあるストリームがhalf-closedもしくはclosedに遷移するまでの間には、DATAフレームが送受信されます。

状態 内容
idle 初期状態
reserved(local) PUSH_PROMISEを送信して予約した状態
reserved(remote) PUSH_PROMISEを受信して予約された状態
open Data送受信可能な状態。MAX_CONCURRENT_STREAMSに含まれる
half-closed(local) Data送受信可能な状態。MAX_CONCURRENT_STREAMSに含まれる
half-closed(remote) Data送受信可能な状態。MAX_CONCURRENT_STREAMSに含まれる
close クローズ

https://summerwind.jp/docs/rfc7540/#section5-1

フレーム

フレームタイプに従って構成される可変長のオクテット列からなるHTTP/2接続内部での最小の通信単位です。

  • 可変長のオクテット列からなる
  • フレームタイプに従ってタイプが分かれる
  • バイナリ形式

http_frame.png

HTTP/1.1以前までは1リクエストにつきヘッダー部とボディ(Data)部を1つのデータ形式でかつ、テキストベースでした。それ故オーバヘッドが発生することがありました。HTTP/2以降はHeaderフレーム, Dataフレームと分割されて、様々なシチュエーションに対して柔軟な送受信が可能になってます。

フレームの種類

Type フレームの種類 役割 gRPCでの使用
0 DATA HTTP/1.1におけるリクエスト/レスポンスのボディ部分に相当
1 HEADERS HTTP/1.1におけるリクエスト/レスポンスのヘッダー部分に相当
2 PRIORITY ストリームの優先順位を指定(クライアントのみ送信可能)
3 RST_STREAM エラーなどの理由でストリームを終了するために用いる
4 SETTINGS 接続設定を変更する
5 PUSH_PROMISE サーバプッシュを予告します(サーバのみ送信可能)
6 PING 接続の生存状態を調べる
7 GOAWAY エラーなどの理由で接続を終了するために用いる
8 WINDOW_UPDATE ウィンドウサイズを変更する。フロー制御で使用する
9 CONTINUATION サイズの大きなHEADERS/PUSH_PROMISEフレームの断片

※実はgRPCを介してはPRIORITYとPUSH_PROMISEは使用していません

各フレームの内部構成

各フレームの構成は下記の通り。フレームヘッダーとはPayload以外を指す

項目 容量(bit) 内容
Length 24 Payloadの容量を示す
Type 8 フレームの種類を指す
Flags 8 フレームタイプ固有の真偽値フラグ
R 1 予約済みの1ビットのフィールド。送信時は0固定、受信時は無視
Stream Identifier 31 所属するStreamIDを示す
Frame Payload 0〜 データ部

ヘッダー圧縮

送受信したヘッダーサイズを削減するために下記の方法が取られています。

  • ハフマン符号化データ圧縮
  • ヘッダーのKey-ValueのIndex化により、以前送信した値は送信データ量を縮小可能

ハフマン符号化を使用すると、転送時に個々の値を圧縮できます。また、以前に転送した値をインデックス化したリストを使用すると、インデックス値を転送することで、重複する値をエンコードできます。これは、効率的な検索とヘッダー全体のキーと値の再構築に使用できる場合があります。

header_compress.png

フロー制御

フロー制御は、センダーからレシーバーに望ましくない量のデータや処理できないデータを送信して負荷をかけることを防ぐしくみです。

  • レシーバーの負荷が高くビジー状態だったり、または特定のストリームに所定のリソースを割り当てたいだけの場合

    • 優先度の高い動画ストリーミング中にユーザーによって一時停止がなされてストリーミングよりも他のタスクの優先度を上げたい場合
    • 高速のダウンストリーム接続と低速のアップストリーム接続がある場合、リソースの使用状況を制御するため、プロキシサーバーにてアップストリームの速度に合わせてダウンストリームのデータ配信速度を制限したい場合
  • 各レシーバーは、各ストリームおよび接続全体に必要な任意のウィンドウサイズを設定可能

    • ウィンドウサイズとはACKを待たずに一度に送信できるデータ量
    • デフォルト値は65,535バイト
    • 最大ウィンドウサイズ(2^31-1 バイト)
  • 各レシーバーは最初の接続とストリームフロー制御ウィンドウ(バイト単位)をWINDOW_UPDATEフレームによって予約。これはセンダーがDATAフレームを発行すると削減。

  • フロー制御を無効にすることはできない

  • フロー制御はエンドツーエンドではなく、中間でフロー制御を使用してリソースの使用を制御したり、リソース割り当てのしくみを実装したりできる

gRPC

gRPC側でどのように処理をしているかを簡単に説明します。
Goのライブラリーを使用していますが、おそらく他の言語でも参考になるかと思います。

送受信の処理

シーケンシャル図2.png

クライアントでのTCP接続時のオプション

No コネクション接続時に設定するオプション
1 書き込みバッファサイズ。システムコールの容量の2倍(デフォルト32KB)
2 読み込みバッファサイズ。システムコールの容量(デフォルト32KB)
3 初期ウィンドウサイズ (最小値64KB)
4 コネクション上での初期ウィンドウサイズ (最小値64KB)
5 デフォルトストリームコールオプション設定(下記参照)
6 外部API接続パラメーター
7 接続失敗時のリトライ回数設定
8 サーバー接続をバックグラウンドで行う設定
9 トランスポート層のセキュリティーを有効にする
10 TLSなど認証・認可・セキュリティー設定(トランスポート-セッション層)
11 TLSなど認証・認可・セキュリティー設定(RPC単位)
12 外部APIを用いたTLSなど認証・認可・セキュリティー設定
13 接続失敗時に接続する接続先を設定
14 KeepAlive用設定
15 Unary接続のInterceptorを設定
16 Stream接続のInterceptorを設定

https://github.com/grpc/grpc-go/blob/master/dialoptions.go

クライアントからリクエスト(HEADERSフレーム、DATAフレーム)送信時のストリームオプション

No ストリーム単位で設定するオプション
1 (画像、動画などの)圧縮形式
2 接続・リクエスト失敗時に再接続するか
3 クライアントストリーム
4 受信時のヘッダーフレームとデータフレームの合計の最大サイズ
5 送信時のヘッダーフレームとデータフレームの合計の最大サイズ
6 RPC単位の認証・認可設定
7 RPC用のコーデック設定(デフォルトはprotocol buffers)
8 リトライ時の最大バッファサイズ

サーバーサイドでのTCP接続時の設定

No コネクション接続時に設定するオプション
1 書き込みバッファサイズ。(デフォルト32KB)
2 読み込みバッファサイズ。(デフォルト32KB)
3 初期ウィンドウサイズ (最小値64KB)
4 コネクション上での初期ウィンドウサイズ (最小値64KB)
5 KeepAlive設定
6 KeepAlive強制執行設定
7 コーデック設定
8 受信時のヘッダフレームとデータフレームの合計の最大サイズ(デフォルト4MB)
9 送信時のヘッダフレームとデータフレームの合計の最大サイズ(デフォルトmath.MaxInt32)
10 MAX_CONCURRENT_STREAMS(同時接続可能なストリーム数)
11 認証・認可設定
12 Unary接続のInterceptorを設定
13 Stream接続のInterceptorを設定
14 接続タイムアウト制限時間
15 HTTP/2のHEDAERSリストの無圧縮のサイズ
16 HTTP/2のHEDAERSのサイズ

KeepAliveについて

  • サーバー立ち上げ後、常にKeepAliveチェックを行っている
  • クライアントTCP接続直後にKeepApive設定済ならPINGフレームを送信
  • サーバーサイドも該当クライアントとTCP接続開始直後より、KeepAlive設定情報の間隔でPINGフレームを送付する
  • クライアント、サーバーそれぞれにKeepAlive設定が存在する
    • ただし、サーバーサイドにはKeepAlive強制執行用の設定もある
  • GOAWAYフレームの理由がPingによるものの場合、自動的にKeepAlive制限の設定数か変わる

KeepAlive設定

パラメータ 内容
MaxConnectionIdle GoAwayを送信してアイドル接続が閉じられるまでの時間の長さ。アイドル期間は未処理のRPCの数が最後にゼロになったとき、または接続が確立されたときから定義される(デフォルト:無限)
MaxConnectionAge Go Awayを送信して接続が閉じられるまでの、接続が存在する最大時間の期間。この設定値の0.9〜1.1倍にランダム補正を加えて実施する(デフォルト:無限)
MaxConnectionAgeGrace MaxConnectionAgeの後の追加期間であり、その後は接続が強制的に閉じる(デフォルト:無限)
Time Ping送信間隔。 1秒未満には設定不可
Timeout タイムアウト待機時間

KeepAlive強制執行ポリシー

サーバー側でキープアライブ施行ポリシーを設定するために使用されます。サーバーは、このポリシーに違反するクライアントとの接続を閉じます

パラメータ 内容
MinTime クライアントPING待機制限(デフォルト5分)
PermitWithoutStream trueの場合、サーバーはアクティブなストリーム(RPC)がない場合でもキープアライブpingを許可します。 falseの場合、アクティブなストリームがないときにクライアントがpingを送信すると、サーバーはGOAWAYを送信して接続を閉じます。(デフォルト:false)

https://github.com/grpc/grpc-go/blob/master/keepalive/keepalive.go#L77

MAX_CONCURRENT_STREAMS(同時接続可能なストリーム数)

1TCPコネクション内で多重化させられるストリーム数。
この値を超えるストリーム作成を要するHEADERSフレームが送信されたタイミングで、HTTP/2はPROTOCOL_ERROR またはREFUSED_STREAMのストリームエラーを返します。
同時接続可能なストリーム数は、下記の状態のストリームの数で計算されます。

  • "open"
  • "half-closed"

ストリームの状態に関してはこちらを参照

gRPCでは使用されていないHTTP/2の機能

改めて調べてみてちょっと驚いたのはgRPCでは下記の機能を使用していないことです。

  • サーバープッシュ
  • ストリームの優先順位を指定

サーバープッシュ

gRPCは実はサーバートリガーのプッシュ機能はありません。
1つのTCPコネクションが使い回せることで、何度もリクエストを送信したり、間隔を空けて何度もレスポンス返したりすることで見た目上サーバープッシュに近いことはできるかもしれませんが、あくまでもクライアントからのリクエストとそれに対するサーバーレスポンスの返却を実施するものです。

ストリームの優先順位を指定

複数のストリームに依存関係を持たせたり、ストリームの優先度を設けて制御する機能はgRPCでは利用できません。

ソース上で確認

サーバープッシュ

GoのgRPCパッケージ上におけるクライアント側でのサーバーからのフレーム受信時のソースの一部を抜粋すると下記の通りです。(readerメソッド(*2))

gRPCで使用するすべてのフレームタイプをswitchで判別してますが、サーバープッシュ開始時にサーバーからクライアントに送られるPUSH_PROMISEが存在しません。

http2_client.go
        switch frame := frame.(type) {
        case *http2.MetaHeadersFrame:
            t.operateHeaders(frame)
        case *http2.DataFrame:
            t.handleData(frame)
        case *http2.RSTStreamFrame:
            t.handleRSTStream(frame)
        case *http2.SettingsFrame:
            t.handleSettings(frame, false)
        case *http2.PingFrame:
            t.handlePing(frame)
        case *http2.GoAwayFrame:
            t.handleGoAway(frame)
        case *http2.WindowUpdateFrame:
            t.handleWindowUpdate(frame)
        default:
            errorf("transport: http2Client.reader got unhandled frame type %v.", frame)
        }

https://github.com/grpc/grpc-go/blob/d720ab346fd09f63ecd5b34fcf3696d3d345f938/internal/transport/http2_client.go#L1280

ストリームの優先度

サーバー側でクライアントからのフレーム受信時のソースの一部を抜粋すると下記の通りです。
(handleStreamメソッド(*1))
こちらもクライアントからフレームタイプをswitchで判別してますが、優先順位指定で使用するPRIORITYが存在しません。

http2_server.g
        switch frame := frame.(type) {
        case *http2.MetaHeadersFrame:
            if t.operateHeaders(frame, handle, traceCtx) {
                t.Close()
                break
            }
        case *http2.DataFrame:
            t.handleData(frame)
        case *http2.RSTStreamFrame:
            t.handleRSTStream(frame)
        case *http2.SettingsFrame:
            t.handleSettings(frame)
        case *http2.PingFrame:
            t.handlePing(frame)
        case *http2.WindowUpdateFrame:
            t.handleWindowUpdate(frame)
        case *http2.GoAwayFrame:
            // TODO: Handle GoAway from the client appropriately.
        default:
            errorf("transport: http2Server.HandleStreams found unhandled frame type %v.", frame)
        }

https://github.com/grpc/grpc-go/blob/d720ab346fd09f63ecd5b34fcf3696d3d345f938/internal/transport/http2_server.go#L477

HTTP/2パッケージでは

Goのgrpcパッケージhttp2パッケージを内包してます。
http2パッケージにはhttp2.PushPromiseFrame, http2.PriorityFrameは存在します。

所感

個人的な必要性からgRPCという切り口からまとめてみました。多少に理解のお役に立てれば恐縮です。
gRPCの利用事例はもちろんのことWebSocket over HTTP/2 (RFC8441)などのHTTP/2を使用した他のWebテクノロージーも増えてくると思います。

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