20191203のGoに関する記事は24件です。

Go Modules時代の静的解析

はじめに

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

Go Modulesがデフォルトで有効になったGo1.13がリリースされてはや3ヶ月。皆さんいかがお過ごしでしょうか。私は「Modules何もわからん」となっててんやわんやでした。
この記事ではModulesの変化の影響を受けやすいGoプログラムの静的解析について、入門〜中級者向けに解説していきます。

静的解析とは?

プログラムを実行せずにその内容を解析することを静的解析と言います。コンパイラによる構文解析や型チェックをはじめ、エディタがタイポを検出したり補完機能を提供したりするのも静的解析です。これらとは逆に、ユニットテストや性能評価などはプログラムを実行しているので動的解析と呼ばれます。
Goはgofmtgoimportsなど静的解析ツールが豊富なことでも有名です。

Go Modulesとは?

Go公式の依存パッケージのバージョン管理システムです。Go 1.11から導入され、Go 1.13よりデフォルトで有効になっています。
初期のGoは「常に最新版を使えばええやろ!互換性のない変更を加えるなら別のパッケージを作れ!」と強気な姿勢をとっていましたが、色々あってvendorやgo depsなど準公式ツールでの試行錯誤の末ついに公式に導入されたのがGo Modulesというわけです。

静的解析とModulesの関係

静的解析に際してソースコードまたはパッケージファイル(*.a)を読み込むわけですが、当然依存しているパッケージの情報がないと解析を行えないので、静的解析の実行はパッケージ管理システムに依存します。
つまり、Go 1.10以前の静的解析プログラムはModulesに対応していない場合があるということです。そこで本稿ではModulesに対応済みの(標準・準標準)ライブラリとその使い方を紹介します。

