- 投稿日:2019-12-03T23:59:31+09:00
Go Modules時代の静的解析
はじめに
この記事はGo5 Advent Calendar 2019 4日目の記事です。
Go Modulesがデフォルトで有効になったGo1.13がリリースされてはや3ヶ月。皆さんいかがお過ごしでしょうか。私は「Modules何もわからん」となっててんやわんやでした。
この記事ではModulesの変化の影響を受けやすいGoプログラムの静的解析について、入門〜中級者向けに解説していきます。静的解析とは?
プログラムを実行せずにその内容を解析することを静的解析と言います。コンパイラによる構文解析や型チェックをはじめ、エディタがタイポを検出したり補完機能を提供したりするのも静的解析です。これらとは逆に、ユニットテストや性能評価などはプログラムを実行しているので動的解析と呼ばれます。
Goはgofmt
やgoimports
など静的解析ツールが豊富なことでも有名です。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
- Mercariのブログが参考になる
- とにかく動かしたい、基礎を理解したい →
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/analysistest
では複雑になりがちな静的解析プログラムのテストの記述を支援します。また、サブパッケージanalysis/passes/*
としてよく用いられる解析処理を提供しています。
analysis/passes/*
の多くはgo vet
コマンドにより利用できます。例外であるbuildssa
,ctrlflow
,inspect
は他のAnalyzerに利用されることを前提としたAnalyzerで、新たに静的解析ツールを自作する場合も利用できます。それぞれ後で解説するssa
、cfg
、ast/inspector
が実装の本体となっています。このパッケージに関してはGoにおける静的解析のモジュール化について - Mercari Engineering Blogが非常によくまとまっていて分かりやすいので一読をおすすめします。
Package ast/astutil
既存のASTを改変して新しいASTを作る関数
Apply()
や式(Expr)の括弧を外して中身を取り出す関数Unparen()
などASTに関するをユーティリティ提供します。Package ast/inspector
ASTをトラバースするためのデータ構造を提供します。標準パッケージの
ast.Inspect()
に比べて、メモ化による複数呼び出しの高速化とノードのフィルタリング、探索スタック付きのトラバースをサポートします。analysis/passes/inspect
で利用されているのはこちらになります。
メモ化のためのオーバーヘッドがあるので、同じASTに対して数回しか呼び出さない場合はast.Inspect()
のほうが高速なことに注意です。Package callgraph
関数Aの内部で関数Bが呼び出されている、といった関係をグラフとして表現します。グラフの生成の実装はサブパッケージとして
cha
,rta
,static
の3種類のアルゴリズムが提供されています。Package cfg
ある関数内での
if
やswitch
による分岐をグラフで表現します。ある処理が必ず呼び出されているか検査したいときなどに便利です。ただし論理演算子の短絡評価やpanic
による分岐などはサポートされていないため、それらが必要な場合後述するgolang.org/x/tools/go/ssa
を使います。Package packages
ファイルの探索から字句解析、構文解析そして型チェックを一括で処理してくれる上位APIです。
analysis
パッケージを用いない場合はこのパッケージが解析の起点になると思います。Package ssa
静的単一代入(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で静的解析に入門しましょう!
- 投稿日:2019-12-03T23:56:42+09:00
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.gopackage 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: 57474https://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.gopackage 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.gopackage 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.gofunc TestPanic(t *testing.T) { panic("from TestPanic") }ですのでpanicが起きたらrecover()で捕まえてインスタンスを終了すればいいので、TestMainのdeferすればよいと考え、TestMainを以下のように変更しました。
main_test.gofunc 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.gofunc 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等で共通の前後処理を行おうとすると例外時にハマることになるというお話でした。
- 投稿日:2019-12-03T23:01:43+09:00
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
コメントのデータをひっぱるのに使用使い方
- マルチコメントビューワーでHTML5コメジェネのパスを通す
- マルチコメントビューワーで通常通りコメントを収集する
- localhost:8080にアクセスする
- コメントが表示されるようになる
- カラーキーを指定して背景をぶっこ抜くことでOBS上で一定時間以上コメントがない場合は自動的に透明になるよ!
実装方法
- localhostにアクセスされたらconfigを読み出し、Websocketでの通信に切り替える
- TwitterからIFTTTを通してスプレッドシートに送られたデータと、マルチコメビュから出力されたxmlを読み込む
- 2で得られたデータの今までに送信したデータとの差分を取って残りをwebsocketで送る
- 受け取ったらキューに格納して一定時間ごとに取り出す
- キューの中にデータが一定時間以上なくなったら表示部分を
opacity: 0;
にして消去感想的な何か
そもそもローカルでしか動かないのにサーバーサイドとは()
現在アプリケーションとして開発中です・・・
- 投稿日:2019-12-03T22:58:58+09:00
Goの基本個所の復習①(println)
はじめに
私は現在Ruby on railsやJavaScript(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()
に相当します。
(ちなみに、printI
nではなくprintl
nなので注意を)今回は
()
の中身にそれぞれHello World
と2019
が入力されているので、出力は前述の通りとなります。
- 投稿日:2019-12-03T22:49:30+09:00
Firebase AuthenticationとGAEで30分くらいでログイン機能を実装してみる
TL;DR
今からFirebase Authenticationを使ってソーシャルログイン機能を30分くらいで検証用に実装します。
各種SNSのデベロッパーサイトへの登録はしてあるものとします。
要件として作成するのは、トップページと、ログイン後ページの2画面のシンプルな構成。コードは置いといた。(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ログイン
- ログアウト
アプリケーションの構築
今回は以下のようなシステム構成を目指します。
赤い部分が我々がコードで表現するアプリケーション部分です。
それ以外の部分は各種コンソール画面から、ボタンポチポチで実現できます。各種ソーシャルプロバイダの登録
まずは以下のソーシャルプロバイダのデベロッパーサイトへ登録し、APIキーなどを使用可能な状態にしてください。詳細な手順は省きます。
- Twitter Developers : https://developer.twitter.com/content/developer-twitter/ja.html
- Facebook for Developers : https://developers.facebook.com/?locale=ja_JP
- Github developers: https://github.com/settings/developers
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/helloworldFirebase Consoleでの認証の設定
https://console.firebase.google.com/u/0/
Firebaseのクラウドコンソールにアクセスしてみましょう。
"ログイン方法"のタブにいくと、各種プロバイダのログイン方法が指定できますので、必要なものを有効にしましょう。今回は最初に述べた、「メール/パスワード」、「Google」、「Twitter」、「Facebook」、「Github」の5つを有効にしましょう。
ここで有効にするために、プロバイダによっては、APIキー及び、シークレットキーなどを求められるので、最初に各種デベロッパーサイトで設定して入手したAPIキーとシークレットキーを入力しましょう。とりあえず画面を2つつくる
すでに作成したGAEにてHello Worldを表示するアプリケーションが存在する前提で進みます。
とりあえずGoからサーバサイドレンダリングでHTMLの内容を表示するだけでいいので、一旦、今回作りたいトップページと、ログイン後ページのガワを作ります。
構成はこんな感じ。
├── app.yaml ├── main.go └── templates ├── my.html └── signup.html各ファイルは以下の通り。
app.yamlruntime: go113templateディレクトリを作り、その配下に
サインアップ前のトップページと、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.gopackage 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コンソールプロジェクトの設定をみる。
Settingページへ遷移するので、「アプリを追加」ボタンを押す。
今回はWebアプリなのでWebを選択。
適当にアプリの名前を決めると、以下のような画面になるので、ここで表示されたコードを、自分のアプリケーションに埋め込む。(今回はFirebase Hostingは使わないので未チェックでOK)
アプリケーションに上記のコードを埋め込んで動作させる
上記コードはGithubで公開とかしないならベタ書きでもいいのですが、あんまり見られたくないので、
secret.yaml
に環境変数として記述し、それをapp.yaml
で読み込むようにします。
また、secret.yaml
をGithubに公開したら意味ないので、.gitignore
にsecret.yaml
を記載して、git管理対象から除外します。
複数人で共有する場合は、secret.yaml
を暗号化するか、別の仕組みを使うかする必要がありますが、今回のアプリケーションではそこまではやりません。最終的なディレクトリ構造はこんな感じ。
├── app.yaml ├── go.mod ├── main.go ├── secret.yaml ├── .gitignore └── templates ├── my.html └── signup.html
secret.yaml
はこんな感じで書いておいて、secret.yamlenv_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.yamlenv_variables: runtime: go113 includes: - secret.yaml
secret.yaml
は.gitignore
に。secret.yamlsecret.yaml
次に
main.go
内にてos.Getenv
で環境変数を読み込み,signup.html
とmy.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とかをいろいろいじったりする必要はありません。
サインイン状態は以下のように、
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を使うと簡単にソーシャルログイン機能が作れます。
レンタルサーバーとか借りようと思ってる人、ためしに使ってみては?困ったら
信頼性の高い公式ドキュメントを見ましょう。
ただし翻訳のタイムロスが無い分、ドキュメントは日本語のものより英語のもののほうが情報が新しかったりするので注意してください。
- 投稿日:2019-12-03T22:13:48+09:00
GoでtwitterAPI触ってみた
Goの勉強も含めて、Twitter APIを利用して、
「アオキ大好き!最高!」
とツイートしている人を全員フォロー
してみせます。
anaconda
というパッケージを使います。anacondaとは
Pythonのあれじゃないです。
anaconda
とは、Twitter APIにアクセスするためのGoパッケージです。便利!Twitter APIの利用申請
間違いなく一番めんどくさい過程。
こちらを見ながら頑張りましょう。結構テキトーでも通ります。実装
main.gopackage 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件ヒットしました!!今回作成したリポジトリはこちらから
- 投稿日:2019-12-03T21:53:49+09:00
Go言語の標準パッケージだけで画像処理をする その1 (入出力)
ZOZOテクノロジーズ #5 Advent Calendar 2019の記事です。
昨日は @satto_sann さんの「Alexaのアカウントリンクをデバッグする方法」でした。本記事では、Go言語の標準パッケージでの画像処理について書いていきます。
はじめに
最初に明言しておきますが、これはGo言語での画像処理を勧める記事ではありません。
あくまで、自分にメリットがありそうという方のみ参考にしていただければと思います。自分はZOZOテクノロジーズの中でも、アパレル生産におけるファクトリーオートメーションという分野を担当しています。
担当分野が広いというのと、自動化の対象がアパレル関係ということもあり、
開発環境を実行環境に入れることができなかったり、実行環境により使う言語を変えたりと手間がかかってしまう場面がよく発生します。特に画像処理関係でプロトタイプをサクッと作る際には、開発環境をその都度作るのが面倒だったりします。
そんな手間を無くせないかなーとふと思い、本記事シリーズを書くことにしました。
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 (回転、反転)」という記事を書きます。
- 投稿日:2019-12-03T21:50:58+09:00
Goの並行処理パターンを使って速攻で分かった気になる
この記事はチームラボエンジニアリングアドベントカレンダー10日目の記事です。
昨日の記事は、bonobono555さんによるCompletableFutureの公式ドキュメントを読んでも分からないので Java逆引きレシピを片手に理解するでした。はじめに
趣味でGoを触るようになって半年ぐらいは経ったと思います!
こんなにハマったのは並行処理がめちゃくちゃ面白かったからです。関数型でも基本的に上から処理されますよね?
(手続き型だと、基本上から、関数型は高階関数多用してると上からでは無くなりますが)
それがgoroutineを使うことによって簡単に並行に処理をさせる事が出来るんです!
これを、他の言語で同じ事やろうとしたらすごく難しいんです!(やった事ないから知らんけど)とりあえずムチャクチャ楽しいんでこの記事がその入り口にでもなってくれたら嬉しいです!
この記事では詳しい説明より要点をまとめた並行処理パターンを紹介します!並行処理とは? 並列処理と混ざってない?
突然ですが、並行と並列の違い分かりますか??
一言で言うと、
並行処理は1人がその場の判断で沢山の仕事をこなす事
並列処理は複数人が仕事を同時にこなす事
です!並行処理はあくまで処理を同時に行ってはいないんです。
この様に処理を切り替えて行います。(人の目には同時に見える)
これが並行と並列のザックリとした違いです。
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さんによるジェネリクスを見ても逃げないです。
参考
- 投稿日:2019-12-03T21:46:00+09:00
Goで便利なTUIファイラーを作ったのでその実装の話
こんにちわ。ゴリラです。
最近GoでシンプルなTUIファイラーを作ったので、軽く紹介して実装の話をしていきます。以前書いたこちらの記事でも軽く紹介しています。
機能
ざっくり以下の機能を持っています。
- ディレクトリ、ファイルの作成、削除、コピー、リネーム、プレビュー
- ディレクトリのブックマーク
詳しくはREADMEを参照してください。
実装について
筆者はTUIツールを作るときに、いつもtviewというライブラリを使っています。
tview
の基本的な使い方については別途記事を書く予定なので、本記事ではそれ以外でいくつかの機能の実装について解説していきます。ファイル一覧
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
が表示されます。
Owner
とGroup
Sys()
をsyscall.Stat_t
に型アサーションしてOwner
とGroup
を取ります。Sys()
の実態はOSによって異なります。Windowsの場合はsyscall.Win32FileAttributeData
型にアサーションします。
Stat_t
からはUid
とGid
を取得できるので、それをもとにos/user
パッケージのLookupId
とLookupGroupId
を使用してユーザ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というライブラリをを使いました。ブックマーク
ff
はb
でディレクトリをブックマークしておくことができます。ブックマークはデフォルトでは無効になっているので、
以下のように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
inVim
になってしまうんです。
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
は任意の外部コマンドを実行する事ができます。内部では単に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に記述していますが、ユーザに気をつけてねっていうような機能は果たしてどうなんだろう?というモヤモヤ感は自分の中にあるので将来的にはこの機能を廃止するかもしれないです。
まとめ
ざっくりですが、実装について軽く解説しました。ファイラーを作ることで、ファイルシステムまわりについて知見を得られたので、作ってよかったと感じています。
ただ、ファイルシステムの深いところは全然わからないので(例えばなぜコピーのシステムコールがないのかなど)、そこは引き続き勉強していこうと思います。
- 投稿日:2019-12-03T21:38:01+09:00
効率の悪い 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 さんです!
- 投稿日:2019-12-03T19:25:30+09:00
【Go言語】作成したpackageをimportして使用。「cannot find module for path」エラー時の解決
◇ 流れ
【Go言語】packageを作成
↓
importに「./XXXXX」でパス指定
↓
実行
↓
「cannot find module for path」と怒られる
↓
解決◇ ディレクトリ構成
GOPATH
┣━bin
┣━pkg
┗━src
┗━test
┣━route
┃ ┗━route.go
┣━go.mod
┗━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 init
でgo.mod
を作成。ターミナル$ cd C:\GOPATH\src\test $ go mod init go: creating new go.mod: module testgo.modmodule test go 1.13◇ main.go と route パッケージを作成。
main.gopackage main import "./route" func main() { route.Route() }route/route.gopackage 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.modmodule test ← modulの名前 go 1.13main.gopackage 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
- 投稿日:2019-12-03T17:32:45+09:00
[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も含めて)には、たいていインターフェースが含まれている事が多いと感じる。使用したい外部ライブラリがあったら、直接参照せずに済まないかインターフェースを探してみよう。
- 投稿日:2019-12-03T14:27:07+09:00
本番環境で使える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に変換して提供するためのミドルウェアです。
インストール
$ 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$ # fetches this repo into $GOPATH $ go get -d github.com/envoyproxy/protoc-gen-validate $ # installs PGV into $GOPATH/bin $ make buildprotoファイル
user.protosyntax = "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.go
とlogin.pb.validate.go
が生成されるはずです。gRPC Serverの実装
ここからは実際のコードを見ながら解説していきます。
一部抜粋して説明するので、コード全体が見たい場合はサンプルコードを見てください。main.gopackage 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.gofunc 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で使うように設定しています。
RegisterLoginServiceServer
とRegisterUserServiceServer
は、前述のコマンドで生成したgoファイルに実装されているので、それを呼び出すだけです。
また、生成されたgoファイルには、LoginServiceServer
やUserServiceServer
といったインターフェースが定義されいます。
それを実装したうえでそれぞれを初期化する関数がNewLoginServiceServer
やNewUserServiceServer
になっています。ここまではいろんな解説ページに載っていることですが、インターセプターに関しては情報が少ないのと、導入するプロジェクトに必要なインターフェースを選択したり、場合によっては実装することになるので、以降は上記の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の採番など)も解説するので、お楽しみに。
- 投稿日:2019-12-03T14:08:05+09:00
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.yaml1. Persistent Diskを使って、NFSサーバ立ち上げ
nfs-container.deployment.yamlapiVersion: 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.yamlapiVersion: 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.yaml2. NFSを使って、GKE中にストレージを作成
nfs-volume.yamlapiVersion: 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.yaml3. GKEのストレージマウントのPod作成
golangで簡単なジョブを作成する。
ジョブ処理はoutput-pathにサンプル10ファイルを作成する。予定は複数ジョブを稼働して、共有用のNFSにファイルを作成する。dummyjob.gopackage 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.DockerfileFROM alpine:latest WORKDIR /app COPY ./dummyjob /appcloudbuild.dummyjob.yamlsteps: # 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.yaml2つのジョブはNFSを共有利用できることを稼働ログで確認できます。
NFSを共有して読み書きできることを確認できました。
本記事で利用したソースコードはこちら
https://github.com/itdevsamurai/gke/tree/master/nfs
最後まで読んで頂き、どうも有難う御座います!
DevSamurai 橋本
- 投稿日:2019-12-03T13:11:21+09:00
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
- 投稿日:2019-12-03T06:00:19+09:00
ぼくのかんがえたさいきょうのシングルバイナリ生成ツール
皆さん、シングルバイナリ サイコーですかー!(挨拶)
この記事は 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。上の
jsonize
、paw-ua
、insert-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.bash
、jsonize
、paw-ua
、ip2geo
、insert-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
mylog
とpipeline
の間に-
(ハイフン)がない点にご注意ください。mylog-pipeline
がmylog
のサブコマンドのようにして呼び出されていますね。これは、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コンパイラはコンパイルしてくれるんですねー。すごいですねー!(小並感)11Let's enjoy シングルバイナリ
というわけで、皆さんもお手軽便利なシングルバイナリ・ライフを楽しんでください。
詳しい使い方は WIOのドキュメントを参照してくださいね。
それでは、シングルバイナリまじサイコー!(別れの挨拶)
ギョーム内容につき、お察しください。 ↩
ちなみに自分は、とりあえずログをJSONに変換してしまって、LDJSONをパイプラインでひとつひとつ変換処理かけていくのが好みです。こうするとパイプラインの各コマンドが並列で走るので、自然とマルチコアの恩恵を受けられますしね。 ↩
MaxMaind社の提供するIPアドレスと地点情報の紐付けデータベースです(https://dev.maxmind.com/geoip/geoip2/geolite2/ )。 GeoIp2 で使います。 ↩
ちなみに今のところ、Linux(Ubuntu 16.04)、macOS (Mojave)、Windows 10 (1909) で動作確認してあります。たぶん、BSDとかの他のUNIX系OSでも動くとは思いますが、確認環境がないためビルドタグはつけていません。どなたか興味がある人は、誰か試してみてほしいです。 ↩
あとで紹介しますが、ファイル名を変えなくてもバイナリに含む方法もあります。 ↩
まぁ、それを言ったら
xz
もなんですが、イマドキはxz
は標準で入っていることが多いので、横においておきます。 ↩ちなみに、作業環境は開発環境・本番環境ともにUbuntuです。(ただし、開発環境はUbuntu on WSL) ↩
今回はたまたま
jq
の依存ライブラリが少なくてうまくいきましたが、現実にはこんなふうにうまくいくケースは少ないと思います。依存ライブラリが大量にある場合もありますし、そのライブラリがさらに他のライブラリに依存していたりするので、ただしく必要なライブラリのリストを抽出するのはかなり大変です。従って、シングルバイナリで動くGo製のコマンドか、シェルスクリプトを埋め込むのが現実的な範囲です。複雑な依存関係をデプロイしたいなら、素直にDockerみたいなコンテナで配布するのがいいと思います。だだし、Dockerを使うためにはroot権限が必要なんですよね。root権限なしに、ホームディレクトリにコピるだけで動くのが、シングルバイナリの利点ですよね! ↩お気づきの方もいるかもしれませんが、これは古式ゆかしい(?)PAR::Packaer と同じ仕組みです。 ↩
ご想像のとおり、サブコマンドをPATHから探索して実行する様式はGitサブコマンドをマネたものです。このあたりのmain関数の挙動は
main.go
を書き換えれば好きなように変更できます。詳しくは WIOのドキュメントを参照してください。 ↩ちなみにこの手法は、今一番主流のバイナリ埋め込みツールと目されるstatikを参考にしています。なので、こんな乱暴な方法でも問題はないと思います。 ↩
- 投稿日:2019-12-03T03:54:11+09:00
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サーバをインストールします。
インストール手順は「こちらを参考」にしてください。実装は認証サーバに登録する処理と認証する処理の二つの手順を説明します。
①登録のフロー
アテステーションがよくわからないと思うので[こちらの記事]を参考にしてください。②認証のフロー
登録と認証でフローにあまり差がないのでまとめて解説しますが、ここでは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 ) 上記までの手順でエラーがなかった場合,登録完了ページをブラウザに表示する.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]アテステーションとは
- 投稿日:2019-12-03T01:42:51+09:00
Twistのchatbotを拡張してみた
昨日作ったchatbotがあのままだと全く実用性がないので、もうちょっとサンプルっぽくしました
昨日作ったもの
https://qiita.com/usk81/items/bfd15ec3c5ecc23d0b8fTwistに関しては昨日作ったものの記事を参考にしてください
成果物
コメントしたものをそのまま返すようにしました
Page: https://github.com/usk81/twistbot
Release: https://github.com/usk81/twistbot/releases/tag/v0.0.1-alpha2Request Body
Request Bodyは
x-www-form-urlencoded
で送られているようです。
どうせなので、Structureにおこしてみました。
というか、Developerページと差異があってほぼここの解析に時間使ってました。Structureへのデコードは
Gorilla
のSchema
を使用しました。
https://github.com/gorilla/schema今回は対応しませんでしたが、リクエストの中に
url_ttl
とurl_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/zapmessage_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)) }
- 投稿日:2019-12-03T01:15:15+09:00
Go言語でユニットテスト, テストしやすいコードにモックを書く
Go言語でテストしやすいコードを書く
Go言語でテスタブル(テストしやすい)コードを書いてユニットテストを実践しましょう.
この記事では以下について書きます.ユニットテストの概要については書きません.
- Go言語でのユニットテストの書き方
- テストアサーション
- Go言語でのユニットテストの実行方法
- テストしやすいコードの書き方
- Go言語でのモックの作成とモックの使い方
Go言語でのユニットテストの書き方
テストコードのファイル(xxxx_test.go)
Go言語では本体コードと同じパッケージ内(同じディレクトリ内)にテストファイルを作成するのが一般的のようです. ファイル名は
xxx_test.go
にするのがルールです.├── calc.go └── calc_test.goテストコードの基本
テストコードでは以下のパッケージを使います.
- testing (必須)
- github.com/stretchr/testify/assert (アサーション用, 必須ではないがあると便利)
関数は
TestXXXX
という名称で引数に*testing.T
型を取る必要があります.
関数名は日本語も含めることができます.テストコードではテスト項目をよりわかりやすくするために開発チームが最も理解できる言葉で書く場合があります(筆者個人としては,テストコードに限っては日本語関数名を勧めて言います.).func TestGetUser_正常系(t *testing.T) { } func TestGetUser_データベース接続障害時の場合にエラーを返すことを検証(t *testing.T) { }例としてsumという関数のテストを書いてみます.
calc.gofunc sum(a, b int) int { return a + b }calc_test.goimport ( "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.gotype 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.gofunc (db *productDB) GetProduct(data *model.Product) { data = &model.Product{} }この場合,以下のようにすることで引数に代入した振る舞いができます.
expectedProduct := &model.Product{} dbMock.EXPECT().GetProduct.SetArg(0, expectedProduct)まとめ
Go言語におけるユニットテストとユニットテストを書きやすくするための実装方法,モック化について書きました.
Tips
モックの生成時にエラーになったら..
モック生成後,元の構造体・インターフェイスを変更後に再度生成する際にエラーになる場合があります.
大抵は既存のコードと新しいコードで関係性が維持できない場合.そんなときは,問題発生原因のコードをビルド対象から外すとモックが生成できるようになるかもしれません.以下の記述をソースコードの1行目に加えると外れます.
// +build test
- 投稿日:2019-12-03T01:15:15+09:00
Go言語でユニットテスト, テストしやすいコードとモックを書く
Go言語でテストしやすいコードを書く
Go言語でテスタブル(テストしやすい)コードを書いてユニットテストを実践しましょう.
この記事では以下について書きます.ユニットテストの概要については書きません.
- Go言語でのユニットテストの書き方
- テストアサーション
- Go言語でのユニットテストの実行方法
- テストしやすいコードの書き方
- Go言語でのモックの作成とモックの使い方
Go言語でのユニットテストの書き方
テストコードのファイル(xxxx_test.go)
Go言語では本体コードと同じパッケージ内(同じディレクトリ内)にテストファイルを作成するのが一般的のようです. ファイル名は
xxx_test.go
にするのがルールです.├── calc.go └── calc_test.goテストコードの基本
テストコードでは以下のパッケージを使います.
- testing (必須)
- github.com/stretchr/testify/assert (アサーション用, 必須ではないがあると便利)
関数は
TestXXXX
という名称で引数に*testing.T
型を取る必要があります.
関数名は日本語も含めることができます.テストコードではテスト項目をよりわかりやすくするために開発チームが最も理解できる言葉で書く場合があります(筆者個人としては,テストコードに限っては日本語関数名を勧めて言います.).func TestGetUser_正常系(t *testing.T) { } func TestGetUser_データベース接続障害時の場合にエラーを返すことを検証(t *testing.T) { }例としてsumという関数のテストを書いてみます.
calc.gofunc sum(a, b int) int { return a + b }calc_test.goimport ( "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.gotype 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.gofunc (db *productDB) GetProduct(data *model.Product) { data = &model.Product{} }この場合,以下のようにすることで引数に代入した振る舞いができます.
expectedProduct := &model.Product{} dbMock.EXPECT().GetProduct.SetArg(0, expectedProduct)まとめ
Go言語におけるユニットテストとユニットテストを書きやすくするための実装方法,モック化について書きました.
Tips
モックの生成時にエラーになったら..
モック生成後,元の構造体・インターフェイスを変更後に再度生成する際にエラーになる場合があります.
大抵は既存のコードと新しいコードで関係性が維持できない場合.そんなときは,問題発生原因のコードをビルド対象から外すとモックが生成できるようになるかもしれません.以下の記述をソースコードの1行目に加えると外れます.
// +build test
- 投稿日:2019-12-03T01:09:50+09:00
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に分がありますが、ただのチャットツールとして使っているのであれば乗り換えたほうがハッピーになれそうです。(ファンですが、中の人ではありませんよ)
Bot
Webhookの仕様
Slackなどであればポーリングしないといけませんが、TwistはREST Hookを採用しているので、subscribeに登録すれば、イベントが発生したときに登録したURLに勝手にリクエストが送られるのでポーリングは不要で、普通のREST APIの応用で作成できます。
https://resthooks.org/Botの場合、
General Integration
を設定すればsubscribeの登録も不要なのでかなり楽です。設定方法
- 自分のTeamにログインし、
Add integrations
をクリックします![]()
- Manageをクリックして新規追加をします
![]()
- 名前や説明を書きます。
Integration type
はGeneral Integration
を設定してください。
General Integration
を設定するとOutgoing Hookを別途設定しなくても全てのイベントに反応してくれます。
4.Bot
をクリックし、Outgoing webhook URL
を設定します。URLはベーシック認証付きのものだとエラーで設定できないようなので、セキュリティを考慮したいときはパラメータでトークンを渡すかIPなどで制限するしかなさそうです。
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にコメントを送ると下記のようになります
作ったものはここにあります。
作ってみたい方は参考にどうぞ。page: https://github.com/usk81/twistbot
release: https://github.com/usk81/twistbot/releases/tag/v0.0.1-alpha1
- 投稿日:2019-12-03T00:26:34+09:00
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] 21bと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関数を使用し別の変数に格納した方がいいみたいです
終わりに
今回、自分の未熟さゆえに曖昧な部分が多いのに加え適切な言い方ではない箇所があるためとてもいい記事ではないと思います。ですが今回初めてこんな挙動をすると知ったので知らない人に知っていただければと思い書きました。また本記事について詳しく知っている方はコメントにて教えて頂けるととても助かります。
- 投稿日:2019-12-03T00:26:34+09:00
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] 21bと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関数を使用し別の変数に格納した方がいいみたいです
終わりに
今回、自分の未熟さゆえに曖昧な部分が多いのに加え適切な言い方ではない箇所があるためとてもいい記事ではないと思います。ですが今回初めてこんな挙動をすると知ったので知らない人に知っていただければと思い書きました。また本記事について詳しく知っている方はコメントにて教えて頂けるととても助かります。
- 投稿日:2019-12-03T00:16:22+09:00
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 } } }
- targetがnilならnilと比較
- 以下ループ
- targetが比較可能なら比較して一致したらtrue(以前のio.EOFパターン)
- errに
Is(err error) bool
メソッドがあったら、それを呼び出し、trueならそのまま返す- 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と似ています。
- targetには代入するので有効なポインタ型でなければpanic
- targetの要素は型を一致させて代入させる必要があるのでインタフェースかエラー型でないとpanic
- 以下ループ
- ターゲット型にerrが代入可能であれば代入して完了
As(interface{}) bool
メソッドがあればそれを呼ぶ- 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さんの投稿です。