TL; DR

  • レールに乗っかりたい、モジュール化された静的解析ライブラリを作りたい → analysis
  • とにかく動かしたい、基礎を理解したい → go/*, packages

Package go/*

Goが公式にサポートする静的解析用のパッケージ群です。基本的にこの後に紹介するパッケージの内部で使用されるため、ast, token, typesの型を提供するパッケージをメインにさわることになります。

go/* 説明
ast ASTを構成する型とそれらに対する操作
build GOPATHやビルドタグなどの情報
constant 型の指定されていない定数に対する操作
doc ソースコードからドキュメントを収集
format Go標準のフォーマッタ
importer コンパイラ実装(gc, gccgo)ごとのimport処理を抽象化
parser 文字列/ファイル/ディレクトリを読み込んでASTを返す
printer ASTを整形して表示
scanner 字句解析の実装
token 字句解析結果の型とそれらに対する操作
types 型チェッカーの実装と結果の型

注意:これらのうち、importerはModules対応で変更が入っており、importer.For()importer.ForCompiler()に修正する必要があります……が、そもそもimporter.Default()を使っている場合が多いと思うのでそんなに影響はないのではないでしょうか。

Package golang.org/x/tools/go/*

Goの準標準パッケージです。上記の標準パッケージを利用したハイレベルな機能が数多く提供されています。ここではその中でも特に知っていると得するパッケージを紹介します。
標準ライブラリではないため後方互換性が保証されていないと注意書きされてますが、それこそModulesを使えば勝手にアップデートされて動かなくなるということはないので大体の人は心配しなくてもいいでしょう。
Modules対応に際し大きな変更が入り、loaderパッケージが廃止されて代わりにpackagesパッケージが新たに用意されました。

Package analysis

analysis - GoDoc

静的解析プログラムを抽象化し、再利用可能性を高めて他の解析と簡単に組み合わせられるようにします。サブパッケージanalysis/analysistestでは複雑になりがちな静的解析プログラムのテストの記述を支援します。また、サブパッケージanalysis/passes/*としてよく用いられる解析処理を提供しています。
analysis/passes/*の多くはgo vetコマンドにより利用できます。例外であるbuildssa, ctrlflow, inspectは他のAnalyzerに利用されることを前提としたAnalyzerで、新たに静的解析ツールを自作する場合も利用できます。それぞれ後で解説するssacfgast/inspectorが実装の本体となっています。

このパッケージに関してはGoにおける静的解析のモジュール化について - Mercari Engineering Blogが非常によくまとまっていて分かりやすいので一読をおすすめします。

Package ast/astutil

astutil - GoDoc

既存のASTを改変して新しいASTを作る関数Apply()や式(Expr)の括弧を外して中身を取り出す関数Unparen()などASTに関するをユーティリティ提供します。

Package ast/inspector

inspector - GoDoc

ASTをトラバースするためのデータ構造を提供します。標準パッケージのast.Inspect()に比べて、メモ化による複数呼び出しの高速化とノードのフィルタリング、探索スタック付きのトラバースをサポートします。analysis/passes/inspectで利用されているのはこちらになります。
メモ化のためのオーバーヘッドがあるので、同じASTに対して数回しか呼び出さない場合はast.Inspect()のほうが高速なことに注意です。

Package callgraph

callgraph - GoDoc

関数Aの内部で関数Bが呼び出されている、といった関係をグラフとして表現します。グラフの生成の実装はサブパッケージとしてcha, rta, staticの3種類のアルゴリズムが提供されています。

Package cfg

cfg - GoDoc

ある関数内でのifswitchによる分岐をグラフで表現します。ある処理が必ず呼び出されているか検査したいときなどに便利です。ただし論理演算子の短絡評価やpanicによる分岐などはサポートされていないため、それらが必要な場合後述するgolang.org/x/tools/go/ssaを使います。

Package packages

packages - GoDoc

ファイルの探索から字句解析、構文解析そして型チェックを一括で処理してくれる上位APIです。analysisパッケージを用いない場合はこのパッケージが解析の起点になると思います。

Package ssa

ssa - GoDoc

静的単一代入(Static Single-Assignment: SSA)形式の中間表現を生成します。これによりコード解析が容易になります。例えば次のようなコードを解析したときに、strに代入されうる値が"Hello, world!""Hello, 世界!"のどちらかであることが分かります。こういった複数部分にまたがる解析を実行するにはほぼ必須と言えるでしょう。

func hello(world string) {
    str := "Hello, " + world + "!"
    fmt.Println(str)
}

const DEFAULT_WORLD = "world"

func main() {
    var lang string
    fmt.Scan(&lang)
    if lang == "Japanese" {
        hello("世界")
    } else {
        hello(DEFAULT_WORLD)
    }
}

analysisを使わない場合のサンプルコード

analysisを使う場合は上記のMercariさんのブログを見れば十分なので、ここではpackagesを起点にコードを解析するサンプルプログラムを紹介します。
簡単に説明すると、関数とそれを呼び出している関数を見つけてprintしています。ssaパッケージを使うと同じことがより簡単かつ正確にできますが、あくまでサンプルとして見ていただければ幸いです。

package main

import "fmt"
import "go/ast"
import "go/types"
import "golang.org/x/tools/go/packages"

func main() {
    dir := "."
    conf := &packages.Config{
        Dir: dir,
        Mode: packages.NeedName |
            packages.NeedFiles |
            packages.NeedImports |
            packages.NeedDeps |
            packages.NeedTypes |
            packages.NeedSyntax |
            packages.NeedTypesInfo,
    }
    pkgs, err := packages.Load(conf)
    if err != nil {
        fmt.Println("failed to parse dir %s: %w", dir, err)
        return
    }
    if packages.PrintErrors(pkgs) > 0 {
        fmt.Println("Some packages have errors")
        return
    }

    for _, pkg := range pkgs {
        for _, file := range pkg.Syntax {
            scope2fn := make(map[*types.Scope]*types.Func)
            // スコープの情報にアクセス
            filescope, _ := pkg.TypesInfo.Scopes[file]
            // 定義情報にアクセス
            for _, obj := range pkg.TypesInfo.Defs {
                fn, ok := obj.(*types.Func)
                if !ok {
                    continue
                }
                if fn.Scope().Parent() == filescope {
                    // トップレベルの関数定義を集める
                    scope2fn[fn.Scope()] = fn
                }
            }
            // ASTを探索
            ast.Inspect(file, func(n ast.Node) bool {
                switch expr := n.(type) {
                case *ast.CallExpr:
                    var caller, callee string
                    callee = types.ExprString(expr)
                    scope := pkg.Types.Scope().Innermost(expr.Pos())
                    if scope == nil {
                        return true
                    }
                    // forやif、無名関数のスコープは無視
                    for scope.Parent() != nil {
                        if fn, ok := scope2fn[scope]; ok {
                            caller = fn.Name()
                        }
                        scope = scope.Parent()
                    }
                    fmt.Println(caller + " -> " + callee)
                }
                return true
            })
        }
    }
}

まとめ

Goはシンプルな言語仕様のために静的解析がとてもやりやすい言語だと思います。ぜひ皆さんもGoで静的解析に入門しましょう!

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

aetestがめちゃ遅いのでどうにかしたかった話

aetestとは

https://cloud.google.com/appengine/docs/standard/go/tools/localunittesting/reference?hl=ja
GAE/Go用のローカルテストのためのパッケージです。
GAEも第2世代へ移り変わり最早今後は新規に使われることもそう無いであろうパッケージですが供養として過去の苦労を記しておきたいと思います。

以下はGAEのテストサンプルコードです。aetestはテスト実行時にGAE特有の機能をローカルで利用する際のサーバーを立ち上げてくれます。そのサーバーの情報を含んだcontextを利用することでテストが可能になります。

サンプルコードではGAEのMemcacheの機能を利用しています。

main_test.go
package main

import (
    "google.golang.org/appengine"
    "google.golang.org/appengine/aetest"
    "google.golang.org/appengine/memcache"
    "testing"
)

func TestAeTest(t *testing.T) {
    inst, err := aetest.NewInstance(nil)
    if err != nil {
        t.Fatal(err)
    }
    defer inst.Close()

    req, err := inst.NewRequest("GET", "/", nil)

    ctx := appengine.NewContext(req)

    it := &memcache.Item{
        Key:   "some-key",
        Value: []byte("some-value"),
    }
    err = memcache.Set(ctx, it)
    if err != nil {
        t.Fatalf("Set err: %v", err)
    }
    it, err = memcache.Get(ctx, "some-key")
    if err != nil {
        t.Fatalf("Get err: %v; want no error", err)
    }
    if g, w := string(it.Value), "some-value" ; g != w {
        t.Errorf("retrieved Item.Value = %q, want %q", g, w)
    }
}

aetest.NewInstance() の中で何が行われているかというと、簡素なGAEアプリケーション設定を一時領域に作っています。

設定は内部に定義されており、GAE/goのアプリケーションとして最低限の設定で起動されています。

https://github.com/golang/appengine/blob/master/aetest/instance_vm.go#L278-L292

const appYAMLTemplate = `
application: %s
version: 1
runtime: go111

handlers:
- url: /.*
  script: _go_app
`

const appSource = `
package main
import "google.golang.org/appengine"
func main() { appengine.Main() }
`

GAEの開発用サーバーのコマンドが組まれ、
dev_appserver.py dev_appserver.py /var/folders/1w/9n3yj1g95g12zgjhm9xxcypc0000gp/T/tmp4C_Mm8appengine-go-bin/app.yml のようなコマンドを子プロセスで立ち上げています。

開発用サーバーが起動するとAPIなどのURLが標準出力にでるので、正規表現ですっぱ抜いてきてそこにdatastore等の命令を投げるようにしています。結構ワイルドな実装ですね。

INFO     2019-10-10 06:39:59,912 devappserver2.py:278] Skipping SDK update check.
WARNING  2019-10-10 06:39:59,912 devappserver2.py:294] DEFAULT_VERSION_HOSTNAME will not be set correctly with --port=0
INFO     2019-10-10 06:40:00,098 datastore_emulator.py:155] Starting Cloud Datastore emulator at: http://localhost:21206
WARNING  2019-10-10 06:40:00,100 simple_search_stub.py:1196] Could not read search indexes from /var/folders/1w/9n3yj1g95g12zgjhm9xxcypc0000gp/T/appengine.testapp.hoshina/search_indexes
INFO     2019-10-10 06:40:01,231 datastore_emulator.py:161] Cloud Datastore emulator responded after 1.132465 seconds
INFO     2019-10-10 06:40:01,232 api_server.py:275] Starting API server at: http://localhost:64020
INFO     2019-10-10 06:40:01,237 api_server.py:265] Starting gRPC API server at: http://localhost:64022
INFO     2019-10-10 06:40:01,312 dispatcher.py:256] Starting module "default" running at: http://localhost:64023
INFO     2019-10-10 06:40:01,316 admin_server.py:150] Starting admin server at: http://localhost:64025
INFO     2019-10-10 06:40:03,328 stub_util.py:357] Applying all pending transactions and saving the datastore
INFO     2019-10-10 06:40:03,328 stub_util.py:360] Saving search indexes
INFO     2019-10-10 06:40:04,252 instance.py:294] Instance PID: 57474

https://github.com/golang/appengine/blob/master/aetest/instance_vm.go#L150-L151

var apiServerAddrRE = regexp.MustCompile(`Starting API server at: (\S+)`)
var adminServerAddrRE = regexp.MustCompile(`Starting admin server at: (\S+)`)

最初勘違いしていたのですが、aetestは対象のアプリケーションを立ち上げてくれるわけではなく、appengineAPIを利用するのに最低限必要なものを用意してくれるだけです。
なのでアプリケーションのルーティングを再現してくれるわけでもないので、テストで実際のアプリケーションを想定したリクエストを作ってhttp.ClientでDo(req)しても動いたりはしないので注意が必要です。

という挙動をしているaetest.NewInstance()ですが、毎回子プロセスを立ち上げるのでかなり遅いです。
下記のテストでは、2件のテストでそれぞれ5秒程度かかってしまい、実行時間の大半はdev_appserver.pyの起動に費やされています。

main_test.go
package main

import (
    "google.golang.org/appengine"
    "google.golang.org/appengine/aetest"
    "testing"
)

func TestAppID(t *testing.T) {
    ctx, done, err := aetest.NewContext()
    if err != nil {
        t.Fatal(err)
    }
    defer done()
    val := appengine.AppID(ctx)
    if val != "testapp" {
        t.Fatalf("got: %v\nwant: %v", val, "testapp")
    }
}

func TestDefaultVersionHostname(t *testing.T) {
    ctx, done, err := aetest.NewContext()
    if err != nil {
        t.Fatal(err)
    }
    defer done()
    val := appengine.DefaultVersionHostname(ctx)
    if val != "" {
        t.Fatalf("got: %v\nwant: %v", val, "")
    }
}

インスタンスは基本的に使い回せるはずなので、パッケージ変数に作成したインスタンスを保持して、インスタンスを要求された時設定が同じであれば以前に作成したインスタンスを返すことで毎回起動することを避けることができます。

main_test.go
package main

import (
    "context"
    "fmt"
    "google.golang.org/appengine"
    "google.golang.org/appengine/aetest"
    "os"
    "testing"
)

func TestAppID(t *testing.T) {
    ctx, done := NewContext(nil)
    defer done()
    val := appengine.AppID(ctx)
    if val != "testapp" {
        t.Fatalf("got: %v\nwant: %v", val, "testapp")
    }
}

func TestDefaultVersionHostname(t *testing.T) {
    ctx, done := NewContext(nil)
    defer done()
    val := appengine.DefaultVersionHostname(ctx)
    if val != "" {
        t.Fatalf("got: %v\nwant: %v", val, "")
    }
}

var instances map[string]aetest.Instance

func NewContext(opt *aetest.Options) (context.Context, func()) {
    var err error
    key := fmt.Sprintf("%#v", opt)
    //設定が異なる場合違うインスタンスを作成する
    if _, ok := instances[key]; !ok {
        instances[key], err = aetest.NewInstance(opt)
        if err != nil {
            panic(err)
        }
    }

    req, err := instances[key].NewRequest("GET", "/", nil)
    if err != nil {
        panic(err)
    }
    ctx := appengine.NewContext(req)
    return ctx, func() {
    //なにもしない
    }
}

func TestMain(m *testing.M) {
    instances = map[string]aetest.Instance{}
  //パッケージ内のテストを終了
    status := m.Run()

    //goroutineがインスタンスの終了を待っているので最後に終了する
    for _, i := range instances {
        i.Close()
    }
    os.Exit(status)
}

注意が必要なのは、aetest.Instanceは起動した子プロセスが終了するのをgoroutineで待ち続けているため、インスタンスが一つでも起動しているとgoroutineの終了を待ってしまいテストのプロセスを終了することができません。

https://github.com/golang/appengine/blob/master/aetest/instance_vm.go#L114-L120

そのためテストを終了させるためには、必ず最後にインスタンスを終了させる必要があります。

しかしここで問題があります。テスト内でpanicを起こすと、m.Run()の後に行っているインスタンスの終了が行われないため、テストが終了できなくなってしまうのです。

main_test.go
func TestPanic(t *testing.T) {
    panic("from TestPanic")
}

ですのでpanicが起きたらrecover()で捕まえてインスタンスを終了すればいいので、TestMainのdeferすればよいと考え、TestMainを以下のように変更しました。

main_test.go
func TestMain(m *testing.M) {
    status := 0

    defer func() {
        for _, i := range instances {
            i.Close()
        }
        os.Exit(status)
    }()
    instances = map[string]aetest.Instance{}
    status = m.Run()

    //goroutineがインスタンスの終了を待っているので最後に終了する
    for _, i := range instances {
        i.Close()
    }
}

しかしこの変更は無意味です。

通常、テストの中でpanicが起きた場合、テスト結果を出力し即終了しようとしますが、testingとは別のgoroutineでchannelが入力待ちの状態などで停止していると、テストの完了シグナルが送られずにテストプロセス自体が終了できなくなってしまいます。

仮に終了シグナルが返ったとしても、そもそもtestingの各テストはgoroutineで実行されており、goroutineをまたいでrecoverすることはできないので二重に意味のないコードでした。

結局どうしたらいいのかというと、goroutineがサーバーの終了を待たなくて良いようにするか、panicしても制御が戻って来れるようにする必要があります。

前者でやるとすればTestMainで全パターンのサーバーを作成しておくことで解決が可能です。ただし設定のパターンが多いと難しいでしょう。
後はpanicしても明示的にtestingの制御を返すことで親のgoroutineに終了を伝えることができます。

main_test.go
func TearDown(t *testing.T) {
    if err := recover(); err != nil {
        debug.PrintStack() //スタックトレースが出ないので自分で吐いている
        t.Fatal(err)
    }
}

func TestPanic(t *testing.T) {
    defer TearDown(t)
    panic("from TestPanic")
}

マイナーな話なのですが、テストが外部プロセスに依存してテスト実行時にその状態も管理しようとすると似たようなケースに陥るかもしれません。(テスト実行時にデータベースをいろんなパターンで起動したいとか)
その場合、TestMain等で共通の前後処理を行おうとすると例外時にハマることになるというお話でした。

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

Go+Nuxt+etc...を使ってライブ配信サイト+Twitterのコメビュを作ってみた

この記事はJsys Advent Calendar 2019の記事です
Qiita初投稿になりますので色々と突っ込みどころがあるかもしれないです

やったこと

Go+Nuxt+その他諸々を使ってリアルタイムにライブ配信サイト10種+Twitterの特定のハッシュタグでつぶやかれた内容を自動的に表示するソフトフェアを作った

目次

  • 使った言語+フレームワーク
  • 実行に必要な前提要件
  • 使い方
  • 実装方法
  • 感想的な何か

使った言語+フレームワーク

フロントエンド

  • Nuxt.js
    フロントをモダンに書きたいのと使い慣れてるので採用

サーバーサイド

  • Go
    個人的な慣れで採用
    オブジェクト指向のような手続き型のような謎の言語
    モダンな書き方が大体採用されてる
    青いホリネズミがキモカワイイ

  • フレームワーク

    • Gin
      高機能なHTTP/HTTPS通信用フレームワーク
      Nuxtから吐き出した静的ファイルを一行でルーティングできたりする
    • Melody
      小機能のWebsocket通信用ライブラリ
      Javascriptライクな実装ができたり、セッション情報を一時的に保存できる機能が元々あるなど地味に便利

前提要件

  • IFTTT
    スプレッドシートにTwitterのデータをぶっこ抜くのに使用
    Twitter・Google Driveとのインテグレーションをオンにしておくこと
  • Google Service Account
    スプレッドシートからローカルサーバーへデータを転送するのに使用
  • MultiComment Viewer
    Youtube等の配信サイトからのデータをぶっこ抜くのに使用
  • HTML Comment Generator
    コメントのデータをひっぱるのに使用

使い方

  1. マルチコメントビューワーでHTML5コメジェネのパスを通す
  2. マルチコメントビューワーで通常通りコメントを収集する
  3. localhost:8080にアクセスする
  4. コメントが表示されるようになる
  5. カラーキーを指定して背景をぶっこ抜くことでOBS上で一定時間以上コメントがない場合は自動的に透明になるよ!

実装方法

  1. localhostにアクセスされたらconfigを読み出し、Websocketでの通信に切り替える
  2. TwitterからIFTTTを通してスプレッドシートに送られたデータと、マルチコメビュから出力されたxmlを読み込む
  3. 2で得られたデータの今までに送信したデータとの差分を取って残りをwebsocketで送る
  4. 受け取ったらキューに格納して一定時間ごとに取り出す
  5. キューの中にデータが一定時間以上なくなったら表示部分をopacity: 0;にして消去

感想的な何か

そもそもローカルでしか動かないのにサーバーサイドとは()
現在アプリケーションとして開発中です・・・

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

Goの基本個所の復習①(println)

はじめに

私は現在Ruby on railsJavaScript(jQuery)の学習を行っていますが、今回息抜きを兼ねてGoの学習を始めてみました。
学んだ基本事項を、自分なりにかみ砕いて解説していこうと思います。

実行

Goの最も簡単なコードは下記の通りです。

package main

func main() {
   println("Hello World")
   println(2019)   
}

これを実行すると、

Hello World
2019

と出力されます。

package mainの部分をパッケージ定義部分func main()の部分を関数定義部分といいます。(これの詳しい解説は今の私にはできないので、いつかの記事で行います)

println()が二つ並んでいますが、これは()の中身を出力するというものでRubyでいうputsやJSでいうconsole.log()に相当します。
(ちなみに、printInではなくprintlnなので注意を)

今回は()の中身にそれぞれHello World2019が入力されているので、出力は前述の通りとなります。

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

Firebase AuthenticationとGAEで30分くらいでログイン機能を実装してみる

TL;DR

今からFirebase Authenticationを使ってソーシャルログイン機能を30分くらいで検証用に実装します。
各種SNSのデベロッパーサイトへの登録はしてあるものとします。
要件として作成するのは、トップページと、ログイン後ページの2画面のシンプルな構成。

スクリーンショット 2019-12-03 22.13.11.png

コードは置いといた。(secret.yamlは準備してくれ・・・)
https://github.com/spre55/firebase-auth-sample/

今回使う主な技術・ツール・サービス

  • HTML/CSS/JavaScript
  • Go 1.13
  • Firebase Authentication
  • Google App Engine (Standard Edition)

Google App Engine(以下GAE)はGCP上で提供されるPaaSです。
デプロイ時にバージョンを指定でき、トラフィックを別バージョンに切り返すことで、Blue/Greenデプロイなども簡単に行えます。
今回はGAEのStandard Editionを使います。Goをサポートしているので、バックエンドはGoを使ってみます。

更に、今回はソーシャルログイン機能ということで、認証を簡単に実装できるようにしてくれるFirebase Authenticationを使います。
アプリはもちろん、クライアントSDKはWebもサポートしているので、JavaScriptを使用して、簡単に認証機能を実装することができます。複雑な要件がなければ、ドキュメントのコピペで終わることでしょう。

実装する機能

  • メール/パスワードサインアップ・ログイン
  • Twitterログイン
  • Googleログイン
  • Facebookログイン
  • Githubログイン
  • ログアウト

アプリケーションの構築

今回は以下のようなシステム構成を目指します。
赤い部分が我々がコードで表現するアプリケーション部分です。
それ以外の部分は各種コンソール画面から、ボタンポチポチで実現できます。

firebase_gae.png

各種ソーシャルプロバイダの登録

まずは以下のソーシャルプロバイダのデベロッパーサイトへ登録し、APIキーなどを使用可能な状態にしてください。詳細な手順は省きます。

GAE/Go で Hello World !

以下からGoogle Consoleにログインしてチュートリアルを行えば、10分経たずで全世界にHello Worldできるかと思います。(実際にタイムアタックを試したら、クレジットカード登録も含め8分程度でできました)
https://cloud.google.com/appengine/?hl=ja

まずは、チュートリアルを終え、GoのアプリケーションをGoogle App Engineでデプロイしてみましょう。話はそれから。

GCPは一年間の無料期間が設定されてます。まだ触ってない人はこれを機にいじってみましょう。

ドキュメントに記載されているクイックスタートにもあると思いますが、公式のGithubアカウントにて、最小構成のhelloworldアプリやその他もろもろが公開されているので、これをそのままクローンしてデプロイできますね。
https://github.com/GoogleCloudPlatform/golang-samples/tree/master/appengine/go11x/helloworld

Firebase Consoleでの認証の設定

https://console.firebase.google.com/u/0/

Firebaseのクラウドコンソールにアクセスしてみましょう。

スクリーンショット 2019-12-03 2.46.54.png

"ログイン方法"のタブにいくと、各種プロバイダのログイン方法が指定できますので、必要なものを有効にしましょう。今回は最初に述べた、「メール/パスワード」、「Google」、「Twitter」、「Facebook」、「Github」の5つを有効にしましょう。
ここで有効にするために、プロバイダによっては、APIキー及び、シークレットキーなどを求められるので、最初に各種デベロッパーサイトで設定して入手したAPIキーとシークレットキーを入力しましょう。

スクリーンショット 2019-12-03 2.47.23.png

とりあえず画面を2つつくる

すでに作成したGAEにてHello Worldを表示するアプリケーションが存在する前提で進みます。

とりあえずGoからサーバサイドレンダリングでHTMLの内容を表示するだけでいいので、一旦、今回作りたいトップページと、ログイン後ページのガワを作ります。

構成はこんな感じ。

├── app.yaml
├── main.go
└── templates
    ├── my.html
    └── signup.html

各ファイルは以下の通り。

app.yaml
runtime: go113

templateディレクトリを作り、その配下に
サインアップ前のトップページと、

signup.html
<!DOCTYPE html>
<html>
<body>
    <h1>Welcome to Sign Up Page!</h1>
</body>
</html>

サインアップ後に入れるマイページを作って、

my.html
<!DOCTYPE html>
<html>
<body>
    <h1>Welcome to My Page!</h1>
</body>
</html>

main.goには、ルーティングまわりを記述。

main.go
package main

import (
    "fmt"
    "log"
    "net/http"
    "os"
    "text/template"
)

func main() {
    http.HandleFunc("/", indexHandler)
    http.HandleFunc("/my", myHandler)

    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
        log.Printf("Defaulting to port %s", port)
    }
    log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil))
}

// indexHandler ... サインアップページ(トップページ)
func indexHandler(w http.ResponseWriter, r *http.Request) {
    if r.URL.Path != "/" {
        http.NotFound(w, r)
        return
    }

    t := template.Must(template.ParseFiles("templates/signup.html"))
    if err := t.ExecuteTemplate(w, "signup.html", nil); err != nil {
        log.Fatal(err)
    }

}

// myHandler ... サインアップ後のマイページ
func myHandler(w http.ResponseWriter, r *http.Request) {
    if r.URL.Path != "/my" {
        http.NotFound(w, r)
        return
    }

    t := template.Must(template.ParseFiles("templates/my.html"))
    if err := t.ExecuteTemplate(w, "my.html", nil); err != nil {
        log.Fatal(err)
    }
}

https://github.com/spre55/firebase-auth-sample/tree/feature/20191203_page_only

GAEにデプロイして、動作するようならオッケー。

Firebase を使える状態にする

Firebaseを使えるようにするため、まずはクライアントSDKをCDNで呼ぶ。そのためにFirebaseコンソールからコードをコピってきてアプリケーション内に埋め込みたい。

まずFirebaseコンソールプロジェクトの設定をみる。

スクリーンショット 2019-12-03 3.24.37.png

Settingページへ遷移するので、「アプリを追加」ボタンを押す。

スクリーンショット 2019-12-03 3.25.15.png

今回はWebアプリなのでWebを選択。

スクリーンショット 2019-12-03 3.25.25.png

適当にアプリの名前を決めると、以下のような画面になるので、ここで表示されたコードを、自分のアプリケーションに埋め込む。(今回はFirebase Hostingは使わないので未チェックでOK)

スクリーンショット 2019-12-03 3.25.51.png

アプリケーションに上記のコードを埋め込んで動作させる

上記コードはGithubで公開とかしないならベタ書きでもいいのですが、あんまり見られたくないので、secret.yamlに環境変数として記述し、それをapp.yamlで読み込むようにします。
また、secret.yamlをGithubに公開したら意味ないので、.gitignoresecret.yamlを記載して、git管理対象から除外します。
複数人で共有する場合は、secret.yamlを暗号化するか、別の仕組みを使うかする必要がありますが、今回のアプリケーションではそこまではやりません。

最終的なディレクトリ構造はこんな感じ。

├── app.yaml
├── go.mod
├── main.go
├── secret.yaml
├── .gitignore
└── templates
    ├── my.html
    └── signup.html

secret.yamlはこんな感じで書いておいて、

secret.yaml
env_variables:
  API_KEY: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
  AUTH_DOMAIN: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.firebaseapp.com"
  DATABASE_URL: "https://XXXXXXXXXXXXXXXXXXXXXXX.firebaseio.com"
  PROJECT_ID: "XXXXXXXXXXXXXXXXXXXXXXX"
  STORAGE_BUCKET: "XXXXXXXXXXXXXXXXXXXXXXX.appspot.com"
  MESSAGING_SENDER_ID: "XXXXXXXXXXXXXXXX"
  APP_ID: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

app.yamlでそれを読み込みます。

app.yaml
env_variables:
runtime: go113

includes:
- secret.yaml

secret.yaml.gitignoreに。

secret.yaml
secret.yaml

次にmain.go内にてos.Getenvで環境変数を読み込み,signup.htmlmy.htmlに渡したいので、

main.go
 ... ()
    if err := t.ExecuteTemplate(w, "signup.html", getEnvVars()); err != nil {
 ... ()

func getEnvVars() map[string]string {
    return map[string]string{
        "apiKey":            os.Getenv("API_KEY"),
        "authDomain":        os.Getenv("AUTH_DOMAIN"),
        "databaseURL":       os.Getenv("DATABASE_URL"),
        "projectId":         os.Getenv("PROJECT_ID"),
        "storageBucket":     os.Getenv("STORAGE_BUCKET"),
        "messagingSenderId": os.Getenv("MESSAGING_SENDER_ID"),
        "appId":             os.Getenv("APP_ID"),
    }
}

signup.html
...(略)
    <script>
        const config = {
            apiKey: "{{.apiKey}}",
            authDomain: "{{.authDomain}}",
            databaseURL: "{{.databaseURL}}",
            projectId: "{{.projectId}}",
            storageBucket: "{{.storageBucket}}",
            messagingSenderId: "{{.messagingSenderId}}",
            appId: "{{.appId}}",
        };

        firebase.initializeApp(config);
...()

こんな感じ。

各種機能の実装

ここまでで、各プロバイダとFirebaseを使う準備はできました。
あとは機能の実装です。

今回実装するのは以下の機能でした。

  • メール/パスワードサインアップ・ログイン
  • Twitterログイン
  • Googleログイン
  • Facebookログイン
  • Githubログイン
  • ログアウト

firebaseuiを使って実装してみたいと思います。

signup.html にて、uiConfig.signInOptionsに各種PROVIDER_IDを指定してあげれば、ソッコーで
ログインボタンのUIが出来上がります。

signup.html
...(略)
        const uiConfig = {
            callbacks: {
                signInSuccessWithAuthResult: function (authResult, redirectUrl) {
                    return true;
                },
                uiShown: function () {
                    document.getElementById('loader').style.display = 'none';
                }
            },
            signInFlow: 'popup',
            signInSuccessUrl: 'my',
            signInOptions: [
                firebase.auth.GoogleAuthProvider.PROVIDER_ID, // Google
                firebase.auth.FacebookAuthProvider.PROVIDER_ID, // Facebook
                firebase.auth.TwitterAuthProvider.PROVIDER_ID, // Twitter
                firebase.auth.GithubAuthProvider.PROVIDER_ID, // Github
                firebase.auth.EmailAuthProvider.PROVIDER_ID, // Email/Password
            ],
        };
        // サインアップ/サインインフォームのUI
        const ui = new firebaseui.auth.AuthUI(firebase.auth());
        ui.start('#firebaseui-auth-container', uiConfig);
...(略)

firebaseuiを使用すると、自分でCSSとかをいろいろいじったりする必要はありません。
スクリーンショット 2019-12-03 4.08.11.png

サインイン状態は以下のように、firebase.auth().onAuthStateChanged()を使用してとってこれます。
ログアウトも、firebase.auth().signOut()を呼ぶだけですね。

my.html
... (略)
        // ユーザのサインイン状態の判定
        firebase.auth().onAuthStateChanged(function (user) {
            if (user) {
                // サインインできてるとき
                console.log('sign in');
            } else {
                // No user is signed in.
                console.log('sign out');
                location.href = "/";
            }
        });

        // ログアウト
        function signOut() {
            firebase.auth().signOut().then(function () {
                console.log('success sign out.');
            }).catch(function (error) {
                console.log('error: ', error);
            });
        }
... (略)

ログインページができました。
まあ、検証用では十分ですね。

まとめ

GoogleAppEngineとFirebaseAuthenticationを使うと簡単にソーシャルログイン機能が作れます。
レンタルサーバーとか借りようと思ってる人、ためしに使ってみては?

困ったら

信頼性の高い公式ドキュメントを見ましょう。
ただし翻訳のタイムロスが無い分、ドキュメントは日本語のものより英語のもののほうが情報が新しかったりするので注意してください。

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

GoでtwitterAPI触ってみた

Goの勉強も含めて、Twitter APIを利用して、「アオキ大好き!最高!」とツイートしている人を全員フォローしてみせます。

anacondaというパッケージを使います。

anacondaとは

Pythonのあれじゃないです。

anacondaとは、Twitter APIにアクセスするためのGoパッケージです。便利!

Twitter APIの利用申請

間違いなく一番めんどくさい過程。
こちらを見ながら頑張りましょう。結構テキトーでも通ります。

実装

main.go
package main

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "net/url"
    "strconv"

    "github.com/ChimeraCoder/anaconda"
)

type TwitterAccount struct {
    AccessToken       string `json:"accessToken"`
    AccessTokenSecret string `json:"accessTokenSecret"`
    ConsumerKey       string `json:"consumerKey"`
    ConsumerSecret    string `json:"consumerSecret"`
}

func main() {
    raw, error := ioutil.ReadFile("./twitterAccount.json")
    if error != nil {
        fmt.Println(error.Error())
        return
    }

    var twitterAccount TwitterAccount
    json.Unmarshal(raw, &twitterAccount)

    api := anaconda.NewTwitterApiWithCredentials(twitterAccount.AccessToken, twitterAccount.AccessTokenSecret, twitterAccount.ConsumerKey, twitterAccount.ConsumerSecret)
    v := url.Values{}
    v.Set("count","10000")
    // v.Set("exclude","retweets")
    searchResult, _ := api.GetSearch(`"アオキ大好き!最高!"`, v)
    fmt.Println(strconv.Itoa(len(searchResult.Statuses))+"件ヒットしました!!")
    for _, tweet := range searchResult.Statuses {
        fmt.Printf("%d\n", tweet.User.Id)
        api.FollowUserId(tweet.User.Id,v)
        fmt.Println("--------------------------------------------------------------")
    }
}

twitterAccount.json
{
  "accessToken": "*****************",
  "accessTokenSecret": "*****************",
  "consumerKey": "*****************",
  "consumerSecret": "*****************"
}
結果
   0件ヒットしました!!

今回作成したリポジトリはこちらから

参考:
anaconda github
anaconda Doc

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

Go言語の標準パッケージだけで画像処理をする その1 (入出力)

ZOZOテクノロジーズ #5 Advent Calendar 2019の記事です。
昨日は @satto_sann さんの「Alexaのアカウントリンクをデバッグする方法」でした。

本記事では、Go言語の標準パッケージでの画像処理について書いていきます。

はじめに

最初に明言しておきますが、これはGo言語での画像処理を勧める記事ではありません。
あくまで、自分にメリットがありそうという方のみ参考にしていただければと思います。

自分はZOZOテクノロジーズの中でも、アパレル生産におけるファクトリーオートメーションという分野を担当しています。

担当分野が広いというのと、自動化の対象がアパレル関係ということもあり、
開発環境を実行環境に入れることができなかったり、実行環境により使う言語を変えたりと手間がかかってしまう場面がよく発生します。

特に画像処理関係でプロトタイプをサクッと作る際には、開発環境をその都度作るのが面倒だったりします。

そんな手間を無くせないかなーとふと思い、本記事シリーズを書くことにしました。

A36FD16C-29EF-4FA3-89A6-717EDFF2409B.jpeg

Go言語 image パッケージ
Package image

ファイルの入力

filepathを利用することで、os間のパス違いを意識せずにコードが書けます。
自分は以下のソースのようにdataファイルを用意し、その中に処理したい画像を置いて読み込ませる方法をよく使います。
プロトタイプを軽く動かす時なんかは便利です。

package main

import (
    "image"
    "log"
    "os"
    "path/filepath"
)

func main() {

    // 実行ファイル直下のパスを取得
    path, _ := os.Getwd()
    dataPath := filepath.Join(path, "data")
    // dataフォルダ直下の画像を一括で読み込む
    jpgPath := filepath.Join(dataPath, "*.jpg")
    jpegPath := filepath.Join(dataPath, "*.jpeg")
    pngPath := filepath.Join(dataPath, "*.png")

    fileList, _ := filepath.Glob(jpgPath)
    f, _ := filepath.Glob(jpegPath)
    fileList = append(fileList, f...)
    f, _ = filepath.Glob(pngPath)
    fileList = append(fileList, f...)

    // 画像を1枚づつ読み込む
    for _, file := range fileList {
        // 画像を読み込む
        img := Input(file)
        println(file, " : " , img)
    }
}

// 画像を読み込む処理
func Input (filePath string) image.Image {

    // 画像を読み込む
    file, err := os.Open(filePath)

    // 画像が読み込めなかった場合
    if err != nil {
        log.Fatal(err)
    }

    // 変換
    img, _, err := image.Decode(file)

    return img
}

ファイルの出力

出力したい拡張子によって処理を分けます。
ここではjpgとpngの出力を紹介します。

const imgQt int = 60

func Output (outputImage image.Image, filePath string, format string) {

    dst, err := os.Create(filePath)

    if err != nil {
        log.Fatal(err)
    }

    switch format {
    case "png":
        // PNGの場合
        err = png.Encode(dst, outputImage)
        if err != nil {
            log.Fatal(err)
        }
        break
    case "jpg":
        // JPGの場合
        qt := jpeg.Options{
            Quality:imgQt,
        }
        err = jpeg.Encode(dst, outputImage, &qt)
        if err != nil {
            log.Fatal(err)
        }
        break
    default:
        // 標準で対応していないフォーマットの場合
        log.Fatal("Unsupported format.")
    }
}

画素値の参照

以下では読み込んだ画像から画素値を参照したり、操作を行なったりしています。

func EditPixel(img image.Image) image.Image {

    // アウトプット画像を定義
    size := img.Bounds()
    // 画像のサイズを変える場合
    // size.Max.X = "任意の数値"
    // size.Max.Y = "任意の数値"
    outputImage := image.NewRGBA(size)

    // 画像の左上から順に画素を読み込む
    for imgRow := size.Min.Y; imgRow < size.Max.Y; imgRow++ {
        for imgCol := size.Min.X; imgCol < size.Max.X; imgCol++ {

            // 画素を取得
            pixel := img.At(imgCol, imgRow)
            println("pixel : ", pixel)
            r, g, b, a := pixel.RGBA()
            println("r : ", r, "g : ", g, "b : ", b, "a : ", a)

            // 画素を操作
            var outputColor color.RGBA64
            // 例(真っ黒な画像にする)
            outputColor.R = 0
            outputColor.G = 0
            outputColor.B = 0
            outputColor.A = 100

            outputImage.Set(imgCol, imgRow, outputColor)
        }
    }

    return outputImage
}

おわりに

ZOZOテクノロジーズ #5 Advent Calendar 2019
明日は「Go言語の標準パッケージだけで画像処理をする その2 (回転、反転)」という記事を書きます。

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

Goの並行処理パターンを使って速攻で分かった気になる

この記事はチームラボエンジニアリングアドベントカレンダー10日目の記事です。
昨日の記事は、bonobono555さんによるCompletableFutureの公式ドキュメントを読んでも分からないので Java逆引きレシピを片手に理解するでした。

はじめに

趣味でGoを触るようになって半年ぐらいは経ったと思います!
こんなにハマったのは並行処理がめちゃくちゃ面白かったからです。

関数型でも基本的に上から処理されますよね?
(手続き型だと、基本上から、関数型は高階関数多用してると上からでは無くなりますが)
それがgoroutineを使うことによって簡単に並行に処理をさせる事が出来るんです!
これを、他の言語で同じ事やろうとしたらすごく難しいんです!(やった事ないから知らんけど)

とりあえずムチャクチャ楽しいんでこの記事がその入り口にでもなってくれたら嬉しいです!
この記事では詳しい説明より要点をまとめた並行処理パターンを紹介します!

並行処理とは? 並列処理と混ざってない?

突然ですが、並行と並列の違い分かりますか??

一言で言うと、
並行処理は1人がその場の判断で沢山の仕事をこなす事
並列処理は複数人が仕事を同時にこなす事
です!

並行処理はあくまで処理を同時に行ってはいないんです。
この様に処理を切り替えて行います。(人の目には同時に見える)
並行処理.png

それに比べ、並列処理は処理を同時に行っています。
並列処理.png

これが並行と並列のザックリとした違いです。

goroutineとは?

goroutine (ゴルーチン) は Go ランタイムによって管理される軽量スレッドです。スレッドのようでスレッドでなく、coroutine のようで coroutine ではありません。しかし、スレッドで coroutine を実行するかのごとく利用することが可能で、かつそれを簡単な構文で実現できます。

個人的にはこの説明が一番しっくり来ました!

詳しい説明はここでは行いませんが、goroutineの大元はメルヴィン・コンウェイさんのcoroutineの考え方を参考にしているみたいです!

syncパッケージ

まあ大枠はここまでにして(自分が説明するより良記事がある)早速実際に動かしてみましょう!

syncパッケージはGoの標準パッケージに用意されています。
相互排他ロック(チャネルのブロックの様な動き)をしてくれたり、複数のゴルーチンを扱う上でブロックを提供してくれたりします。
goroutineってほっとくと簡単に終わってしまったり(厳密には消えないけど)想定と違うタイミングで変数を上書きする事もあります。

これは説明するより見る方が早いです!

これ↓を実行するとどうなると思いますか??

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

"Hello"が出力されないんです!
ここでのgoroutineはメインゴルーチン(main関数)によって生成されているので、メインゴルーチンが終わると終わってしまいます。

こんな風に並行処理を始めると今まで見ないタイプの想定外に出くわすことが多いです!

syncパッケージを実際に使って解決してみましょう!

WaitGroup型

WaitGroupを使うとWait()を呼び出すと内部のカウンターが0になるまでブロックしてくれます!

func main() {
  var wg sync.WaitGroup

  wg.Add(1) // 内部のカウンターを1インクリメント
  go func() {
      defer wg.Done() // 内部のカウンターを1デクリメント
      fmt.Println("Hello")
  }()

  wg.Wait() // 内部のカウンターが0になるまで待機
}

先ほどのコードをこのように書き直すと出力される様になりましたね!

他にも頻出するのは
- Mutex & RWMutext型
- Cond型
- Once型
- Pool型
などなどが挙げられると思いますが、ここら辺の記事はたくさんあるのでそちらを見て見てください。

channel

並行処理を使う上で通信によってメモリ共有を提供してくれる代物です。
チャネルの説明としてとても分かりやすかったのが

水が流れる川のように、チャネルは情報の流れの水路として機能します。値はチャネルに沿って 渡され、そこから下流に読み込まれます。

本当にこの通りでストリームの考え方と近いかも知れません。

また、Goの設計思想でも下記の様に述べられています。

Do not communicate by sharing memory; instead, share memory by communicating.

メモリを共有することで通信しようとしないこと。代わりに通信することでメモリを共有すること。

チャネルのクセとか並行処理で気をつける事とかあれこれ

この後からはすぐに使える並行処理パターンを紹介していきます!

その前に最低限のチャネルの特性や並行処理で気をつける事などを抑えておきましょう。

ブロック

チャネルはブロックして、待つ事が出来ます。

(読み込みの場合)
↓で書き込みの処理(3行目)が無いと5行目でデッドロックを引き起こしてしまいます!

stringStream := make(chan string)
go func() {
  stringStream <- "Hello, World" // チャネルに流す
}()
fmt.Println(<-stringStream) // stringStreamに何か(string)が流れるまでブロックされる

チャネルのブロックを解消する方法は他にも

close(stringStream)

の様にチャネルを閉じてあげる事で <-stringStreamの部分が検知する事が出来ます。

また、配列と同じ様にチャネルもrangeで回す事が出来ます。

for str := range stringStream {
  fmt.Println(str)
}

rangeのfor文も先程と同じ様に、closeを検知してループが終了します。

また、バッファ付きチャネルは宣言時に決めたn個が入るまでブロックされます。

intStream := make(chan int, 5) // バッファが5
for integer := range intStream { // intStreamに5つ流れるまでブロックされる(ループが始まらない)
  fmt.Println(integer)
}

select文

これから紹介する並行処理パターンではselect文も欠かせません!

select {
case <-channel1:
  fmt.Println("channel1 set up")
case <-channel2:
  fmt.Println("channel2 set up")
}

select文はどのチャネルが準備出来たかを評価します。
どのチャネルも準備出来ていない場合はselect文全体がブロックします。
これはイメージ湧きづらいと思うので、並行処理パターン紹介の時に出て来た時にみて見てください!

チャネルには読み込み専用と書き込み専用とその両方行えるものがあります。
どのチャネルにどの権限を与えるかもしっかりと考えた方が良いです。
しかし、便利な事に以下の様にして権限の割り振りが出来ます。

ch := make(chan string) // 書き込み、読み込み両方できる
hoge() := func(stringStream <-chan string) { // 引数に取ったチャネルが読み込み権限のみ
  fmt.Println(<-stringStream) // 書き込みは出来ない 
}(ch) // 普通に渡せる!

使ったら片付ける!

goroutineってほっとくと簡単に終わってしまったり(厳密には消えないけど)想定と違うタイミングで変数を上書きする事もあります。

先程上で厳密には消えないと述べましたが、これには理由があります。
ゴルーチンはランタイムによってガベージコレクションされないので片付け無ければなりません。
いくら軽量だからと言って放置する事は思わぬメモリリーク(ゴルーチンリーク)を引き起こす事になりかねません。

あるgoroutineがgoroutineの生成の責務を担うのであれば、そのgoroutineを停止出来るようにする責務も担わなくてはなりません。(名言)

goroutine生成元(親)から子への終了のシグナルには慣習としてdoneという名前の読み込みチャネルにします。
インターフェイス型はgolangのコミュニティでも敬遠されるイメージがありますが、doneチャネルに関しては問題無いかと思ってます。

doSomething := func(done <-chan interface{}, stringStream <-chan string) {
  for {
    select {
    case s := <-stringStream: // 今回は関係無いです
      fmt.Println(s)
    case <-done: // close(done)を検知し、doSomethingのgoroutineを終了する
      fmt.Println("doSomething 終了")
      return
    }
  }
}

done := make(chan interface{})

go func() { // 5秒後にdoneチャネルを閉じる
  time.Sleep(5 * time.Second) 
  close(done)
}()

doSomething(done, nil) // 5秒後に終了する

こんな風に親から子を終了させます。
任意のタイミングで複数のgoroutineを(ほぼ)同時に終了させることも簡単に出来ますね!

並行処理パターン

お待たせしました!
ちょっと長くなりましたが、明日から使える並行処理パターンを紹介していきます。
並行処理パターンではステージを並べたパイプラインを意識します。
ステージは受け取るものと返すものが同じ型でなければなりません。
パイプラインを使う事で各ステージでの懸念事項を切り分けることが出来ます。

いきなりステージとか言われてもなんやよう分からんですよね。。
そこで最初に意識して欲しいのはジェネレーターです。
パイプラインの始めにはチャネルへの変換を必要とするデータの塊が必ずあるはずです。
その責務を担うのが、ジェネレーターです。

// intを受け取り、intのチャネルを返す
generator := func(done <-chan interface{}, integers ...int) <-chan int {
  intStream := make(chan int, len(integers)) // generator goroutineの中で返却用のチャネルを作る
  go func() { // 別goroutineでintStreamへ値を流していく
    defer close(intStream)
    for _, i := range integers {
      select {
      case <-done:
        fmt.Println("generator goroutine終了")
        return
      case intStream <- i:
      }
    }
  }()
  return intStream
}

これだけでも並行処理の恩恵に感動しませんか!?
今まで「配列作って、値入れて、そこからfor文で処理する」としていた物が「配列(チャネルだけど)に値を流しながら、順番にfor文で処理する」出来るんです!
単純に考えて早くなりそうですよね!

このチャネルを使ってパイプライン処理を書いてみましょう!

// ただintStreamから流れてきた物に引数の値(multiplier)を乗算しているだけです笑
// 今までfor文で回していたような処理ですね!
multiply := func(done <-chan interface{}, intStream <-chan int, multiplier int) <-chan int {
  multipliedStream := make(chan int)
  go func() {
    defer close(multipliedStream)
    for i := range intStream {
      select {
      case <-done:
        return
      case multipliedStream <- i * multiplier;
      }
    }
  }()
  return multipliedStream
}

done := make(chan interface{})
defer close(done)

intStream := generator(done, 1, 2, 3, 4, 5)
pipeline := multiply(done, intStream, 2)

for v := range pipeline {
  fmt.Println(v)
}

これが並行処理の基本の一連の流れです!
雛形とまでは言えませんが、中の処理を変えるだけで、様々な処理が出来ると思います。
- ステージ
- パイプライン
- ジェネレーター
- doneチャネル
この要点を抑えるだけで綺麗な並行処理を行う事が出来ます!
これがパターンです!!!笑
ステージをもっと増やして組み合わせても問題ありません。

素数を5000まで出力すると、並行処理とブロック処理で2倍以上早かったです!

generator := func(done <-chan interface{}, num int) <-chan int {
  intStream := make(chan int)
  go func() {
    defer close(intStream)
    for i := 0; i < num; i++ {
      select {
      case <-done:
        return
      case intStream <- i:
      }
    }
  }()

  return intStream
}

// 素数判定(あってるかな?)
isPrimeNum := func(num int) bool {
  if num == 1 || num == 2 {
    for i := 2; i < num; i++ {
      if num%i == 0 {
        return false
      }
    }
  }

  return true
}

primeFinder := func(done <-chan interface{}, valueStream <-chan int) <-chan int {
  intStream := make(chan int)
  go func() {
    defer close(intStream)
    for v := range valueStream {
      if isPrimeNum(v) {
        intStream <- v
      }
    }
  }()

  return intStream
}

done := make(chan interface{})
defer close(done)

// ステージを複数使う時は重ねて記述しましょう!
for v := range primeFinder(done, generator(done, 5000)) {
  fmt.Println(v)
}

自身もまだまだなので間違いがあるかも知れませんが、温かく見守って頂ければと思います。
最後までありがとうございました!

明日の記事はyanananaさんによるジェネリクスを見ても逃げないです。

参考

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

Goで便利なTUIファイラーを作ったのでその実装の話

こんにちわ。ゴリラです。
最近GoでシンプルなTUIファイラーを作ったので、軽く紹介して実装の話をしていきます。

以前書いたこちらの記事でも軽く紹介しています。

機能

ざっくり以下の機能を持っています。

  • ディレクトリ、ファイルの作成、削除、コピー、リネーム、プレビュー
  • ディレクトリのブックマーク

詳しくはREADMEを参照してください。

実装について

筆者はTUIツールを作るときに、いつもtviewというライブラリを使っています。
tviewの基本的な使い方については別途記事を書く予定なので、本記事ではそれ以外でいくつかの機能の実装について解説していきます。

ファイル一覧

image.png

ioutil.ReadDir(dirname string) ([]os.FileInfo, error)を使ってパス配下のディレクトリ・ファイル情報を取得しています。
戻り値のos.FileInfoは以下のようにインターフェイスになっていて、ディレクトリ及びファイルの情報を取得できます。

// A FileInfo describes a file and is returned by Stat and Lstat.
type FileInfo interface {
    Name() string       // base name of the file
    Size() int64        // length in bytes for regular files; system-dependent for others
    Mode() FileMode     // file mode bits
    ModTime() time.Time // modification time
    IsDir() bool        // abbreviation for Mode().IsDir()
    Sys() interface{}   // underlying data source (can return nil)
}

FileInfoのメソッドについて解説していきます。

  • Size()
    Size()で取得できるのはバイト数になっていますが、そのままだと分かりづらいです。
    そこで、サイズを人間が読みやすい形に変換するdustin/go-humanizeといライブラリを使って読みやすい形式に変換しています。
    こちらのライブラリはサイズ以外にもいろんな情報を人間が読みやすい形に変換したりパースできます。詳細はREADMEを参照してください。

  • Mode()
    Mode()はファイルモードの情報os.FileModeを返しますが、そのまま出力すると上位1bitしか表示されません。
    ディレクトリならd--------が表示されます。os.FileModeにはString()が生えているので、それを使うとlsコマンドでみるような形式drwxr-xr-xが表示されます。

  • OwnerGroup
    Sys()syscall.Stat_tに型アサーションしてOwnerGroupを取ります。Sys()の実態はOSによって異なります。Windowsの場合はsyscall.Win32FileAttributeData型にアサーションします。
    Stat_tからはUidGidを取得できるので、それをもとにos/userパッケージのLookupIdLookupGroupIdを使用してユーザIDとグループIDから名前を取得します。

// get file owner, group
if stat, ok := file.Sys().(*syscall.Stat_t); ok {
    uid := strconv.Itoa(int(stat.Uid))
    u, err := user.LookupId(uid)
    if err != nil {
        owner = uid
    } else {
        owner = u.Username
    }
    gid := strconv.Itoa(int(stat.Gid))
    g, err := user.LookupGroupId(gid)
    if err != nil {
        group = gid
    } else {
        group = g.Name
    }
}

もう少し詳しく知りたい方はGoならわかるシステムプログラミングを読んでみてください。

ファイル、ディレクトリの操作

ファイル、ディレクトリのリネーム、新規作成などはosパッケージの関数を使用すれば簡単です。

操作 関数
ファイル作成 os.Create(name string) (*os.File, error)
ディレクトリ作成 os.Mkdir(name string, perm os.FileMode) error
ファイルの削除 os.Remove(name string) error
ディレクトリの削除 os.RemoveAll(path string) error
リネーム os.Rename(oldpath, newpath string) error

1つ問題なのはファイル、ディレクトリのコピー関数が用意されていないことです。そもそもコピーのシステムコールがないからだと思います。
で、io.Copyを使えば中身のコピーはできますが、属性のコピーまではできないです。面倒だったので属性のコピーも含めてやってくれるotiai10/copyというライブラリをを使いました。

ブックマーク

ffbでディレクトリをブックマークしておくことができます。ブックマークはデフォルトでは無効になっているので、
以下のようにconfig.yamlに設定を追記する必要があります。

bookmark:
  enable: true
  file: $XDG_CONFIG_HOME/ff/bookmark.db

このブックマークはsqlite3のDBを使っています。ffを起動するときにfileに設定したファイルをDBファイルとして使います。このファイルはなくても自動で作成するようになっています。以下の実装はブックマークの初期化処理になっています。

file = os.ExpandEnv(file)
// if db file is not exist, create new db file
if !system.IsExist(file) {
    if _, err := os.Create(file); err != nil {
        log.Println(err)
        // if can't create new file, use in memory db
        file = ":memory:"
    }
}

db, err := gorm.Open("sqlite3", file)
if err != nil {
    log.Println(err)
    return nil, err
}
db.SetLogger(DBLogger{})
db.LogMode(true)

if err := db.AutoMigrate(&Bookmark{}).Error; err != nil {
    log.Println(err)
    return nil, err
}

ファイルを作成できないときは:memory:で、インメモリにしています。メモリ上に一時的にDBを作成して使います。
正直インメモリのメリットは皆無なので、ここは改善しようかなと思っています。

ちなみに、sqlite3は面白いことに、空のファイルをDBとして使うことができます。
モックアプリを作る時にとても便利だなと思いました。

DBの操作はGORMというORMを使っています。標準のパッケージでも使おうかなと思いましたが、慣れているライブラリに頼りました。ちゃんとdatabase/sqlを勉強したい…

プレビュー

ffは引数-previewもしくは以下の設定をconfig.yamlに追加することでプレビュー機能を有効にすることができます。

preview:
  enable: false
  # preview colorscheme. you can use colorscheme following
  # https://xyproto.github.io/splash/docs/all.html
  colorscheme: monokai

プレビューではファイル、ディレクトリの中身をプレビューできます。ファイルはシンタックスハイライトされます。
ただ、大きすぎるファイルを開くと固まってしまうのでプレビューしないようしています。

シンタックスハイライトはalecthomas/chromaというライブラリを使っています。
ライブラリの使い方についてはREADMEを参照してください。
GoでソースコードをANSIエスケープシーケンスでハイライトしたいときに便利です。

具体的な実装は次のようになります。chromaはANSIの文字列を返しますが、そのままtviewに出力しても色がつかず、ANSIもそのまま出力されます。tviewでも色が表示できるようにtview.TranslateANSI()を使う必要があります。

func (p *Preview) Highlight(entry *Entry) string {
    // Determine lexer.
    b, err := ioutil.ReadFile(entry.PathName)
    if err != nil {
        log.Println(err)
        return err.Error()
    }

    ext := filepath.Ext(entry.Name)
    l := lexers.Get(ext)
    if l == nil {
        l = lexers.Analyse(string(b))
    }
    if l == nil {
        l = lexers.Fallback
    }
    l = chroma.Coalesce(l)

    // Determine formatter.
    // TODO check terminal
    f := formatters.Get("terminal256")
    if f == nil {
        f = formatters.Fallback
    }

    // Determine style.
    s := styles.Get(p.colorscheme)
    if s == nil {
        s = styles.Fallback
    }

    it, err := l.Tokenise(nil, string(b))
    if err != nil {
        log.Println(err)
        return err.Error()
    }

    var buf bytes.Buffer

    if err := f.Format(&buf, s, it); err != nil {
        log.Println(err)
        return err.Error()
    }

    return tview.TranslateANSI(buf.String())
}

ファイル編集

e$EDITORを使ってファイルを編集できますが、Vimの場合は少し面白い動きをします。
Vimにはターミナルの機能があり、Vim上でターミナルを開きコマンドを実行することができます。
通常のターミナルなのでその中で更にVimを立ち上げることもできます。つまりVim in Vimになってしまうんです。
ffをVimのプラグインとして使う時、選択したファイルを起動中のVimで開きたいと思う方が多いかと思います。

そこで、Vimのterminal api機能を使って、選択したファイルを実行しているVimで開くようにします。
詳細は省きますが、Vimのターミナル上でsh -c 'echo "\x1b]51;[\"drop\",\"main.go\"]\x07"'を実行するとmain.goが開きます。便利ですね。
Goの場合はexecパッケージを使って実行します。

// if `ff` running in vim terminal, use running vim
if os.Getenv("VIM_TERMINAL") != "" && editor == "vim" {
    cmd := exec.Command("sh", "-c", fmt.Sprintf(`echo '\x1b]51;["drop","%s"]\x07'`, file))
    cmd.Stdout = os.Stdout
    return cmd.Run()
}

何を言っているのかよくわからない方もいると思いますが、以下のgifを見てみてください。
それでもよくわからんって方はぜひ試してみてください。

ff-in-vim-edit.gif

コマンド実行

ffは任意の外部コマンドを実行する事ができます。内部では単にexec.Commandを使っているだけです。

text := cmdline.GetText()
if text == "" {
    return
}

cmdText := strings.Split(text, " ")

// expand environments
for i, c := range cmdText[1:] {
    cmdText[i+1] = os.ExpandEnv(c)
}

cmd := exec.Command(cmdText[0], cmdText[1:]...)

buf := bytes.Buffer{}
cmd.Stderr = &buf
cmd.Stdout = &buf
if err := cmd.Run(); err == nil {
    cmdline.SetText("")
}

result := strings.TrimRight(buf.String(), "\n")
if result != "" {
    gui.Message(result, cmdline)
}

例えばmkdir -p a/b/cというようにディレクトリ階層を作りたい場合はコマンドのが早いです。そういった柔軟な使い方が出来るようにしたいためコマンド機能を実装しましたが、1点問題があります。
それは標準入力、出力を使用できないことです。
それによりVimといった標準入力を使うようなコマンドを実行するとffが固ります。
実装側で防ぐ方法があるなら良いのですが、今のところベストな対処法は思いつかないです。

一応READMEに記述していますが、ユーザに気をつけてねっていうような機能は果たしてどうなんだろう?というモヤモヤ感は自分の中にあるので将来的にはこの機能を廃止するかもしれないです。

まとめ

ざっくりですが、実装について軽く解説しました。ファイラーを作ることで、ファイルシステムまわりについて知見を得られたので、作ってよかったと感じています。
ただ、ファイルシステムの深いところは全然わからないので(例えばなぜコピーのシステムコールがないのかなど)、そこは引き続き勉強していこうと思います。

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

効率の悪い append は prealloc という linter で見つけることができるぞ

本稿は、Go2 Advent Calendar 2019 の 2 日目の記事です。
@pankona という名前でやっています。よろしくお願いします!

prealloc という linter について紹介します。

効率の悪い append

さて、Go を書いていると一度はやってしまうであろう "あまりよくないプラクティス" のひとつとして、いわゆる "append の連打" があるかと思います。
append を何度も何度も繰り返すようなコードは、ひらたく言えば "処理効率の悪いコード" になってしまう可能性があります。

package main

// append を繰り返すことによって効率が悪くなってしまう例

func AppendAndAppend() {
    var a []int

    // 1000 万回の append が行われていく
    for i := 0; i < 10000000; i++ {
        a = append(a, i)
    }
}

上記のスニペットには、以下のような問題点・改善点があります。

  • append の際、配列の cap が足りないときは内部配列が確保しなおされます。内部配列の確保し直しは比較的コストの高い処理であり、何度も行うと時間が掛かります。
  • 可能限り少ない回数で済ますのが (できれば一度で済ますのが)、処理効率の観点では良いと考えられます。

この例の場合は繰り返しの回数 (=作られる配列の長さ) が分かっているので、以下のように書き換えることができます。

package main

// append による内部配列の再確保を抑える書き方

func MakeEnoughLen() {
    // make にて len をあらかじめ確保しておく
    a := make([]int, 10000000)

    for i := 0; i < 10000000; i++ {
        // append せずに直接代入する
        a[i] = i
    }
}

あるいは、以下のように書いても同等の処理速度が得られそうです。

package main

// append による内部配列の再確保を抑える書き方

func MakeEnoughCap() {
    // make にて cap をあらかじめ確保しておく
    a := make([]int, 0, 10000000)

    for i := 0; i < 10000000; i++ {
        // append を呼ぶが、cap が十分に確保されているので内部配列の再確保は行われない
        a = append(a, i)
    }
}

上記の 3 関数を Benchmark すると、以下のような結果が得られます。

BenchmarkAppendAndAppend-12     58911350            18.0 ns/op        42 B/op          0 allocs/op
BenchmarkMakeEnoughLen-12       624822230           3.49 ns/op         8 B/op          0 allocs/op
BenchmarkMakeEnoughCap-12       618240639           3.91 ns/op         8 B/op          0 allocs/op

最初にあげた例 (AppendAndAppend 関数) がダントツで遅いのがお分かりいただけるかと思います…!

golangci-lint で検出できるよ! (ただし要有効化)

さて、効率の悪い append があるのは分かりましたが、これらは人間が目ヂカラを使って検出し、手ずから直していく必要があるのでしょうか。
否、実はこのパターンは lint によって発見することができます…!(手ずから直す必要はある)

取り出だしたるは、みなさんご存知 golangci-lint です。
golangci-lint は、最大で 40 個弱くらい (2019/12 現在) の Go の linter を一気に実行してくれる便利ツールです。

golangci-lint の中には prealloc と呼ばれる linter が含まれています (ただし、デフォルトでは無効になっています) 。

prealloc は、前項で紹介したような "ループ内で append を行っている、かつ繰り返し回数が明らかである" ような append にまつわる効率を改善できそうなコード を検出します。

まず、デフォルトで無効にされてしまっている prealloc を有効にするための golangci-lint の設定ファイルを紹介します。

# prealloc はデフォルトで無効なので、設定ファイルで有効にする
# prealloc を有効にするような golangci-lint の設定ファイルはたとえば以下

linters:
  enable:
    - prealloc

linters-settings:
  prealloc:
    simple: true
    range-loops: true
    for-loops: true

いざ、以下のソースコードを実験台にしてみます。

package main

func AppendAndAppend() {
    var a []int
    for i := 0; i < 10000000; i++ {
        a = append(a, i)
    }
}

件の設定ファイルを置いた状態で golangci-lint を実行すると、以下のような指摘があがります。

$ golangci-lint run
main.go:4:2: Consider preallocating `a` (prealloc)
    var a []int

a は事前に確保したほうがええやろ、と怒られました。なるほど便利…。

指摘を踏まえて、以下のように十分な cap を確保するようにしたところ、linter に怒られることはなくなりました。

package main

func AppendAndAppend() {
    a := make([]int, 0, 10000000)
    for i := 0; i < 10000000; i++ {
        a = append(a, i)
    }
}

なるほど便利…。

注意事項

prealloc という便利な linter について紹介しましたが、
実は golangci-lint の README.md の prealloc の項には以下のように注意書きがされています。

prealloc:
    # XXX: we don't recommend using this linter before doing performance profiling.
    # For most programs usage of prealloc will be a premature optimization.

要約すると、

  • この linter を使う前に、まずパフォーマンスのプロファイリングをするのが先でしょう
  • prealloc を使うのは多くの場合において早すぎる最適化だろう

とのことであります。わかる。

個人的には、常に append を効率良く書いておくことは早すぎる最適化の話とは違うんではないかと思わないでもないですが、
そもそも Go の append が雑な書き方でも割と速いこともあり、気にするのは後にしても良かろうというのはそのとおりかもしれません。

まとめ

prealloc という linter について紹介しました。早すぎる最適化はいかがでしょうか…!
気になった方は、ぜひちょいと試してみたらいいんじゃないかなと思います!Let's give it a try!

Go2 Advent Calendar 2019、3 日目は shibukawa さんです!

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

【Go言語】作成したpackageをimportして使用。「cannot find module for path」エラー時の解決

◇ 流れ

【Go言語】packageを作成

importに「./XXXXX」でパス指定

実行

「cannot find module for path」と怒られる

解決

◇ ディレクトリ構成

:file_folder: GOPATH
 ┣━ :file_folder: bin
 ┣━ :file_folder: pkg
 ┗━ :file_folder: src
    ┗━ :file_folder: test
       ┣━ :file_folder: route
       ┃   ┗━ :page_facing_up: route.go
       ┣━ :page_facing_up: go.mod
       ┗━ :page_facing_up: main.go

◇ go環境

ターミナル
$ go version
go version go1.13.4 windows/amd64

$ go env
set GO111MODULE=on
set GOBIN=C:\GOPATH\bin
set GOENV=C:\Users\XXXXXX\AppData\Roaming\go\env
set GOEXE=.exe
set GOHOSTARCH=amd64
set GOOS=windows
set GOPATH=C:\GOPATH
set GOROOT=C:\Go

◇ go.mod を作成。

プロジェクトルートに、$ go mod initgo.modを作成。

ターミナル
$ cd C:\GOPATH\src\test

$ go mod init
go: creating new go.mod: module test
go.mod
module test

go 1.13

◇ main.go と route パッケージを作成。

main.go
package main

import "./route"

func main() {
    route.Route()
}
route/route.go
package route

import (
    "github.com/labstack/echo"
    "net/http"
)

func Route() {
    e := echo.New()

    e.GET("/", getHandler)

    e.Logger.Fatal(e.Start(":1323"))
}

// サイトを表示させる
func getHandler(c echo.Context) error {
    return c.String(http.StatusOK, "Hello, World!")
}

◇ 実行

ターミナル
go run main.go
go: finding golang.org/x/crypto latest
build _/c_/GOPATH/src/test/route: cannot find module for path _/c_/GOPATH/src/test/route

cannot find module for pathとエラーがでて怒られた。。。

パスはあっているのに何故か、
package の読み込みが出来ていないのでかなり詰まりました。

↓↓↓↓↓ 解決策 ↓↓↓↓↓↓

go.modにmodulの名前があるので
「./route」を「test/route」に置き換える

go.mod
module test ← modulの名前

go 1.13
main.go
package main

import "./route" → "test/route"  に変更

func main() {
    route.Route()
}

参考サイト

go 1.11のmodules(vgo)が有効な環境で相対importが cannot find module for path でエラーになった話。
https://pod.hatenablog.com/entry/2018/12/26/074944

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

[golang] 言語仕様から見るunittestとClean Architectue

概要

GolangでServiceを作る中で感じたunittestおよびClean Architectureという壁についてまとめてみた。

Golangにおけるテストとモック

『Unittestのために、モジュール間参照はインターフェースを使わなければならない』

Golangでunittestを書こうと思ったとき、困るのがモックだ。
例えばJavaであれば、どんなClassもそれを継承したClassを(無理やり)作る事ができるので、対象のコードの中の依存オブジェクトをモックに差し替える事ができる。
例えばPythonやJavaScriptであれば、そもそも型がないので自由にモックに差し替える事ができる。

ところが、Golangでは、

type Sample struct{}
var hoge = &Sample{}

としてしまうと、hogeにはSample構造体以外のものに差し替える事ができなくなる。構造体への参照には多態性はないのだ!

このことは、あるモジュールをテストするとき、そのモジュールが他のモジュールを構造体直参照していると「モックに差し替える事ができない」事を意味する。

だから、我々はこう書かなければならない。

type Sample interface {
   hoge()
}
type sample{} struct
func (s *sample) hoge() {
   // hoge
}
var hoge Sample = &sample{}

このように参照を構造体ではなくインターフェースを通した参照にしなければならない。

これがまずGolangでプログラムを書く際のきまりごとである。

Dependency Injection by Constructor

モジュールが別のモジュールを参照しているとき、モジュール間連携は必ずConstructによってDIしよう。

type A interface {
   Hoge() string
}
type a struct {
}
func (*a) Hoge() string {
   return "hoge"
}
type Sample struct {
   a A
}
func NewSample(a A) *Sample {
   return &Sample{a}
}

こうすれば、テストコード内でNewSample関数によって自由にmock_aを差し込んだSampleを作成できる。

def main(){
   a := &mockA{}
   sut := NewSample(a)
}

そして、production codeでは、一番外側のmain.goの中でDIしよう。

DIコンテナが欲しくなる時もあるが、それは別の話で

関数をモックと差し換えるOptionを用意する

例えばメソッドの中でtime.New()していたりuuid.NewV4()していたりするコードは多いと思う。これをテストでモックしようと思うと、monkey.Patchする必要が生じる。だがmonkey.Patchはどうも動作が不安定な気がしている。これは例えば

type Sample struct {
   uuid : func() (uuid.UUID, error)
   now : func() time.Time
}

func NewSample(opts ...SampleOption) *Sample {
   s := &Sample{
      uuid: uuid.NewV4
      now: time.Now
   }
   for _, f := range opts {
      f(s)
   }
   return s
}
type SampleOption func(*Sample)

func WithUUID(uuid : func() (uuid.UUID, error)) SampleOption {
   return func(s *Sample){
      s.uuid = uuid
   }
}
func WithNow(now : func() time.Time) SampleOption {
   return func(s *Sample){
      s.now = now
   }
}

func (s *Sample) Hoge() Fuga {
   now := s.now()
   id, err := s.uuid()
}

こんな風にして、、テストするときは

   s := NewSample(
      WithUUID(func() {
         return uuid.FromString("aaaa-aaaaa-...aaaaaa-aaaa")
      }),
      WithNow(func() {
         return time.Date(2000, 1, 1)
      }),
   )

これで、モジュールの依存先がstructであろうがfuncであろうが、コンストラクタを通してモックと差し換える事ができる。

Cyclic importとディレクトリ分割

Golangを書いていると、頻繁にcyclic import(循環参照)が発生する。
最もよく見るのが以下のようなパターン。

interfaceがimplを参照している

// interface

type Sample interface {
  hoge(options ...Option)
}
// impl
type sample struct{}
func (s *sample) hoge(option Option){
   // hoge
}
type Option func(s Sample)

例えばこんな風に書いてしまうと

interface ==> impl ==> interface

と循環参照になる。つまり

『implは必ずinterfaceを参照するのだから、interfaceはimplを参照してはならない。interfaceに登場するoptionやenumのような値は、interfaceで定義しなければならない』

cyclic import問題から、以下のことが言える(まあこういう言語は珍しくないのでGolang独自とは言えないかもしれないが)

『ディレクトリ内のファイル間では依存性問題は発生しない。ディレクトリを分割するなら、あらかじめディレクトリ間の依存の方向性を決定しておかなければならない』

言わずもがなと思うが、あるディレクトリ下に配置されるファイル間では、インポート問題は発生しないので、cyclic import問題は、プロジェクトを複数のディレクトリに分割しようとしたときにはじめて発生する。

そして、各ディレクトリを定義して、それぞれのディレクトリ間の参照の方向性を事前に決定しなければならない、ということがcyclic import問題から「Golangにおける開発の前提」であるという事が言える。

Clean Architecture in my opinion

以上を見ると、Clean Architectureというのは実はGolang開発の前提条件+αに見えないだろうか?

Clean Architectureについて解説はしないが、
同心円の絵は、結局cyclic importを避けるためにディレクトリ間の依存の方向をコントロールするための提案である。これらの名称であったり、それぞれのモジュールが必要という事ではない。必要ないならhandlerだけでもいいし、handler ==> repository参照していても構わない。

右下の絵はモジュール間連携は必ずinterfaceを経由しようと言っているに過ぎない。Goland開発では当たり前のことだ。

結論

Golangの言語仕様は特殊のため、開発するうえで制限がある。その制限と戦うためには、どうしてもClean Architectureのような概念が必要なのだろうと思う。

Sprint bootやDjangoのようなフレームワークを使えば、フレームワークがディレクトリ構成を規定してくるが、Golang開発ではそういう規定はないっぽい

主要なライブラリ(aws-sdk-goも含めて)には、たいていインターフェースが含まれている事が多いと感じる。使用したい外部ライブラリがあったら、直接参照せずに済まないかインターフェースを探してみよう。

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

本番環境で使えるgRPC-Gateway【前編】

VISITS Technologies Advent Calendar 2019 3日目は@istshが担当します。

最近業務で、echoからgRPC-Gatewayに移行することになり、interceptorやmetadataについていろいろ調べたので、
gRPC Serverの実装(前編)gRPC Clientの実装(後編)に分けてお送りしようと思います。

以降の説明は、サンプルコードを使って行います。

gRPC-Gatewayとは

gRPC-Gatewayとは、gRPCで書かれたAPIを、JSON over HTTPのAPIに変換して提供するためのミドルウェアです。

https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_317035_1efa8277-e25c-8a68-7956-e9460bd4626c.png
(よく見る図)

インストール

$ go get -u -v github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway
$ go get -u -v github.com/golang/protobuf/protoc-gen-go

protoc-gen-validate

$ # fetches this repo into $GOPATH
$ go get -d github.com/envoyproxy/protoc-gen-validate
$ # installs PGV into $GOPATH/bin
$ make build

protoファイル

user.proto
syntax = "proto3";

package user;
option go_package = "github.com/istsh/go-grpc-sample/app/pb/v1/user";

import "google/api/annotations.proto";
import "validate/validate.proto";

service UserService {
    rpc CreateUser(CreateUserRequest) returns (CreateUserResponse) {
        option (google.api.http) = {
            post: "/v1/user"
            body: "*"
        };
    }
}

message CreateUserRequest {
    string email = 1 [(validate.rules).string = { min_len: 3, max_len: 254 }];
    string password = 2 [(validate.rules).string = { min_len: 8, max_len: 64 }];
}

message CreateUserResponse {
}

goファイルを生成

$ protoc \
    proto/v1/login/login.proto \
    -I . \
    -I $GOPATH/src/github.com/envoyproxy/protoc-gen-validate \
    -I $GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
    -I $GOPATH/src/github.com/grpc-ecosystem/grpc-gateway \
    --go_out=plugins=grpc:$GOPATH/src \
    --validate_out="lang=go:$GOPATH/src" \
    --grpc-gateway_out=logtostderr=true:$GOPATH/src

これを実行すると、login.pb.gologin.pb.validate.goが生成されるはずです。

gRPC Serverの実装

ここからは実際のコードを見ながら解説していきます。
一部抜粋して説明するので、コード全体が見たい場合はサンプルコードを見てください。

main.go
package main

// 省略...

func main() {
    db := connectDB()
    defer db.Close()

    r := persistence.NewDBRepository(db)
    u := usecase.NewUserUsecase()

    listenPort, err := net.Listen("tcp", ":9090")
    if err != nil {
        logrus.Fatalln(err)
    }

    s := newGRPCServer(r, u)
    reflection.Register(s)
    s.Serve(listenPort)
}

上記のコードは、gRPC Serverをポート9090で起動するコードです。
DBの部分は説明する必要はないと思うので割愛します。

newGRPCServer*grpc.Serverを返してくれるので、それを用いて任意のポートでサーバーを起動します。
ではnewGRPCServerの実装を見てみましょう。

main.go
func newGRPCServer(r repository.Repository, u usecase.UserUserCase) *grpc.Server {
    s := grpc.NewServer(
        grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(
            interceptor.RequestIDInterceptor(),
            interceptor.AuthenticationInterceptor(),
            grpc_validator.UnaryServerInterceptor(),
            grpc_recovery.UnaryServerInterceptor(),
        )),
    )

    loginpb.RegisterLoginServiceServer(s, server.NewLoginServiceServer(r, u))
    userpb.RegisterUserServiceServer(s, server.NewUserServiceServer(r, u))

    return s
}

上記のコードは、インターセプターを4種類定義し、loginとuserで使うように設定しています。
RegisterLoginServiceServerRegisterUserServiceServerは、前述のコマンドで生成したgoファイルに実装されているので、それを呼び出すだけです。
また、生成されたgoファイルには、LoginServiceServerUserServiceServerといったインターフェースが定義されいます。
それを実装したうえでそれぞれを初期化する関数がNewLoginServiceServerNewUserServiceServerになっています。

ここまではいろんな解説ページに載っていることですが、インターセプターに関しては情報が少ないのと、導入するプロジェクトに必要なインターフェースを選択したり、場合によっては実装することになるので、以降は上記の4種類のインターセプターについて解説します。

RequestIDInterceptor

requestid_interceptor.go
// RequestIDInterceptor is a interceptor of access control list.
func RequestIDInterceptor() grpc.UnaryServerInterceptor {
    return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
        requestID := requestid.RequestID(ctx)
        ctx = context.WithValue(ctx, log.CtxRequestIDKey, requestID)
        return handler(ctx, req)
    }
}
requestid.go
// RequestID takes a request id from metadata.
func RequestID(ctx context.Context) string {
    md, ok := metadata.FromIncomingContext(ctx)
    if !ok {
        return unknownRequestID
    }

    key := strings.ToLower(DefaultXRequestIDKey)
    header, ok := md[key]
    if !ok || len(header) == 0 {
        return unknownRequestID
    }

    requestID := header[0]
    if requestID == "" {
        return unknownRequestID
    }

    return requestID
}

上記のインターセプターは、contextからmetadataを取得し、そこにx-request-idというキーがあれば、その値を返し、なければ<unknown>を返すというコードです。
後編で不足分は解説しますが、gRPC ClientとHTTP Request Headerなどの情報を連携する為にmetadataを使います。

AuthenticationInterceptor

authentication_interceptor.go
// Authenticator provides Authenticate method.
// Each service should implement this interface, otherwise, all requests will be rejected with authentication error.
type Authenticator interface {
    Authenticate(ctx context.Context, req interface{}) (context.Context, error)
}

// AuthenticationInterceptor is a interceptor of authentication.
func AuthenticationInterceptor() grpc.UnaryServerInterceptor {
    return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
        authenticator, ok := info.Server.(Authenticator)
        if !ok {
            // If Service doesn't implement Authenticator, return InternalServerError always.
            return nil, status.New(codes.Internal, "Authenticator is not implemented").Err()
        }

        ctx, err := authenticator.Authenticate(ctx, req)
        if err != nil {
            return nil, status.New(codes.Unauthenticated, fmt.Sprintf("Not authenticated: %v", err)).Err()
        }

        return handler(ctx, req)
    }
}

上記のインターセプターは、Authenticateインターフェースを実装したサービスで認証の処理を実行するコードです。
非常にシンプルですが、各サービス毎に認証の処理を実装できるようになっています。

grpc_validator.UnaryServerInterceptor

これはgithub.com/grpc-ecosystem/go-grpc-middleware/validatorのインターセプターです。
goファイルの生成コマンドで、--validate_outオプションをつけることで生成される*.pb.validate.goの検証処理をやってくれます。
protoファイルでvalidateを使っている場合はこのインターフェースは必ず使いましょう。

grpc_recovery.UnaryServerInterceptor

これはgithub.com/grpc-ecosystem/go-grpc-middleware/recoveryのインターセプターで、panicをハンドリングしてくれます。

ChainUnaryServer

s := grpc.NewServer(
    grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(
        interceptor.RequestIDInterceptor(),
        interceptor.AuthenticationInterceptor(),
        grpc_validator.UnaryServerInterceptor(),
        grpc_recovery.UnaryServerInterceptor(),
    )),
)

これまでの4つのインターセプターを順番に実行するために、grpc_middleware.ChainUnaryServerを使います。

まとめ

これでgRPC Server側の実装は一通り解説しました。
大切なのは、これはgRPC Serverについての説明であって、gRPC Client(gRPC-Gateway)についての説明は別ということです。
また、コードをみて気がついたかもしれませんが、紹介したインターセプターはgRPC Server用のものです。
後編でgRPC Clientのインターセプター(ログ、リクエストIDの採番など)も解説するので、お楽しみに。

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

GKE上にNFSを構築する方法

別の記事で永続ディスク(Persistent Disk)設定方法を紹介しましたが、永続ディスクの制限は複数ノードからマウントして同時に読み書きできない。
本記事では、NFS(Network File System)を利用して、その制限を解決する方法を紹介致します。

実施手順

  • Persistent Disk作成
  • Persistent Diskフォーマット
  • Persistent Diskを使って、NFSサーバ立ち上げ
  • NFSを使って、GKE中にストレージを作成
  • GKEのストレージマウントのPod作成

最初の2つのステップ(Persistent Disk作成、とPersistent Diskフォーマット)は別の記事で紹介したため、それらの記事を参照してください。

※本手順はGCPとgcloudコマンドの利用経験があることが望ましいです。

ワークフォルダの構成

nfs_on_gke
├── README.md
├── cloudbuild.dummyjob.yaml
├── deployment
│   ├── dummy_job_01.deployment.yaml
│   └── dummy_job_02.deployment.yaml
├── dummyjob.Dockerfile
├── job
│   └── dummyjob.go
├── nfs-container.deployment.yaml
├── nfs-service.deployment.yaml
└── nfs-volume.yaml

1. Persistent Diskを使って、NFSサーバ立ち上げ

nfs-container.deployment.yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: nfs-server
spec:
  replicas: 1
  selector:
    matchLabels:
      role: nfs-server
  template:
    metadata:
      labels:
        role: nfs-server
    spec:
      containers:
      - name: nfs-server
        image: gcr.io/google_containers/volume-nfs:latest
        ports:
          - name: nfs
            containerPort: 2049
          - name: mountd
            containerPort: 20048
          - name: rpcbind
            containerPort: 111
        securityContext:
          privileged: true
        volumeMounts:
          - mountPath: /exports
            name: mypvc
      volumes:
        - name: mypvc
          gcePersistentDisk:
            pdName: gce-nfs-disk
            fsType: ext4
nfs-service.deployment.yaml
apiVersion: v1
kind: Service
metadata:
  name: nfs-server
spec:
  ports:
    - name: nfs
      port: 2049
    - name: mountd
      port: 20048
    - name: rpcbind
      port: 111
  selector:
    role: nfs-server
  type: LoadBalancer

GKEにデプロイ

# クラスタ作成
gcloud container clusters create ds-gke-small-cluster \
    --project ds-project \
    --zone asia-northeast1-b \
    --machine-type n1-standard-1 \
    --num-nodes 1 \
    --enable-stackdriver-kubernetes

# k8sコントロールツールをインストール
gcloud components install kubectl
kubectl version

# GKEのクラスタにアクセスするため、credentialsを設定
gcloud container clusters get-credentials --zone asia-northeast1-b ds-gke-small-cluster

# デプロイ NFSサーバ
kubectl apply -f nfs-container.deployment.yaml

# 外からアクセスするため、NFSサービスをデプロイ。クラスタ内でアクセスしかない場合、このステップをスキップ
kubectl apply -f nfs-service.deployment.yaml

デプロイ後の確認
gcp_gke_kubernetes_nfs_devsamurai_001.png
gcp_gke_kubernetes_nfs_devsamurai_002.png

2. NFSを使って、GKE中にストレージを作成

nfs-volume.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs-data-volume
spec:
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteMany
  nfs:
    server: "xx.xx.xx.xx"
    path: "/exports"
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: nfs-data-volume
spec:
  accessModes:
    - ReadWriteMany
  storageClassName: ""
  resources:
    requests:
      storage: 10Gi

ボリューム定義実施

# PersistentVolumeとPersistentVolumeClaimを作成。複数接続で読み書きできるaccessModes(ReadWriteMany)
kubectl apply -f nfs-volume.yaml

定義実施後確認
gcp_gke_kubernetes_nfs_devsamurai_003.png

3. GKEのストレージマウントのPod作成

golangで簡単なジョブを作成する。
ジョブ処理はoutput-pathにサンプル10ファイルを作成する。予定は複数ジョブを稼働して、共有用のNFSにファイルを作成する。

dummyjob.go
package main

import (
    "flag"
    "fmt"
    "io/ioutil"
    "log"
    "os"
    "time"
)

var jobName = flag.String("job-name", "", "specify job name")
var outputPath = flag.String("output-path", "", "specify output path for job")

func main() {

    flag.Parse()

    outlog("Dummy Job start ...")

    for i := 0; i < 10; i++ {
        unixtime := time.Now().Unix()
        fileName := fmt.Sprint(*jobName, "_", unixtime, ".txt")

        // make a file
        file, err := os.Create(*outputPath + "/" + fileName)
        if err != nil {
            log.Fatal(err)
        }
        outlog("created a file: ", fileName)

        // out some
        file.WriteString("hello from " + *jobName)
        file.Close()

        // sleep to delay process
        time.Sleep(2 * time.Second)
    }

    // list all file in path
    outlog("List all files:")
    files, err := ioutil.ReadDir(*outputPath)
    if err != nil {
        log.Fatal(err)
    }

    for _, file := range files {
        outlog(file.Name())
    }

    outlog("Dummy Job finished.")

}

func outlog(args ...string) {
    log.Println(*jobName+":", args)
}
dummyjob.Dockerfile
FROM alpine:latest
WORKDIR /app
COPY ./dummyjob /app
cloudbuild.dummyjob.yaml
steps:
# go build
- name: golang:1.12
  dir: .
  args: ['go', 'build', '-o', 'dummyjob', 'job/dummyjob.go']
  env: ["CGO_ENABLED=0"]

# docker build
- name: 'gcr.io/cloud-builders/docker'
  dir: .
  args: [
         'build',
         '-t', '${_GCR_REGION}/${_GCR_PROJECT}/${_GCR_IMAGE_NAME}:${_GCR_TAG}',
         '-f', 'dummyjob.Dockerfile',
         '--cache-from', '${_GCR_REGION}/${_GCR_PROJECT}/${_GCR_IMAGE_NAME}:${_GCR_TAG}',
         '.'
        ]

# push image to Container Registry
- name: 'gcr.io/cloud-builders/docker'
  args: ["push", '${_GCR_REGION}/${_GCR_PROJECT}/${_GCR_IMAGE_NAME}']

substitutions:
  # # GCR region name to push image
  _GCR_REGION: asia.gcr.io
  # # Project ID
  _GCR_PROJECT: project-abc123
  # # Image name
  _GCR_IMAGE_NAME: dummy-job
  # # Image tag
  _GCR_TAG: latest

イメージをビルドして、2つのジョブをデプロイする

# build dummy-job image on Container Registry
gcloud builds submit --config cloudbuild.dummyjob.yaml

# deploy job
kubectl apply -f deployment/dummy_job_01.deployment.yaml
kubectl apply -f deployment/dummy_job_02.deployment.yaml

ビルドイメージの確認
gcp_gke_kubernetes_nfs_devsamurai_004.png

デプロイジョブはGKEの中に確認
gcp_gke_kubernetes_nfs_devsamurai_005.png

2つのジョブはNFSを共有利用できることを稼働ログで確認できます。
gcp_gke_kubernetes_nfs_devsamurai_006.png

NFSを共有して読み書きできることを確認できました。

本記事で利用したソースコードはこちら

https://github.com/itdevsamurai/gke/tree/master/nfs

最後まで読んで頂き、どうも有難う御座います!

DevSamurai 橋本

関連記事:GKEの中で永続ディスク(Persistent Disk)の利用方法

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

Go言語 理解度チェックテスト1

Go言語入門者向けの簡単なチェックテストについて解説します。

まずはテスト1!

package main

func main() {s
    as := []struct{ N int }{{N: 10}, {N: 20}}
    for _, a := range as {
        a.N *= 10
    }
    println(as[0].N, as[1].N)
}

この表示結果はいくつになるでしょうか?

実行結果は以下になります。

$ go run main.go 
10 20
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ぼくのかんがえたさいきょうのシングルバイナリ生成ツール

皆さん、シングルバイナリ サイコーですかー!(挨拶)

この記事は Go4 Advent Calendar 2019 の3日目の記事です。
今日は「ぼくのかんがえたさいきょうのシングルバイナリ生成ツール」を紹介したいと思います。

ことの発端

毎回言ってるのでもうご存知かもしれませんが、自分はシングルバイナリ大好きな人です。そして、コマンドをパイプでつなげるのが大好きな人でもあります。

そんな私が、あいも変わらずアクセスログの分析をしていました。
全体の処理はこの↓シェルスクリプトのようなかんじです:

pipeline.bash
#!/usr/bin/env bash

xz -cd "$@"                             \
  | jsonize                             \
  | jq -c 'select(.method == "GET")
         | select(.user_agent != "-")'  \
  | paw-ua                              \
  | insert-db

圧縮されたログを受け取って、解凍して、JSON形式にして、不要な行をjqでフィルタして、UserAgentをごにょごにょ1して、DBにインサートするというだけです2

上の jsonizepaw-uainsert-db は、もちろんGoで実装しています。
で、実装したコマンドたちを、本番環境にコピーして使うわけです。こんなふうに:

$ scp pipeline.bash jsonize paw-ua insert-db prod-server:~/bin
$ ssh prod-server
prod-server> pipeline.bash /var/log/archive/*.xz

コピーするだけで動くシングルバイナリまじサイコーですね!

さてここで、ログデータに位置情報を付加してほしいという要望がありました。
というわけで、GeoIp2を使ってサクッと ip2geo コマンドを実装しました。
これをpipelineに追加しまして:

pipeline.bash
#!/usr/bin/env bash

xz -cd "$@"                             \
  | jsonize                             \
  | jq -c 'select(.method == "GET")
         | select(.user_agent != "-")'  \
  | paw-ua                              \
  | ip2geo -db ./GeoLite2-City.mmdb     \
  | insert-db

また本番環境にコピーして実行しましたらば・・・

$ scp pipeline.bash jsonize paw-ua insert-db prod-server:~
$ ssh prod-server
prod-server> pipeline.bash /var/log/archive/*.xz
pipeline.bash: ip2geo: command not found

あっ、ハイ、すみません・・・新しいコマンドをコピーするの忘れてました。。。

やっぱり、コマンド履歴にばかり頼ってちゃいけませんね・・・気を取り直しまして、

$ scp pipeline.bash jsonize paw-ua ip2geo insert-db prod-server:~/bin

今度はちゃんとコピーしましてから、実行してみますに~

$ ssh prod-server
prod-server> pipeline.bash /var/log/archive/*.xz
ip2geo: open GeoLite2-City.mmdb: no such file or directory

あゔぁゔぁゔぁゔぁゔぁゔぁ・・・
デスヨネー!GetLite2データベース3をコピるの忘れてましたよねー!!すいませんでしたよねー!!!

事ここに至るに当たり、気がついてしまったのです。おかしい、こんなはずじゃない、と。

僕らが夢見ていたシングルバイナリの世界はこんなはずじゃなかった!僕らが夢見ていたのは、バイナリ1つをコピーするだけで動くお手軽便利な世界ではなかったのか!?そうだ、今こそ取り戻そう!僕らの理想の世界を!!

というわけでWhole-In-One、略してWIO(わいおー)というのを作りました。

Whole-In-One なバイナリを作ってみよう

では、WIOの使い方の紹介です。まずはインストールから↓

Installation

ちょっと面倒なのですが、 go get ではなく、次のようにビルドスクリプトを使ってビルドしてください:

$ git clone github.com/Maki-Daisuke/go-whole-in-one
$ cd go-whole-in-one/cmd/wio
$ ./make.sh install

なお、Windows用にPowerShell版もあります4

> git clone github.com/Maki-Daisuke/go-whole-in-one
> cd go-whole-in-one\cmd\wio
> .\make.ps1 install

これで wio コマンドが使えるようになりました。

Init

では、さっそく上の例をシングルバイナリにしていきましょう。
今回作成するシングルバイナリなコマンド名は mylog とします。安直ですねー

なにはともあれ、プロジェクト用のディレクトリを作りましょう:

$ mkdir mylog
$ cd mylog

そして、このディレクトリで wio init を実行します:

$ wio init
$ ls
main.go  pack.go  packing-list

すると、このように3つのファイルが出来上がりました。とりあえず、ファイルはこのままにしておきます。

Generate

次に、上の例にあった pipeline.bashjsonizepaw-uaip2geoinsert-db を、それぞれ次のように頭に mylog-がつくようにリネームして、PATHの通っているところに置いておきます:

mv pipeline.bash mylog-pipeline
mv jsonize       mylog-jsonize
mv paw-ua        mylog-paw-ua
mv ip2geo        mylog-ip2geo
mv insert-db     mylog-insert-db

ファイル名の先頭に mylog- とつけておくことで、これから作るバイナリに中に含まれるようになります5

リネームしたのに合わせて、pipelineの中身もファイル名を書き換えました:

mylog-pipeline
#!/usr/bin/env bash

xz -cd "$@"                                       \
  | mylog-jsonize                                 \
  | jq -c 'select(.method == "GET")
         | select(.user_agent != "-")'            \
  | mylog-paw-ua                                  \
  | mylog-ip2geo -db $WIOPATH/GeoLite2-City.mmdb  \
  | mylog-insert-db

おっと、GeoLite2データベースもバイナリに含めるのをわすれないようにしましょう。packing-listファイルを開いて、末尾にファイル名を追記しておきます:

packing-list
# This file was generated by wio-init.
mylog-*
./GeoLite2-City.mmdb  # ←この行を追加

ここまでできたら、ビルドしましょう:

$ wio build

これで、mylog コマンドができました。

Deploy

できたコマンドはこんなふうに使えます:

$ scp mylog prod-server:~/bin
$ ssh prod-server
prod-server> mylog pipeline /var/log/archive/*.xz

mylogpipeline の間に-(ハイフン)がない点にご注意ください。mylog-pipelinemylog のサブコマンドのようにして呼び出されていますね。これは、mylog-jsonize など他のコマンドについても同様です。

また、GeoLite2データベースも含まれているので、なんらかの外部ファイルに依存しているようなコマンドもシングルバイナリの中に押し込むことができます。なので、バイナリ1個コピーするだけでお手軽便利にコマンドが実行できる世界がやってきました!

やっぱり、シングルバイナリ サイコーですね!

しかしさらなる問題が…

もしかしたら、上のスクリプトを見た人の中には気づいてしまった人もいるかもしれませんね。そう、mylog-pipelineスクリプトは jq がインストールされている環境じゃないと動かないということを!!6
これでは真のシングルバイナリとは言えないのではないか!!!?!!!

しかし案ずるでない、迷える子羊よ。そなたは如何なるファイルでもバイナリに埋め込むことができる!ほれ、このように packing-list に追加するのじゃ:

packing-list
# This file was generated by wio-init.
mylog-*
./GeoLite2-City.mmdb
jq  # ←この行を追加

そしておもむろにビルドしてコピーすれば、あら不思議・・・

$ wio build
$ scp mylog prod-server:~/bin
$ ssh prod-server
prod-server> mylog pipeline /var/log/archive/*.xz
jq: error while loading shared libraries: libonig.so.2: cannot open shared object file: No such file or directory

エラーで怒られるのである・・・orz

デスヨネー。jq さんには 鬼車 が必要ですもんねー!

気を取り直して

はいはい、鬼車がいるんなら、libonig.soを埋め込んであげればいいんでしょ7

packing-list
# This file was generated by wio-init.
mylog-*
./GeoLite2-City.mmdb
jq
/usr/lib/x86_64-linux-gnu/libonig.so.2  # ←さらに追加

というわけで、再チャレンジ:

$ wio build
$ scp mylog prod-server:~/bin
$ ssh prod-server
prod-server> mylog pipeline /var/log/archive/*.xz

今度は動きました!8

仕組み

ちょっとだけWIOの仕組みについて書いておきましょう。

と言っても、とても単純で、packing-list で指定されたファイルを tar形式にかためて、バイナリに埋め込んでいるだけです。
mylogコマンドが実行されたときに、tarファイルをtempディレクトリに展開して、そこにPATHを通すなどの下準備をしてからサブコマンドに execしているというわけです。ちなみに、tarの展開はコマンドの初回起動時のみで、2回目以降はtempディレクトリのキャッシュが使われます9

wio init は、デフォルトで コマンド名-サブコマンド名 という名前のコマンドをPATHから探索して実行する main関数を出力しますので、上記のように mylog-サブコマンド という命名規則にしておくと、埋め込んだコマンドがサブコマンドのように呼び出せるようになるのです10

で、実際どうやってバイナリに埋め込んでんの?

それは、ソースコードのこのへんを見てもらえればわかると思うのですが、pack.go のひな形の中に %q というフォーマット文字列が見えるかと思います:

func WritePackGo(data []byte, codec string) error {
    ...(中略)...
    _, err = fmt.Fprintf(out, `// DO NOT EDIT! This file was generated by wio-init.
package main
import(
    ...(中略)...
)
var(
    data = %q
    hash = "%X"
)
    ...(中略)...
`, data, md5.Sum(data), codec)
    return err
}

で、ここにtarで固めたデータをドカドカっと流し込んで、Goコンパイラに文字列としてコンパイルしてもらっているだけです。
そうなんですねー。こんな乱暴な方法でも、ちゃんとGoコンパイラはコンパイルしてくれるんですねー。すごいですねー!(小並感)11

Let's enjoy シングルバイナリ

というわけで、皆さんもお手軽便利なシングルバイナリ・ライフを楽しんでください。

詳しい使い方は WIOのドキュメントを参照してくださいね。

それでは、シングルバイナリまじサイコー!(別れの挨拶)


  1. ギョーム内容につき、お察しください。 

  2. ちなみに自分は、とりあえずログをJSONに変換してしまって、LDJSONをパイプラインでひとつひとつ変換処理かけていくのが好みです。こうするとパイプラインの各コマンドが並列で走るので、自然とマルチコアの恩恵を受けられますしね。 

  3. MaxMaind社の提供するIPアドレスと地点情報の紐付けデータベースです(https://dev.maxmind.com/geoip/geoip2/geolite2/ )。 GeoIp2 で使います。 

  4. ちなみに今のところ、Linux(Ubuntu 16.04)、macOS (Mojave)、Windows 10 (1909) で動作確認してあります。たぶん、BSDとかの他のUNIX系OSでも動くとは思いますが、確認環境がないためビルドタグはつけていません。どなたか興味がある人は、誰か試してみてほしいです。 

  5. あとで紹介しますが、ファイル名を変えなくてもバイナリに含む方法もあります。 

  6. まぁ、それを言ったら xz もなんですが、イマドキは xz は標準で入っていることが多いので、横においておきます。 

  7. ちなみに、作業環境は開発環境・本番環境ともにUbuntuです。(ただし、開発環境はUbuntu on WSL) 

  8. 今回はたまたまjqの依存ライブラリが少なくてうまくいきましたが、現実にはこんなふうにうまくいくケースは少ないと思います。依存ライブラリが大量にある場合もありますし、そのライブラリがさらに他のライブラリに依存していたりするので、ただしく必要なライブラリのリストを抽出するのはかなり大変です。従って、シングルバイナリで動くGo製のコマンドか、シェルスクリプトを埋め込むのが現実的な範囲です。複雑な依存関係をデプロイしたいなら、素直にDockerみたいなコンテナで配布するのがいいと思います。だだし、Dockerを使うためにはroot権限が必要なんですよね。root権限なしに、ホームディレクトリにコピるだけで動くのが、シングルバイナリの利点ですよね! 

  9. お気づきの方もいるかもしれませんが、これは古式ゆかしい(?)PAR::Packaer と同じ仕組みです。 

  10. ご想像のとおり、サブコマンドをPATHから探索して実行する様式はGitサブコマンドをマネたものです。このあたりのmain関数の挙動は main.go を書き換えれば好きなように変更できます。詳しくは WIOのドキュメントを参照してください。 

  11. ちなみにこの手法は、今一番主流のバイナリ埋め込みツールと目されるstatikを参考にしています。なので、こんな乱暴な方法でも問題はないと思います。 

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

FIDO2でパスワードレス認証のサーバサイド実装(Golang)

はじめに

[CA Tech Dojo/Challenge/JOB Advent Calendar 2019]の3日目はハゲが書かせていただきます。私は今年の夏に「CA Tech Dojo (Go)」に参加させていただきました。自走力が鍛えられる内容で、その後の学習の生産性が向上するのでおススメです。その時の「参加レポート」があるので、興味のある人はぜひ見てください。

本題

今日はFIDOと呼ばれる新しい認証の仕組みについて紹介します。時間が無くて実装まで手が付けれなかったので、後日更新します。

IDとパスワードを用いた認証の問題点

  • リスト攻撃に弱いこと
    あらかじめ入手してリスト化したID・パスワードを利用して、アクセスされてしまう。

  • フィッシング攻撃に弱いこと
    ユーザが正規のサイトによく似たサイトにIDとパスワードを入力してしまい、盗まれる。

  • 利用しているサービスの数だけパスワードを記憶しておかなければならないこと
    紙媒体に書いてしまったり、同じものを使いまわしてしまったり、忘れてしまったりする。→ユーザ体験の品質が下がる。

  • 入力する手間がめんどくさいこと
    ユーザ体験の品質が下がる。

FIDO(Fast IDentity Online)とは

セキュリティとユーザ体験を共存させる認証技術を提供する仕組みのことです。パスワードレスな認証にすることでパスワード認証の問題を解決しました。

FIDOの余談

現在ではFIDO2も誕生し、Web認証やデバイス間連携に対応したことでFIDO認証が広く使われるようになりました。今後はAndroidなどのネイティブアプリ上でFIDO認証ができるようにするそうです。今回扱うのはFIDO2です。

FIDOの登録・認証モデル

①利用者は事前に認証器を認証サーバに登録する。

②この際に、認証器は秘密鍵と公開鍵のペアを生成し、公開鍵とユーザIDを紐づけて認証サーバに送付する。

③認証の際には、認証サーバが一度だけ有効なランダムな文字列(challenge)を生成し、認証器に送付する。

④認証器が生体情報などで本人性を検証したら、このchallengeに対して秘密鍵で署名を生成し、署名付きchallengeを認証サーバに送る。

⑤認証サーバは公開鍵によって復号し、適切な署名であることを検証出来たら認証が成功する。

従来の認証モデルとFIDOの認証モデルの違い

従来の認証は、利用者が通信路を介して、ID、クレデンシャル情報(パスワードや生体情報など)を認証サーバに送付する仕組みだったので、利用者と認証サーバでクレデンシャル情報を共有していました。

しかし、FIDOは、認証器によってローカルで本人性を認証するプロセスと、認証サーバに検証結果を送付するというプロセスを分離しているので、ネットワーク上にクレデンシャル情報が流れることはないという強みがあります。

生体認証に関する余談

生体情報を用いた認証は、パスワードを用いる認証に比べて、パスワードを記憶する必要が無かったり、入力する手間が省けたりと最高のユーザ体験を提供していましたが、生体情報は変更が難しいため、一度流出すると二度と使えなくなることから、管理コストが高くなってしまうという問題点がありました。しかし、FIDOを用いることによって通信経路で生体情報が流出する可能性がなくなったので生体認証が普及しやすくなりました。とてもありがたいです。

FIDO2のサーバサイド実装(Golang)

環境
Ubuntu18.04LTS
Golang1.11.5

今回はFIDO2を使ってWeb上でFIDO認証をできるようにします。
まずはライブラリをインポートします。
go get github.com/duo-labs/webauthn
また、軽量なWebツールキットのgorillaを使います。
go get github.com/gorilla/mux
次にオープンソースのFIDO2サーバをインストールします。
インストール手順は「こちらを参考」にしてください。

実装は認証サーバに登録する処理と認証する処理の二つの手順を説明します。

①登録のフロー

FIDO2の登録フロー.png
アテステーションがよくわからないと思うので[こちらの記事]を参考にしてください。

②認証のフロー

FIDO2の認証フロー.png
登録と認証でフローにあまり差がないのでまとめて解説しますが、ここではFIDOの解説論文の引用をします。

( 1 ) 利用者は PC のブラウザで RP の登録ページへのリンクをクリック若しくは URL を入力する.
( 2 ) ブラウザは指定されたアドレスへ要求を行う.
( 3 ) RP サーバは FIDO 鍵登録を開始する旨を FIDO サーバへ伝える.
( 4 ) FIDO サーバはメタデータを利用してポリシーの一覧とチャレンジを作成し,RP へ応答する.
( 5 ) RP も UAF と同様にポリシーを追加し,ブラウザにJavaScript とチャレンジとともに送信する.
( 6 ) PC とスマートフォンは例えば Bluetooth のペアリングのような初期接続作業を事前にしておく,若しくはまだ接続されていない状態であればこのときに行う.RP が送信した JavaScript によって PC からスマートフォンへmakeCredential()(鍵生成要求)が呼び出される.(CTAP)
( 7 ) 対応する認証器に鍵生成要求が届けられ,利用者にジェスチャを求める.
( 8 ) 利用者は認証の内容を確認した上で,ジェスチャを入力する.
( 9 ) 認証器は利用者の認証が成功したら RP 固有のアテステーション用秘密鍵を使ってチャレンジに署名する.
( 10 ) 作成した署名とともに認証用公開鍵をブラウザ,RPサーバを通じて FIDO サーバへ送信する
( 11 ) FIDO サーバはアテステーション用公開鍵を使って署名を検証し,成功した場合,認証用公開鍵を利用者とひも付けて登録する.
( 12 ) 上記までの手順でエラーがなかった場合,登録完了ページをブラウザに表示する.

[5][解説論文 FIDO認証とその技術]

Golangで実装する部分はRPサーバの部分で、基本的にブラウザやFIDOサーバとの通信がメインです。時間なかったのでソースコードは後日乗っけます。

Step1. RPサーバはブラウザから利用者のIDを受け取り、FIDO鍵登録を開始する旨をFIDOサーバへ伝える。

Step2. FIDOサーバのAPIにアクセスし、FIDOサーバで作成されたポリシーとチャレンジを受け取る。

Step3. RPもポリシーを加え、ブラウザに送信する。

このあと、認証器で生体情報による認証がおこなわれ、チャレンジに署名を加えたものと認証用の公開鍵をRPに送る。

Step4. 署名付きのチャレンジと認証用公開鍵をブラウザから受け取り、FIDOサーバへ送信する。

Step5. FIDOサーバ上で検証に成功したら、公開鍵と利用者のIDを紐づけて登録する。

Step6. エラーが無ければ登録完了ページをブラウザへ返す。

認証のフローで登録のフローと違う部分は、認証器による認証後、つまりStep4の署名付きのチャレンジと認証用公開鍵をブラウザから受け取りFIDOサーバへ送信する部分です。登録時に公開鍵はすでにFIDOサーバに保管されているため、ブラウザからは公開鍵は送られず、署名付きのチャレンジを送付するだけで済みます。

参考文献

[1]GitHub duo-labs/webautun
[2]WebAuthnでパスワードレスなサイトを作る。安全なオンライン認証を導入するFIDOの基本
[3]FIDO認証によるパスワードレスログイン実装入門
[4]Mercari Engineering Blog
[5]FIDO 認証とその技術
[6]アテステーションとは

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

Twistのchatbotを拡張してみた

昨日作ったchatbotがあのままだと全く実用性がないので、もうちょっとサンプルっぽくしました

昨日作ったもの
https://qiita.com/usk81/items/bfd15ec3c5ecc23d0b8f

Twistに関しては昨日作ったものの記事を参考にしてください

成果物

コメントしたものをそのまま返すようにしました

スクリーンショット 2019-12-02 23.46.22.png

Page: https://github.com/usk81/twistbot
Release: https://github.com/usk81/twistbot/releases/tag/v0.0.1-alpha2

Request Body

Request Bodyはx-www-form-urlencodedで送られているようです。
どうせなので、Structureにおこしてみました。
というか、Developerページと差異があってほぼここの解析に時間使ってました。

StructureへのデコードはGorillaSchemaを使用しました。
https://github.com/gorilla/schema

今回は対応しませんでしたが、リクエストの中にurl_ttlurl_callback というものがあります。
url_ttlにはタイムスタンプ
url_callbackにはURLが格納されています。
リクエストの返却に時間がかかる場合は、一度202で空リクエストを返して、url_ttlで設定されている制限時間内にurl_callbackにリクエストを返せばいいそうです。

type TwistOutgoingRequest struct {
    EventType         string `schema:"event_type"`
    WorkspaceID       int    `schema:"workspace_id"`
    Content           string `schema:"content"`
    UserID            int    `schema:"user_id"`
    UserName          string `schema:"user_name"`
    URLCallback       string `schema:"url_callback"`
    URLTTL            int    `schema:"url_ttl"`
    MessageID         int    `schema:"message_id"`
    ThreadID          int    `schema:"thread_id"`
    ThreadTitle       string `schema:"thread_title"`
    ChannelID         int    `schema:"channel_id"`
    ChannelName       string `schema:"channel_name"`
    CommentID         int    `schema:"comment_id"`
    ConversationID    int    `schema:"conversation_id"`
    ConversationTitle string `schema:"conversation_title"`
    VerifyToken       string `schema:"verify_token"`
}

Handler

修正前

func botHandler(w http.ResponseWriter, r *http.Request) {
    if r == nil {
        w.WriteHeader(http.StatusInternalServerError)
        w.Write([]byte("can not get request"))
    }
    defer r.Body.Close()
    bs, _ := ioutil.ReadAll(r.Body)
    w.Write([]byte(fmt.Sprintf("header: %s, body: %s", r.Header.Get("Content-Type"), string(bs))))
}

修正後

デバッグも兼ねて、ログも仕込んでみました。
サーバのログをミドルウェア層をzapで作ったので、ソフトウェア層も揃えました。
https://github.com/uber-go/zap

message_idとかが返ってきてたので、APIでmessage取得しないといけないのかと思ってましたが、contentに平文で入ってました。

func botHandler(w http.ResponseWriter, r *http.Request) {
    lg, _ := zap.NewProduction()
    defer lg.Sync()

    if r == nil {
        lg.Error("can not get request")
        w.WriteHeader(http.StatusInternalServerError)
        w.Write([]byte("can not get request"))
    }
    if err := r.ParseForm(); err != nil {
        lg.Warn(fmt.Sprintf("failed to parse request %s", err.Error()))
        w.WriteHeader(http.StatusBadRequest)
        w.Write([]byte("request is invalid"))
    }
    var req TwistOutgoingRequest
    if err := decoder.Decode(&req, r.PostForm); err != nil {
        lg.Error(fmt.Sprintf("fail to decode parsed request %s : %#v", err.Error(), r.PostForm))
        w.WriteHeader(http.StatusInternalServerError)
        w.Write([]byte(err.Error()))
    }
    w.Write([]byte(req.Content))
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go言語でユニットテスト, テストしやすいコードにモックを書く

Go言語でテストしやすいコードを書く

Go言語でテスタブル(テストしやすい)コードを書いてユニットテストを実践しましょう.
この記事では以下について書きます.ユニットテストの概要については書きません.

  • Go言語でのユニットテストの書き方
  • テストアサーション
  • Go言語でのユニットテストの実行方法
  • テストしやすいコードの書き方
  • Go言語でのモックの作成とモックの使い方

Go言語でのユニットテストの書き方

テストコードのファイル(xxxx_test.go)

Go言語では本体コードと同じパッケージ内(同じディレクトリ内)にテストファイルを作成するのが一般的のようです. ファイル名はxxx_test.goにするのがルールです.

├── calc.go
└── calc_test.go

テストコードの基本

テストコードでは以下のパッケージを使います.

関数はTestXXXXという名称で引数に*testing.T型を取る必要があります.
関数名は日本語も含めることができます.テストコードではテスト項目をよりわかりやすくするために開発チームが最も理解できる言葉で書く場合があります(筆者個人としては,テストコードに限っては日本語関数名を勧めて言います.).

func TestGetUser_正常系(t *testing.T) {
}

func TestGetUser_データベース接続障害時の場合にエラーを返すことを検証(t *testing.T) {
}

例としてsumという関数のテストを書いてみます.

calc.go
func sum(a, b int) int {
    return a + b
}
calc_test.go
import (
    "testing"
    "github.com/stretchr/testify/assert"
)

func TestSum(t *testing.T) {
    actual := sum(3, 5)
    expected := 8

    assert.Equal(t, expected, actual)
}`

ユニットテストの実行

goコマンドでユニットテストを実行できます.

$ go test calc_test.go

もしくは次のようにすることでカレントディレクトリ配下のすべてのテストを実行できます.

$ go test ./....

また,-vオプションを付けることでテストの実行状態の詳細を出力できます.

go test -v  calc_test.go
go test -v ./...

ここまでGo言語におけるユニットテストの基本でした.

テストしやすいコードの書き方

テストしやすいコードとは?

ユニットテストを積極的に導入するには予め本体コードをテストしやすいコードにしておく必要があります.そのためには以下の心がけが必要です.この心がけは言語を問わず言えることです.Go言語に限ったことではありません.

  • 関数に外部依存の要素を作らない.
    • 外部依存の要素は引数で注入できるようにする.
    • 外部依存を含む関数はまとめる.
  • 一つの関数でたくさんの処理をしない.
    • これは保守性の高いコードを書く上で基本.
  • モック化できるようにコードを書く

モック化できるコード

本記事では,Go言語において「モック化できるように書く」ことを記述します.
ユニットテスト時においてモック化とはテストに依存する外部依存の関数の動作をテストのためにふるまいを置き換えることを言います.

例えば,テスト対象の関数内でデータベースに接続してユーザ情報を取得する処理を行っている場合,モック化することでユニットテストでは実際にはデータベースに接続しませんが,接続した体で値を返してテストを進めると言ったことです.

Go言語においてモック化できるコードにするためには以下のようにします.

  • 構造体に関数を実装する
  • その構造体のインターフェイスを外部公開する

構造体の置き換えはできませんが,インターフェイスであれば同じ入出力の関数を持てば置き換えができます.

具体的には以下のようにします.

user_db.go
type UserDB interface {
    GetUser(userID string) (*model.User, error)
}

func NewUserDB() UserDB {
    return &userDB{}
}

type userDB struct {
}

func (db *userDB) GetUser(userID string) (*model.User, error) {
}

この例は,ユーザデータベースからユーザ情報を取得する関数を書いています.
userDBは実際に処理を行う関数を持つ構造体ですが,小文字から始まっているように外部公開していません.一方で,この構造体のインターフェイスであるUserDBは外部公開しています.また,userDB生成する関数NewUserDBは外部公開しています.そしてこの関数の戻り値は構造体userDBではなくインターフェイスUserDBを指しています,

従って,呼び出し側は以下のようになります.

db := NewUserDB()
user, err := db.GetUser("12")

このときUserDBを外部注入できるようにしておけばテストしやすいコードを書くことができます.
具体的には以下のようにします. この例ではインターフェイスUserDBに依存するUserServiceを記述しています.ポイントは構造体ではなくインターフェイスにしている点です.これのよりUserDBを置き換えるとができます.

struct userService{
    db: UserDB
}

func (s *userService) GetUser(userID string){
   user, err := s.db.GetUser(userID)
   if err != nil {
       //....エラー時の処理       
   }

   //...正常系処理
}

func main() {
    service := userService{ db: NewUserDB() }
    service.GetUser("hoge")
}

モックの作成

Go言語ではインターフェイスからモックを自動生成することができます.モックを作って,本来の処理を置換してテストを書きましょう.

モックの生成コマンド

モックの生成にはmockgenコマンドを使用します.
以下のようにすることで,user_db.goのモックがmocks/配下に作成されます.最後のpackageオプションで生成するモックが属するパッケージを指定しています.

go get github.com/golang/mock/mockgen
mockgen -soruce user_db.go -destination mocks/user_db.go -package mocks

モックを使ったテストコード

生成したモックは元とインターフェイスを持つので置き換えることができます.そしてモック特有の関数で振る舞いを変えることができます.

func TestGetUser_正常系(t *testing.T) {
    ctrl := gomock.NewController(t)
    defer ctrl.Finish(()

    dbMock := mocks.NewMockUserDB(ctrl)
    dbMock.EXPECT().GetUser("user1").Return(&model.User{}, nil)

    service := userService{ db: dbMock }
    userService.GetUser("user1")
}

以下は,生成したモックを使ったユニットテストの例です.
ctrlの生成についてはいわゆるおまじないと言っていいでしょう.

mocks.NewMockUserDBにてモックを生成しています.次行のEXPECT()...の部分で,期待される呼び出し方とふるまいについて定義しています.ここで期待したとおりに関数が呼び出されなければテストエラーとなります.

モックの使い方

モックの詳細な使い方については公式リファレンスを参照してください.
ここでは,よく使うであろう使い方について記述します.

引数チェック

EXPECT().関数名(期待したい値)を記述します・「期待したい値」で呼び出しがなければテストエラーとなります.

dbMock.EXPECT().GetUser("user-id1")

何でもいい引数

引数ななんでも良い場合は,gomock.Any()を指定します.

import "github.com/golang/mock/gomock"

dbMock.EXPECT().GetUser(gomock.Any())

戻り値の振る舞い

戻り値を指定したい場合はReturnで指定します.

expectedUser := model.User{}

dbMock.EXPECT().GetUser("user-id1").Return(expectedUser, nil)

代入の振る舞い

戻り値ではなく,ポインタ引数に代入したい場合はSetArgを指定します.

product_db.go
func (db *productDB) GetProduct(data *model.Product) {
    data = &model.Product{}
}

この場合,以下のようにすることで引数に代入した振る舞いができます.

expectedProduct := &model.Product{}
dbMock.EXPECT().GetProduct.SetArg(0, expectedProduct)

まとめ

Go言語におけるユニットテストとユニットテストを書きやすくするための実装方法,モック化について書きました.

Tips

モックの生成時にエラーになったら..

モック生成後,元の構造体・インターフェイスを変更後に再度生成する際にエラーになる場合があります.
大抵は既存のコードと新しいコードで関係性が維持できない場合.

そんなときは,問題発生原因のコードをビルド対象から外すとモックが生成できるようになるかもしれません.以下の記述をソースコードの1行目に加えると外れます.

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

Go言語でユニットテスト, テストしやすいコードとモックを書く

Go言語でテストしやすいコードを書く

Go言語でテスタブル(テストしやすい)コードを書いてユニットテストを実践しましょう.
この記事では以下について書きます.ユニットテストの概要については書きません.

  • Go言語でのユニットテストの書き方
  • テストアサーション
  • Go言語でのユニットテストの実行方法
  • テストしやすいコードの書き方
  • Go言語でのモックの作成とモックの使い方

Go言語でのユニットテストの書き方

テストコードのファイル(xxxx_test.go)

Go言語では本体コードと同じパッケージ内(同じディレクトリ内)にテストファイルを作成するのが一般的のようです. ファイル名はxxx_test.goにするのがルールです.

├── calc.go
└── calc_test.go

テストコードの基本

テストコードでは以下のパッケージを使います.

関数はTestXXXXという名称で引数に*testing.T型を取る必要があります.
関数名は日本語も含めることができます.テストコードではテスト項目をよりわかりやすくするために開発チームが最も理解できる言葉で書く場合があります(筆者個人としては,テストコードに限っては日本語関数名を勧めて言います.).

func TestGetUser_正常系(t *testing.T) {
}

func TestGetUser_データベース接続障害時の場合にエラーを返すことを検証(t *testing.T) {
}

例としてsumという関数のテストを書いてみます.

calc.go
func sum(a, b int) int {
    return a + b
}
calc_test.go
import (
    "testing"
    "github.com/stretchr/testify/assert"
)

func TestSum(t *testing.T) {
    actual := sum(3, 5)
    expected := 8

    assert.Equal(t, expected, actual)
}`

アサーション

if文で値をチェックしてt.Fail()することでもテストできますが、"github.com/stretchr/testify/assert"の関数を使うことで読みやすいコードを書くことができます。
詳細についてはassertのgodocを参照してください。

以下よく使うであろうアサーションを紹介します。

// 値が等しいかどうかを確認
assert.Equal(t, expected, actual)

// 値がNilであるか確認
assert.Nil(t, actual)

// 値がNilでないか確認
assert.NotNil(t, actual)

// エラー発生であることを確認
assert.Error(t, err)

// エラー発生でないことを確認
assert.NoError(t, err)

ユニットテストの実行

goコマンドでユニットテストを実行できます.

$ go test calc_test.go

もしくは次のようにすることでカレントディレクトリ配下のすべてのテストを実行できます.

$ go test ./....

また,-vオプションを付けることでテストの実行状態の詳細を出力できます.

go test -v  calc_test.go
go test -v ./...

ここまでGo言語におけるユニットテストの基本でした.

テストしやすいコードの書き方

テストしやすいコードとは?

ユニットテストを積極的に導入するには予め本体コードをテストしやすいコードにしておく必要があります.そのためには以下の心がけが必要です.この心がけは言語を問わず言えることです.Go言語に限ったことではありません.

  • 関数に外部依存の要素を作らない.
    • 外部依存の要素は引数で注入できるようにする.
    • 外部依存を含む関数はまとめる.
  • 一つの関数でたくさんの処理をしない.
    • これは保守性の高いコードを書く上で基本.
  • モック化できるようにコードを書く

モック化できるコード

本記事では,Go言語において「モック化できるように書く」ことを記述します.
ユニットテスト時においてモック化とはテストに依存する外部依存の関数の動作をテストのためにふるまいを置き換えることを言います.

例えば,テスト対象の関数内でデータベースに接続してユーザ情報を取得する処理を行っている場合,モック化することでユニットテストでは実際にはデータベースに接続しませんが,接続した体で値を返してテストを進めると言ったことです.

Go言語においてモック化できるコードにするためには以下のようにします.

  • 構造体に関数を実装する
  • その構造体のインターフェイスを外部公開する

構造体の置き換えはできませんが,インターフェイスであれば同じ入出力の関数を持てば置き換えができます.

具体的には以下のようにします.

user_db.go
type UserDB interface {
    GetUser(userID string) (*model.User, error)
}

func NewUserDB() UserDB {
    return &userDB{}
}

type userDB struct {
}

func (db *userDB) GetUser(userID string) (*model.User, error) {
}

この例は,ユーザデータベースからユーザ情報を取得する関数を書いています.
userDBは実際に処理を行う関数を持つ構造体ですが,小文字から始まっているように外部公開していません.一方で,この構造体のインターフェイスであるUserDBは外部公開しています.また,userDB生成する関数NewUserDBは外部公開しています.そしてこの関数の戻り値は構造体userDBではなくインターフェイスUserDBを指しています,

従って,呼び出し側は以下のようになります.

db := NewUserDB()
user, err := db.GetUser("12")

このときUserDBを外部注入できるようにしておけばテストしやすいコードを書くことができます.
具体的には以下のようにします. この例ではインターフェイスUserDBに依存するUserServiceを記述しています.ポイントは構造体ではなくインターフェイスにしている点です.これのよりUserDBを置き換えるとができます.

struct userService{
    db: UserDB
}

func (s *userService) GetUser(userID string){
   user, err := s.db.GetUser(userID)
   if err != nil {
       //....エラー時の処理       
   }

   //...正常系処理
}

func main() {
    service := userService{ db: NewUserDB() }
    service.GetUser("hoge")
}

モックの作成

Go言語ではインターフェイスからモックを自動生成することができます.モックを作って,本来の処理を置換してテストを書きましょう.

モックの生成コマンド

モックの生成にはmockgenコマンドを使用します.
例えば,以下のようにすることで,user_db.goのモックがmocks/配下に作成されます.最後のpackageオプションで生成するモックが属するパッケージを指定しています.

go get github.com/golang/mock/mockgen
mockgen -soruce user_db.go -destination mocks/user_db.go -package mocks

モックを使ったテストコード

生成したモックは元とインターフェイスを持つので置き換えることができます.そしてモック特有の関数で振る舞いを変えることができます.

func TestGetUser_正常系(t *testing.T) {
    ctrl := gomock.NewController(t)
    defer ctrl.Finish(()

    dbMock := mocks.NewMockUserDB(ctrl)
    dbMock.EXPECT().GetUser("user1").Return(&model.User{}, nil)

    service := userService{ db: dbMock }
    userService.GetUser("user1")
}

これは,生成したモックを使ったユニットテストの例です.
ctrlの生成についてはいわゆるおまじないと言っていいでしょう.

mocks.NewMockUserDBにてモックを生成しています.次行のEXPECT()...の部分で,期待される呼び出し方とふるまいについて定義しています.ここで期待したとおりに関数が呼び出されなければテストエラーとなります.

モックの使い方

モックの詳細な使い方については公式リファレンスを参照してください.
ここでは,よく使うであろう使い方について記述します.

引数チェック

EXPECT().関数名(期待したい値)を記述します・「期待したい値」で呼び出しがなければテストエラーとなります.

dbMock.EXPECT().GetUser("user-id1")

何でもいい引数

引数ななんでも良い場合は,gomock.Any()を指定します.

import "github.com/golang/mock/gomock"

dbMock.EXPECT().GetUser(gomock.Any())

戻り値の振る舞い

戻り値を指定したい場合はReturnで指定します.

expectedUser := model.User{}

dbMock.EXPECT().GetUser("user-id1").Return(expectedUser, nil)

代入の振る舞い

戻り値ではなく,ポインタ引数に代入したい場合はSetArgを指定します.

product_db.go
func (db *productDB) GetProduct(data *model.Product) {
    data = &model.Product{}
}

この場合,以下のようにすることで引数に代入した振る舞いができます.

expectedProduct := &model.Product{}
dbMock.EXPECT().GetProduct.SetArg(0, expectedProduct)

まとめ

Go言語におけるユニットテストとユニットテストを書きやすくするための実装方法,モック化について書きました.

Tips

モックの生成時にエラーになったら..

モック生成後,元の構造体・インターフェイスを変更後に再度生成する際にエラーになる場合があります.
大抵は既存のコードと新しいコードで関係性が維持できない場合.

そんなときは,問題発生原因のコードをビルド対象から外すとモックが生成できるようになるかもしれません.以下の記述をソースコードの1行目に加えると外れます.

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

TwistのchatbotをGoで作ってみた

Advent Calendarの間、毎日なんか作ってます。
https://qiita.com/advent-calendar/2019/project25

今回はTwistのchatbotを作ってみました。
Slackbotは作ったことあるし、いっぱい転がってるので

What is Twist?

Todoistで有名なdoistが作った、SlackやMS Teamsなどのようなチャットツールです。
フルリモートな会社が自分たちのために作ったサービスを商用化しただけあって、メッセージが流れて負えないとか一般的にチャットツールで聞かれる不満は一通り解決されています。
Integrationはまだ、Slackに分がありますが、ただのチャットツールとして使っているのであれば乗り換えたほうがハッピーになれそうです。

(ファンですが、中の人ではありませんよ)

https://twist.com/
title.png

Bot

Webhookの仕様

Slackなどであればポーリングしないといけませんが、TwistはREST Hookを採用しているので、subscribeに登録すれば、イベントが発生したときに登録したURLに勝手にリクエストが送られるのでポーリングは不要で、普通のREST APIの応用で作成できます。
https://resthooks.org/

Botの場合、General Integrationを設定すればsubscribeの登録も不要なのでかなり楽です。

設定方法

  1. 自分のTeamにログインし、 Add integrations をクリックします add-integrations.png
  2. Manageをクリックして新規追加をします integrations-dashboard.png
  3. 名前や説明を書きます。Integration typeGeneral Integrationを設定してください。

General Integrationを設定するとOutgoing Hookを別途設定しなくても全てのイベントに反応してくれます。
new-integration.png
4. Botをクリックし、Outgoing webhook URLを設定します。

URLはベーシック認証付きのものだとエラーで設定できないようなので、セキュリティを考慮したいときはパラメータでトークンを渡すかIPなどで制限するしかなさそうです。
bot-settings.png

Goの設定

Handlerの設定は下記のように作りました。
リクエストボディの内容がわからなかったので、とりあえずリクエストボディの内容をオウム返ししてもらうようにしました

func botHandler(w http.ResponseWriter, r *http.Request) {
    if r == nil {
        w.WriteHeader(http.StatusInternalServerError)
        w.Write([]byte("can not get request"))
    }
    defer r.Body.Close()
    bs, _ := ioutil.ReadAll(r.Body)
    w.Write([]byte(fmt.Sprintf("header: %s, body: %s", r.Header.Get("Content-Type"), string(bs))))
}

上記のように設定して、botにコメントを送ると下記のようになります

スクリーンショット.png

作ったものはここにあります。
作ってみたい方は参考にどうぞ。

page: https://github.com/usk81/twistbot
release: https://github.com/usk81/twistbot/releases/tag/v0.0.1-alpha1

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

Goで配列をappendすると予想と違う動作をするらしい

はじめに

こんにちはRIN1208です。今回はITRCアドベントカレンダーの二日目の記事です。

今回書くのは本日12月2日に六本木ヒルズで行われたStep up Go で初めて知って今後もハマりそうなことを勉強しましたので忘れないように書いていきたいと思います。

実行環境はplay.golangを使用します(なぜかmacだと問題を再現できませんでした)

今回の問題

package main

func main() {
    a := []int{1, 2, 3}
    b := append(a, 4, 5)
    a[0] = 10
    c := append(b, 6, 7)
    b[1] = 20
    println(c[0] + c[1])
}

こちらのコードです。

実行すると....

21

は??????

なんで???3じゃないの?と思いました。普通ポインタとスライスの中身を見てみると

package main

import "fmt"

func main() {
    a := []int{1, 2, 3}
    b := append(a, 4, 5)
    a[0] = 10
    c := append(b, 6, 7)
    b[1] = 20

    println("%p", a)
    println("%p", b)
    println("%p", c)

    fmt.Println(a, b, c)

    println(c[0] + c[1])
}

出力結果

%p [3/3]0x40e020
%p [5/8]0x456000
%p [7/8]0x456000
[10 2 3] [1 20 3 4 5] [1 20 3 4 5 6 7]
21

bとcが同じポインタ??????

goのsliceはこちらのような構造になっているらしいです。

また同じポインタを参照しているので反映されてしまっているみたいです。

ちなみにcapを更新?するとポインタも変わるみたいです

package main

import "fmt"

func main() {
    a := []int{1, 2, 3}
    b := append(a, 4, 5)
    a[0] = 10
    c := append(b, 6, 7)
    b[1] = 20
    d := append(c, 6, 7)
    c[1] = 30

    println("%p", a)
    println("%p", b)
    println("%p", c)
    println("%p", d)

    fmt.Println(cap(a), cap(b), cap(c), cap(d))
    fmt.Println(a, b, c, d)

    println(c[0] + c[1])
}
----------------------------

%p [3/3]0x40e020
%p [5/8]0x456000
%p [7/8]0x456000
%p [9/16]0x430080
3 8 8 16
[10 2 3] [1 30 3 4 5] [1 30 3 4 5 6 7] [1 20 3 4 5 6 7 6 7]
31

結論

goの配列は途中保存するときは上記の書き方ではなくcopy関数を使用し別の変数に格納した方がいいみたいです

終わりに

今回、自分の未熟さゆえに曖昧な部分が多いのに加え適切な言い方ではない箇所があるためとてもいい記事ではないと思います。ですが今回初めてこんな挙動をすると知ったので知らない人に知っていただければと思い書きました。また本記事について詳しく知っている方はコメントにて教えて頂けるととても助かります。

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

Goでsliceをappendすると予想と違う動作をするらしい

はじめに

こんにちはRIN1208です。今回はITRCアドベントカレンダーの二日目の記事です。

今回書くのは本日12月2日に六本木ヒルズで行われたStep up Go で初めて知って今後もハマりそうなことを勉強しましたので忘れないように書いていきたいと思います。

実行環境はplay.golangを使用します(なぜかmacだと問題を再現できませんでした)

今回の問題

package main

func main() {
    a := []int{1, 2, 3}
    b := append(a, 4, 5)
    a[0] = 10
    c := append(b, 6, 7)
    b[1] = 20
    println(c[0] + c[1])
}

こちらのコードです。

実行すると....

21

は??????

なんで???3じゃないの?と思いました。普通ポインタとスライスの中身を見てみると

package main

import "fmt"

func main() {
    a := []int{1, 2, 3}
    b := append(a, 4, 5)
    a[0] = 10
    c := append(b, 6, 7)
    b[1] = 20

    println("%p", a)
    println("%p", b)
    println("%p", c)

    fmt.Println(a, b, c)

    println(c[0] + c[1])
}

出力結果

%p [3/3]0x40e020
%p [5/8]0x456000
%p [7/8]0x456000
[10 2 3] [1 20 3 4 5] [1 20 3 4 5 6 7]
21

bとcが同じポインタ??????

goのsliceはこちらのような構造になっているらしいです。

また同じポインタを参照しているので反映されてしまっているみたいです。

ちなみにcapを更新?するとポインタも変わるみたいです

package main

import "fmt"

func main() {
    a := []int{1, 2, 3}
    b := append(a, 4, 5)
    a[0] = 10
    c := append(b, 6, 7)
    b[1] = 20
    d := append(c, 6, 7)
    c[1] = 30

    println("%p", a)
    println("%p", b)
    println("%p", c)
    println("%p", d)

    fmt.Println(cap(a), cap(b), cap(c), cap(d))
    fmt.Println(a, b, c, d)

    println(c[0] + c[1])
}
----------------------------

%p [3/3]0x40e020
%p [5/8]0x456000
%p [7/8]0x456000
%p [9/16]0x430080
3 8 8 16
[10 2 3] [1 30 3 4 5] [1 30 3 4 5 6 7] [1 20 3 4 5 6 7 6 7]
31

結論

goの配列は途中保存するときは上記の書き方ではなくcopy関数を使用し別の変数に格納した方がいいみたいです

終わりに

今回、自分の未熟さゆえに曖昧な部分が多いのに加え適切な言い方ではない箇所があるためとてもいい記事ではないと思います。ですが今回初めてこんな挙動をすると知ったので知らない人に知っていただければと思い書きました。また本記事について詳しく知っている方はコメントにて教えて頂けるととても助かります。

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

Go 1.13時代のエラー実装者のお作法

Goアドベントカレンダーその2の3日目のエントリーです。

Goではエラー処理の方法としてはプリミティブな方法しか提供しておらず、他の言語のユーザーからやいのやいの言われてきました。Go2でそれを改善するぞからプロポーザル募集でいろいろ意見を募っては二転三転みたいな感じで、Go 1.13ではだいぶおとなしい感じに機能拡張されました。基本的な方向性としてはgithub.com/pkg/errorsから少し機能を取り込んだ感じです。

すでに、数多くのエントリーやらプレゼンテーションやらでGo 1.13の利用者視点でのerrorsの変更点については触れられてきましたので詳しくはそちらをご覧ください。サマリーとしては下請けのパッケージで出てきた詳細なエラーをラップして扱うための便利な機構がいろいろ追加された感じです。

これらでは主にアプリケーションコードの実装者というかライブラリの利用者向けの説明が多く、Go 1.13以降のerrorsを活用するためにエラーを定義する一番底レイヤーな開発者がどのようにエラーを定義すべきか、という点について触れた資料は見かけたことがなかったので(単なる僕の見落としかもしれませんが)、そちら視点でHow Toをまとめます。

次の2つに分けて説明します

  • ラップとアンラップ
  • エラーの比較

ラップとアンラップ

Go 1.13ではエラーを構造化(ラップ)することができるようになりました。2つの機能が提供されました。

  • fmt.Errorf()で、"%w"という識別子を使って、エラーをラップできるようになった
  • errors.Unwrap(err)

ラップできるということは、例えばデータベースの細かなアクセスエラーに対して、データの読み書きのレイヤーでは「データベースでエラーがあったよ」という抽象度の高いエラーとして扱い、なおかつ、HTTPのハンドラーのレイヤーでは「internal server errorで500ですよ」というさらに抽象度の高いエラーとして扱うことが可能になるということです。そして、ただ抽象度をあげるだけではなくて、必要に応じてUnwrap()で詳細なエラーを取り出すことができるというスンポーです。

なお、 errors.Unwrap(err)は次のような実装になっています。あんまり見慣れない感じな人も多いですが、型アサーションでUnwrap()メソッドを持っているかどうかを調べ、持っていたら呼び出すというコードになっています。事前にinterfaceを定義しておくコードであれば、まだおとなしい感じですが、この書き方だと静的型付け言語のくせに、まるでRubyやPythonのようなダックタイピングになっていますね。

func Unwrap(err error) error {
    u, ok := err.(interface {
        Unwrap() error
    })
    if !ok {
        return nil
    }
    return u.Unwrap()
}

Unwrap()メソッドがあればそれを呼び、なければnilを返すコードになっています。errorインタフェースを満たすオブジェクトを作成する2つの標準ライブラリの関数のうち、errors.New()は最上位のエラーを作成するためのものなのでUnwrap()メソッドは持ちません。fmt.Errorf()で生成するエラーの場合はUnwrap()メソッドがあるので、ラップされた子エラーを取り出すことができます。

fmt.Errorf()を使う場合以外で、他の詳細なエラーに対して情報を付与して抽象度の高いエラーを扱う構造体などを作りたい場合には、このUnwrap()メソッドを用意するのが、エラー実装者のお作法ということになります。

バッチ処理的に複数のリクエストをバルクで送信したときに、一括でエラーを受け取るというのがGo CloudのdocstoreにはAPIであります。このActionListErrorは、errorインタフェースを満たす型ですが、配列でもあります。この型のUnwrap()は、配列が1つだけの要素であればその子要素を返すという実装になっています。

type ActionListError []struct {
    Index int
    Err   error
}

func (e ActionListError) Error() string {
    :
}

func (e ActionListError) Unwrap() error {
    if len(e) == 1 {
        return e[0].Err
    }
    return nil
}

エラーの比較

1.12以前の標準ライブラリ内のエラー比較

Goの標準ライブラリは設計が比較的きれいで、Goの勉強には標準ライブラリのコードを読めば良い、と長らく言われてきましたが、その中であまり美しくないのがエラーの比較です。標準ライブラリの中で3種類あります。

まず最初に、比較関数を使う方法です。osパッケージにはIsExists, IsNotExist, IsPermission, IsTimeoutがありますが、あまり見かけません。

_, err := os.Stats("file")
if os.IsNotExist(err) {
  // ファイルが存在しない
}

インスタンスの比較パターンもあります。インスタンスが比較できるというのは、エラー発生時には、グローバル変数に入っているerrors.New()などで定義しておいたもインスタンスをそのままreturnで返すし、比較にも使う方法です。追加情報がないものに限定されますが、使う側もシンプルで一番コードがきれいになります。

if err == io.EOF {
    // ファイル読み込み中に残りのデータを読み込めずに末尾に到達してしまった
}

それ以外だと型アサーションでキャストする方法もあります。

err := json.Unmarshal(jsonStr, person)
if _, ok := err.(*json.InvalidUnmarshalError); ok {
  // unmarshal errorのとき
}

Go 1.13以降のエラーの比較

Go 1.13ではエラー比較に使える関数が二つ追加されました。前者は2つのエラーを比較して同一種類であればtrueを返します。後者のAsはIsと似ていますが、指定された型へのキャストも行います。先ほどのGo 1.12以前に例で触れた分類で言えば、「型があっているかどうか」というシンプルな比較であれば前者、例えば文法チェックエラーで、エラーの情報とエラー箇所の追加情報を持っている場合に、それらにアクセスするという目的があればAsを使います。

  • errors.Is(err, target error) bool
  • errors.As(err error, target interface{}) bool

errors.Is(err, target error) bool

Isの実装は次のようになっています。

func Is(err, target error) bool {
    if target == nil {
        return err == target
    }

    isComparable := reflectlite.TypeOf(target).Comparable()
    for {
        if isComparable && err == target {
            return true
        }
        if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
            return true
        }
        // TODO: consider supporing target.Is(err). This would allow
        // user-definable predicates, but also may allow for coping with sloppy
        // APIs, thereby making it easier to get away with them.
        if err = Unwrap(err); err == nil {
            return false
        }
    }
}
  1. targetがnilならnilと比較
  2. 以下ループ
    1. targetが比較可能なら比較して一致したらtrue(以前のio.EOFパターン)
    2. errに Is(err error) boolメソッドがあったら、それを呼び出し、trueならそのまま返す
    3. errをUnwrapする。できなければfalseを返す

比較可能であれば比較して確認しますし、fmt.Errorfでラップした場合は、最後のUnwrapで必要とする値が出てくるまでは子供の探索をし続ける、というのは、1.13のラップ機構を使い始めた人には納得のロジックだと思います。もう一つきになるパターンが、Unwrapの時と同じ、 Is(err error) boolメソッドがあるかどうかをアノニマスなインタフェースで確認し、存在したら呼び出している点です。これでユーザー定義の比較関数が定義できるようになっています。TODOには逆にtarget.Isがあったら呼べるようにするというのが書かれています。現在はerr側のIsだけを呼んでいます。

errors.As(err error, target interface{}) bool

Asの実装は次のようになっています。Isよりもリフレクションバリバリですね。

func As(err error, target interface{}) bool {
    if target == nil {
        panic("errors: target cannot be nil")
    }
    val := reflectlite.ValueOf(target)
    typ := val.Type()
    if typ.Kind() != reflectlite.Ptr || val.IsNil() {
        panic("errors: target must be a non-nil pointer")
    }
    if e := typ.Elem(); e.Kind() != reflectlite.Interface && !e.Implements(errorType) {
        panic("errors: *target must be interface or implement error")
    }
    targetType := typ.Elem()
    for err != nil {
        if reflectlite.TypeOf(err).AssignableTo(targetType) {
            val.Elem().Set(reflectlite.ValueOf(err))
            return true
        }
        if x, ok := err.(interface{ As(interface{}) bool }); ok && x.As(target) {
            return true
        }
        err = Unwrap(err)
    }
    return false
}

エラーチェックがある以外の基本の流れはIsと似ています。

  1. targetには代入するので有効なポインタ型でなければpanic
  2. targetの要素は型を一致させて代入させる必要があるのでインタフェースかエラー型でないとpanic
  3. 以下ループ
    1. ターゲット型にerrが代入可能であれば代入して完了
    2. As(interface{}) boolメソッドがあればそれを呼ぶ
    3. Unwrapする。nilだったら終了

こちらもAsメソッドがあれば呼ぶという実装になっています。

実装例

Is/Asメソッドを用意しなくても、エラー型とインスタンスが1対1であれば他のロジックで正しく動作します。わざわざIs/Asメソッドを実装しなければならないのはそうならないケースです。下記のコードが実装例です。今までの比較では簡単に実装できなかったような比較元やキャスト元と先の型が違う(左辺がMyErrorで、右辺がMyError2という別の型)というのを実装してみました。

実際にはこのようなトリッキーな実装はあんまりないかもしれませんが、ActionListErrorのような複数のエラーをhas-aでまとめているようなWrapの特殊ケースとか、リトライしたときの過去のエラーも全部保持しておいて適切に処理したいケースとか、そういう場面でのみ役に立つでしょう。とはいえ、エラー処理なのでどのようなニッチケースが発生するかわかりませんので、存在を知っておいて(詳細は必要になったらこのエントリーを探してもらえれば)損はないでしょう。

package main

import (
    "errors"
    "fmt"
    "log"
)

type MyError struct {
    Detail string
}

type MyError2 struct {
    Detail string
}

func (e MyError2) Error() string {
    return fmt.Sprintf("MyError2: %s", e.Detail)
}

func (e MyError) Error() string {
    return fmt.Sprintf("MyError: %s", e.Detail)
}

func (e MyError) Is(target error) bool {
    log.Println("calling Is")
    _, ok := target.(*MyError2)
    return ok
}

func (e MyError) As(target interface{}) bool {
    log.Println("calling As")
    if cast, ok := target.(**MyError2); ok {
        (*cast).Detail = e.Detail
        return true
    }
    return false
}

func main() {
    myError := &MyError{
        Detail: "お腹すいた",
    }
    log.Println(errors.Is(myError, &MyError2{}))

    e := &MyError2{}
    if errors.As(myError, &e) {
        log.Println(e.Detail)
    }
}

まとめ

error型のインタフェース自体はError() stringメソッドを実装すれば満たされるという点は以前から変わっていません(変わっていたら大事件)。Go 1.13からは、これとは別に名前のない3種類のインタフェースを通じて、3つのメソッドがerrorsパッケージによって呼ばれる可能性があります。自前のエラー構造体を実装する場合、つぎの3つのメソッドも実装するとGoの言語標準のエラー処理で正しく扱うことができるようになります。

  • Unwrap() error
  • Is(target error) bool
  • As(target interface{}) bool

とはいえ、実装例で触れたように、基本的にエラーの構造体を1つ作り、その型とのIs/AsだけであればGo 1.12のようなError()メソッドだけを持つ構造体を作れば問題にはなりません。より特殊な構造化が必要なときだけ必要になる機能ですが、一応そのような場面でも対処できる退路はきちんと確保してあるよ、というのが本エントリーの結論です。

明日は@crifffさんの投稿です。

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