haya14busa

haya14busa’s memo

Go に恋した Vimmer の2016年の振り返り

この記事では2016年 @haya14busa がやってきた活動,特にVimとかGo 活動をざっくり振り返ります. 個人的な備忘録です.あとポエム.

過去の振り返り記事

今年はざっくりプログラミング & Vimmer 歴 4年目でした. そろそろプログラミングの基礎的なところわかってなくても,まだはじめてそんな経ってなくてまだ勉強できてないんです〜,また今度やっておきますっ! みたいな言い訳が完全に通用しない年になってきた気がします (そもそも実際に言ったことは無い). まだまだ学んでおきたい分野はたくさんあって,特にもうちょっとレイヤーが下の要素もやっていって理解しておきたさがありますね…

“Go” に恋した 2016年

AlphaGo, Pokemon GO, Amazon Go… “Go” は2016年の1つのキーワードだったように思いますが,個人的には Go 言語の “Go” に恋した1年でした.

credit: Gopher by tenntenn CC BY 3.0

Go との出会い

Go 言語との出会いは去年インターンでGoをはじめて使ったときでした.

その時一目惚れ… をしたわけではありません.

シンプルでとにかくプロジェクトで動くものは作れる感じだったので,なんとなくGoよさそうかなぁとは感じつつも, ちょっと気になっている子(Scala) との大きな違いに戸惑ってたりしてました. “Functional Programming”? なにそれおいしいの? とでも聞こえてきそうな Go の雰囲気に, 僕は一歩足を引いて評価を保留してました. ただ,さも vim で書くために生まれてきた言語では…? というほどの gofmt といった周辺のツールの充実っぷりはこの頃から大好きで,言語というよりは周囲の エコシステムがよさ…という印象でした.

また,Go と出会ったころと時を同じくして,僕はスプラトゥーンと出会ってました. お家に帰って Go やってる場合ではなく,一日中イカする毎日. イカの存在によって Go とはどこかビジネスライクな付き合いにとどまってました.イカはっょぃ.

Go との別れ

それから程なくして,インターンが終了しました. Go をいちから学ぶところからはじめたにも関わらず, 終了時にある程度のものが出来たことを Go にも感謝しつつ, 一旦 Go との付き合いもお別れになりました.

結局,まぁなんかよさそうな言語だ程度の(ふんわり)印象でここで一旦お別れしたわけですが, 後々,このときのいい環境で Go を書いてレビューしてもらったり, 教えてもらったことはとても役に立ちました. ありがたい.

Go との再会

2016年の春,忙しかった時期も一段落し,スプラトゥーンとの距離をうまく保てるようになった僕は ちょっとした小さいツールを Go で書くことにしました.

理由としては,コマンドラインツールとか Go で書くのは便利そうだったことや, 来年以降もGo書くことになりそうかなぁという打算的な思考, また同じ頃にはじめたアルバイト先のいくつかのサブシステムは Go で書かれていることもあり, Go 書けるとそのへんも触れておもしろそうかなという気持ちがありました.

数カ月の Go とのお別れ期間でいろいろ忘れていることに戸惑いつつも,書いていくうちに勘も戻ってきました. ちょっとした自分用ツールをいくつか Go で完成させて,簡単にサーバにもっていって動かすことができてよいなーと感じたり, Google App Engine で雑に動かしたりして遊んでました. アルバイト先でもちょっとずつ書いたりするようになって,Go と触れ合う時間は増えました.

2016年秋,気づけば Go に恋に落ちてた

それからも,定期的に Go で何か作ったりしてました.例えば,

この頃には Go がかなり手に馴染むようになってきました.上記に上げたツールも Go の開発を助けるツールだということからも,Go をより書くようになってきてることがうかがえます.

特にきっかけはありませんでした.でもこの頃にはもう Go に恋してたんだと思います. その結果,特に意味もなく Vim script のパーサを Go 言語で動かしたいなぁ… 既存の Python 実装とかあるけど,Go でいじりたいなぁ… と思って Go 実装を作ったりしてました.

go-vimlparser - Vim Script Parser written in Go

Just for fun ではじめましたが,結果として最速 Vim script parser 実装となって,Vimmer にも嬉しい便利なものになったと思います. Go のパフォーマンスチューニングのやり方も改めて経験できて身についたし, AST の表現や AST walker の実装をするために go/ast のコードを読んでインターフェースをパク…参考にしたりとおもしろかった. Go の標準ライブラリのデザインはとても参考になるし,それを Go 言語でザクザク読めるのは本当にいいなぁと思います.

Go と Vim との共同作業

この頃,Vim はJSONやchannel, job 機能が実装され,外部インターフェースとの通信が容易になり,もうすぐ Vim 8.0 出すよ〜という時期でした. Go に恋した Vimmer としては,もちろんここで Go 言語を使って Vim との共同作業をさせたいというのは必然です! (これでは Go と Vim がカップルになってるのではということは気にしない.でも実際2人の相性はとてもよい)

このあたりの話は Vim Advent Calendar で書きました.

vim-go-client の通信のハンドリングのデザインは Go の net/http のコードを読んで参考にしたりしました. が,もうちょっといい感じにできそうな気がする… 実際に Go で vim-stacktrace という便利プラグインを作れることを確認できて,go-vimlparser も有効活用できたりなど, Vim活にも Go が絡むようになってきてますます Go が好きになっていきました.

Go と Vim との間に生まれた子供 - reviewdog

(Go と Vim が結婚して子供を産みました.僕のGoへの恋は片想い)

reviewdog logo

reviewdog は僕の 2016年に作ったプロダクトの一番のヒット作と言えます.reviewdog の GitHub のスターは執筆時現在 268 です. reviewdog が好きなのは,実際に超便利プロダクトだということはもちろん,Vim の ‘errorformat’ という便利な機能を Go言語で port して Vim の外でも使えるようにしたという,Vim と Go の”よさ” が存分に発揮されたプロダクトだというところがとても気に入っています.

reviewdog のようなものを作るには,いろんな linter などのコマンドの結果を扱わなくてはいけません. 他のツールでは, gometalinter のように linter ごとに正規表現をアドホックに作ったり, それぞれのコマンドの runner を作ったり, checkstyle xml やJSONのある機械的な形式を吐くlinterにのみ対応するといった割り切りをしています.

Vim はそういういろんなコマンドやいろんなアウトプット形式に対応するというのは得意で, ‘errorformat’ という scanf-like な機能がこれにあたり,この形式を使うことに決めました.

ただしこの’errorformat’は Vim でしか使えません.そこを同じくマルチプラットフォームで簡単に動く Go 言語で同等の機能をポート(haya14busa/errorformat)し, Vim のよさを Go がサポートしてより広く使ってもらえるようにできました.

reviewdog は現在もちょくちょく改善していて,特にローカルでもより動かしやすいようにしようと思ってます. 年内にある程度開発してバージョン1.0にしようかと思っていましたが,もう少しかかりそうです.

reviewdog の状況

OSS で導入するのは CI サービスがうまく Secure Token を扱ってくれないせいで, 若干最初の導入が手間なのが使ってもらうには少し壁になってるのが悲しい.Travis〜CircleCI〜頼む〜改善してくれ〜. 一度導入してしまえばあとは楽だし,もしくはローカルでだけ使っても便利なのでもうちょっと使用事案増えて欲しい.

Go 活動その他

Go が好き

書いてるうちに手に馴染んできて,いつの間にか好きになってましたが,結局僕はなぜ Go に恋に落ちたのでしょうか. 1つは間違いなく Go の Simplicity に惹かれたんだと思います.

Simplicity is Complicated

Rob Pike の “Simplicity is Complicated” という発表がオススメです. 僕はこの発表をみて,胸がきゅーーんっとなりました.Go かわいいよ Go.

Go はなぜ成功したんでしょうか? コンパイル速度,実行速度,デプロイの容易さ,充実したツール,質の高いライブラリ,interfaceやconcurrencyをサポートする言語機能… そのどれもが大事で,僕も実際どれも好きですが,Rob Pike は “Simplicity” が答えだといいます. そしてこの “Simplicity” は “Complicated” だとも.

シンプルという言葉はともすれば薄っぺらくなります.特にVimmerという職業(?)柄, Vim plugin とかをいくつかみると,”simple” だとか,他にも “easy”, “minimalictic” と言った単語が並んでたりします.これは自戒を込めてですが,場合によってはちょっと薄っぺらいなぁと感じるようなものもあったりします.

Simplicity is the art of hiding complexity.

Go が simple だとか,less is more だとか,引き算の言語だ…といった表現をされるとき, 僕は全く薄っぺらいだとかは思いません. これは”Simplicity” というのは実は”Complicated” であり, “Simplicity” の背後には”complexity”を洗練された,緻密なデザインや設計,実装が隠れているからなんだと思います.

GCやgoroutine,interfaceといった実際の例がスライドで説明されているので是非見てみてください.

僕らが享受しているGo の “Simplicity” は簡単に実現されたものではありません. 僕らが Go に対して,「あぁ,こういうのでいいんだよ.こういうので.」と感じるとき, その背後には Go がたくさんの機能を削ぎ落として,直交する必要十分な機能のデザインと 複雑な実装が存在しています.

Simplicity is hard—to design.
Simplicity is complicated—to build.
But if you get it right…
Simplicity is easy—to use.
The success of Go proves it.
https://talks.golang.org/2015/simplicity-is-complicated.slide#30

“Simplicity” をデザインするのは難しく,”Simplicity”の実現はとても複雑です. でもこれらをうまくやると,”Simplicity” は使うのが簡単になります.

これが Go は, “simple” であり,”less is more” であり,”引き算の言語” であり, 「あぁ,こういうのでいいんだよ.こういうので.」ということなのでしょう.

Go is more Pythonic than Python

Go and the Zen of Python

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
>>> import this
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

僕が最初に学んだ言語は Python で,The Zen of Python の思想はとても好きでした.

  • Explicit is better than implicit.
  • Simple is better than complex.
  • Readability counts.
  • There should be one– and preferably only one –obvious way to do it.

Go は The Zen of Python に,ともすれば Python 以上にマッチしています. Go が Pythonic なところも間違いなく僕が Go を好きになった一因です. なんなら,Go が Python より Pythonic なので,もう Python で書くようなところは, Go で書きたい.

Go の前に学んだ言語は Scala でした. Scala が魅せてくれるプログラミング言語の可能性にはスゴイなぁ,おもしろいなぁ, と思いつつも,Scala が気になる子止まりだったのは,The Zen of Python の思想の真逆を 行くような,ないし真逆のコードが生まれやすい言語だからだと思います. Scala は今も書いているし,堅牢かつ規模の大きいコードも Scala だと比較的安心して書いていけたり, 僕に “Functional Programming” などを教えてくれた Scala は好きですが,恋に落ちるほどではなかった. 僕を含め,もう少し人類が進化するとまた変わってくる気もする.

Go とのこれから

2017年もGoと仲良くやってきたいです.個人的には特に Vim との連携とか含め. Go 本体になんかコントリビュッションしたいと思いつつできなかったので,機会があればやっていきたい.

あとは,Go のよさを再確認するため(?)に別のパラダイムの言語(rust とか)にちょい浮気しつつやっていきたいですね.

Vim との 2016年 - 本妻は Vim

2016年のVim活もザックリ振り返ります.

EasyMotion 3.0

Vimのカーソル移動はもっともっと爆速になる! Vim-EasyMotion v3.0 をリリースしました - haya14busa

結構前にやった気もしますが,今年でした. バージョン3.0の機能追加でウィンドウをまたいだEasyMotionによるカーソル移動が実現しました. 完全にライフチェンジング機能だし,待望の機能の実現で自分の Vim script 力の高まりも確認できてよかった.

vital のロード高速化

まず,revital.vim という別プラグインで vital.vim のロードを高速化できることを示した後,結局 vital.vim 本体に入れてもらいました. ついでにリファクタリングしたりインターフェースの改善も行いました(後方互換性は維持している). 特定の環境でvital.vimが遅いという問題を完全に解決し,現在では autoload 関数を呼ぶのと変わらなくなってます.

はじめてのパッチ - Vim の Contributer に!

vital のロード高速化の際に Vim のバグを踏んで,小さいですがはじめてパッチを書いて取り込まれました! このバグは本体のバグだったということもあって異常に原因究明が難しく,原因を見つけたのは lambdalisue さんでした.

今年は他にも EasyMotion で踏んだバグの修正パッチや,Vim8.0 リリース前の channelやjob 機能を叩いたパッチを書いたり, Vim 8.0 に lambda いれようぜ! ともう一回 vim-jp でちょっと騒いで,あとは vim-jp の mattn さんや k-takata さんに丸投げするなどをしてました.

そのあたりの話で1つ記事を書いてます -> Vim 8.0 released and Now, I’m a contributor of Vim !!! – Medium

今年になって小さいとは言え Vimのパッチ書いたりできたのはとても嬉しかったです. Vim のソースコードも読むことは出来るし,デバッグの仕方もわかってきたし, 場合によってはちょっと修正するくらいのことも出来るということがわかったのは収穫でした. ただ,もっと C 力は高めたほうがよさそう.

Vim 8.0 & Go関連

  • go-vimlparser
  • vim-go-client
  • vim-stacktrace
  • パッチいくつか.

詳細は Go のところで紹介したので省略.

VimConf

発表は go-vimlparser についての話ですでに書きましたが, t9md さんの t9md/atom-vim-mode-plus の発表が面白くて, Atom の vim-mode-plus のアイデアを Vim にバックポートする業などをしていました.

haya14busa/vim-metarepeat は vim-mode-plus の occurence で出来るようになる便利な機能を別の角度から実装したもので, ドットリピートをオペレータとして,テキストオブジェクト内の対象に対して一括でドットリピートを実行するものです. 記事には書いてないけどissueにちょっと考えとかを書いています -> https://github.com/vim-jp/issues/issues/977#issuecomment-259703728 vim-metarepeat はかなり便利で常用していて,記事書くかぁと思いながら今年が終了しました.

haya14busa/vim-textobj-function-syntax は関数text-objectをVimのsyntaxを使って言語ごとに用意せずに使えるようにしたものです. syntax によって使える/使えない言語がありますが,ちょっと便利. これも記事にはしてないけどissueにちょっとコメントしてます.-> https://github.com/vim-jp/issues/issues/987#issuecomment-262870187

Google Translate の衝撃と Vim

Google Japan Blog: Google 翻訳が進化しました。

今年はGoogle Translate が日英間の翻訳に Neural Machine Translation を導入して, 機械翻訳がとても流暢になりました.Google Translate さんすごい. この改善をみて,Vimから使いやすくするためにいくつかプラグインを作りました.

英語でブログ記事を書く

Google Translate の流れでいうと今年後半からは実験的にブログ記事も英語で書くようにしました. Google Translate 改善前からやっていて,改善後も勿論(?)Google Translateはあくまで補助としての使用ですが.

書いた英語 Posts

英語で書く記事かどうか決めるというよりは,基本的に英語で書いて,どうしても日本語で書いたほうがよさそうだったら日本語で書くことにしています. 直近の2つのアドベントカレンダーは普通に日本語で書いてしまって守れてないけど…正直時間が取れなかったので日本語に逃げました… まぁ日本語で発信するのもそれはそれで大事だと思うのでいいかなぁーとは思ってます.

英語で書く理由としては,今後使うし勉強のためやらなきゃ…という理由と, 発信を日本に閉じる必要性はないかなーと最近は考えてるからでした. 僕はVimとかGoについて書くことが最近は多いですが,それらの記事のターゲットは大抵 Vimmer か Gopher で, 別に想定読者を日本人に絞る必要性はないんですよね.

日本語で書いた後,英訳しようとはなかなか行動できないので,英語・日本語両方ポスト作ると言うよりは, 実質デファクトになってる英語で書けば英語読める日本人は読めるので,基本的には 英語で1つ記事を書くということにしました.

…とはいえ時間やクオリティーの兼ね合い,日本のコミュニティーの活性化, フィードバックのもらいやすさ,そして何より現状の英語力が足りてない問題など, 色々と問題はありますが.そもそもたくさんの人に読んでもらうというのが目的ではないという観点もありそうだけど.

英語で書くと特に日本では伸びづらいですが, 日本で伸びなくても今までリーチしなかった層にリーチするし, 今年書いた記事はだいたいある程度は日本人もそれ以外も読んでもらえたっぽいので, 今の所やってみてよかったです. reddit とかにあげるとそこそこ読みにきてくれて便利.(redditのCEOがコメント編集したという事件があって,若干使いたさが減りましたが…). 来年も続けていこうかなぁと思います.

英語に関してはWritingもそうだけど,どちらかといえば勉強する必要があったのはSpeaking/Listeningだった気が… という説もあり,今後がとても心配すぎる…

2016年活動情報

GitHub Contributions

GitHub Contributions (including private contributions)

GitHub Contributions (including private contributions)

GitHub Contributions (public)

GitHub Contributions (public)

Posts

Posts (English)

歩くのが下手って気付いた2016年

今年は1つ大きく躓いた出来事がありました.躓いただけなら立て直せばよかったはずだったけど, そのまま歩き続ける意味を見失い,歩みを進めようとする心はもう折れていて,しばらくその場でうずくまってしまいました. 躓いたことにも,歩き直せなかったことにも,道を変えて歩くことさえもできずに,ただただうずくまってました. 今まで通りとはいかないにしても,もうちょっとうまく歩けたはずでした. あぁ,僕は歩くのがなんて下手なんだ…

今の所,詳細はオープンインターネットには書かなくてもいいかな…と思ってます. 書く気がないのにここでふんわり書いたのは完全に自己満足です.忘年です.忘れないけど.

あとは完全にここでやるべきことではないけど,一種の懺悔みたいなものです. 僕が単に1人でうずくまったまま世界から消えても,別に世界はどうってことないけど, 無駄に不要なところにまで迷惑をおかけしてしまったり,いくつか見えた差し伸べられた手も 完全にシャットアウトしてしまったことは,単純に反省しています.ごめんなさい.

最近は人間的な生活をおくるリハビリをしていて,心に平穏は戻りつつある気はします.

最後に

2016年の振り返り記事だって言ってるのに無駄なポエムがところどころにあるせいで無駄に長くなった気がします. あとオープンにやってきたこと何でもかんでも放り込み過ぎて長くなったので,もうちょっとハイライトだけ振り返ればよかった気もする. まぁ,まだまだとはいえ,2016年結構いろいろやったのでは? と振り返って思いました. 来年の目標,なにか定量的なものを設定して振り返るのが本当は良い気がしますが, 来年はとりあえず強く生きることを目標にします.

来年がとても素晴らしい1年でありますように.

Go の CI で Lint と カバレッジ回して非人間的なレビューは自動化しよう in 2016年

この記事は Go (その3) Advent Calendar 2016 の24日目の記事です(代打). メリークリスマス!

本記事では Go 言語プロジェクトの CI で回すと便利な各種lintの紹介やカバレッジ計測の方法などなどについて紹介します.

Go 言語おすすめ linter

Go の lint 一覧といえば gometalinter じゃん? みたいな話もあると思うのですが,CIで回すには個別に linter を明示的に回すほうが良いかと思います. ということで 僕が普段使ってるオススメ linter の紹介です.

go vet

Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string. Vet uses heuristics that do not guarantee all reports are genuine problems, but it can find errors not caught by the compilers.

Go 言語標準でついているlintツールで,コンパイラが検出しないエラーを検出できます. false positive な結果も無いようにデザインされているのでエラーがリポートされた場合は安心してCIをfailにできます. go vet に検出されたエラーはほぼ100%直したほうがよいでしょう.

golint

Golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes. Golint differs from govet. Govet is concerned with correctness, whereas golint is concerned with coding style.

gofmtがコードのリフォーマットを,govetがコードの正確性をチェックするのに対して,golint は Go のソースコードの “コーディングスタイル” の問題を報告します.これはエラーというよりも “suggestion” に近く, 基本的に従っていたほうがGoの慣習に沿った確実によいコードになるのですが, まれに,いやここの関数はコメント書かなくても絶対いいよね…とかいうケースもあり 若干消耗することもあるので星4です.

なお,結局はコーディングスタイルの問題をリポートするという思想からデフォルトでは問題があっても exit code は 0 になるので,落としたい場合は -set_exit_status flag を付けましょう.

errcheck

errcheck は関数のエラーの返り値をハンドリングチェックしているかどうかを静的に解析してくれるツールです. エラーを捨てた状態の場合思わぬ動作(nilになってたりだとか)するので,これをチェックしてくれるのは大変嬉しいです.

ただし,少し実際直さなくてもいいという意味でのfalse positive が多くCIで動かすには工夫が必要で星3です.エラーをチェックしてくれるという便利度は満点. なお,最近は標準ライブラリで必ずエラーがnilとして返ってくるような関数についてはリポートしないという false positive を減らす工夫もされているようです.

staticcheck

staticcheck は一言で言うとサードパーティーの go vetです.go vet でチェックされていないようなことを静的に解析してくれます. (例えば正規表現がvalidかチェックしてくれるかなどなど…たくさん項目があるのでREADMEを見てみてください.)

作者の dominikh さん は Go 言語の開発にも関わっていて,dominikh さん製Go lint ツールは個人的には 信頼できる印象です.github/go の issue でも go vet のissueなどに, staticcheck は実際こういうのチェックしてるけどと言った感じで参照されてたりするのを見かけます.

そして次の2つのツールも dominikh さん製です.

gosimple

gosimple は一言で言うとdominikh さん製のサードパーティー gofmt -s です.もっとコードをシンプルに出来るところを報告してくれます. (ただし執筆時現在,自動で修正してくれるオプションとかはない)

例えば if err != nil { return err }; return nil といった構造のコードがあれば return err で十分だよ? と報告してくれます.

報告に false positive もない印象で,あーそんなメソッドあったのか〜ということに気付いたりできてオススメです.

go-unused

使ってない identifier をチェックしてくれます. Go のコンパイラ自体が使ってない変数があるとコンパイル通らないという話もありますが, unused は グローバル変数の var や const, struct の field,export されていない関数などで 使われていないものを報告してくれます.

報告してくれないのは使われてない関数の引数くらいでしょうか? これはインターフェースを満たすための関数など, 引数もらうけど使わないんやというケースなどを考えて報告されていないのではないかという気がします.[要出典]

また個人的には使えてないので強くオススメできませんが,パッケージのリストを渡して exported なもので,渡したパッケージ内で使われていないものをチェック出来る機能もあります. https://github.com/dominikh/go-unused#whole-program-analysis “internal” packages などを使っていたりする場合は便利かもしれないですね.

unused は個人的には大変便利に使っていて,ごくまれにデバック用のexportしてない関数を報告されて, あー…ってなる以外に false positive な結果もなく便利に使っています.

この前 interface を満たすためのダミーの関数をいろんな struct に定義してたんですが, その際,追加すべきでない struct にも追加してしまい,それを unused が報告 してくれたことがありました.インターフェースを満たすかどうかといった観点での”used”もちゃんと見てくれていて大変良い子だな〜と思い感動しました. オススメです.

gofmt -s

linter ではない若干番外編その1.

gofmt -s の結果 diff があるかどうか,つまりもっとシンプルにかけた部分がないかということをチェックできます.

コマンド: (! gofmt -s -d . | grep '^')

括弧は travis などのyamlでvalidなものにするために使っています. gofmt も exit code が 1 になったりすることはないので個人的には grep '^' の結果を否定することによって,diffを表示しつつ,diffがあればfailにするという手法を使っています. (xargs -r とかはMacでは-rがないとかがある…)

基本的にGo言語開発時はみんな gofmt かけていると思いますが,-s は付けていなかったり, goimportgofmt を代用していた場合に -s オプションがなくてかけてなかったりするので CI で実行するとちょっと便利です.

ちょっと便利なんですが,-s つけるとめっちゃシンプルになって絶対いいよね…! というよりは, あー手元で実行してなくて,CIサーバでdiffでちゃったよ…直すか…とちょ〜っとだけ消耗することが あるという意味で個人的にはオススメ度星4です.(goimport -sフラグ足してくれ〜)

go test -race

  • おすすめ度: ★★★★★

linter ではない若干番外編その2. go test-race を付けるとrace condition があるかチェックしてくれます. 特に goroutine とか使ってるコードであれば,CIでのテスト実行時にはぜひ付けておくべきでしょう. 僕も何度も-raceに怒られてコード直したり,ああここlockいるなと気付かされております.

その他

あんまり僕がまだ使えてなくて,ちゃんとオススメできないけど便利な静的チェックツールはいくつかあります.

interfacer

  • https://github.com/mvdan/interfacer
  • 引数の型とかインターフェースでいいじゃん?というところを指摘してくれる. e.g. *os.File -> io.Reader
  • 個人的にはインターフェースにするといいところは最初からインターフェース使ってたりするしそこまでの恩恵は感じない

gosum

  • https://github.com/haya14busa/gosum
  • 急に拙作ツールの宣伝(?)
  • Go のインターフェースで直和型っぽいことを表現したときの,type switch に抜け漏れがないかを静的にチェックするツールです.
  • Scala でいう sealed trait のパターンマッチでコンパイラが抜け漏れがあると warning だしてくれるというやつのGoバージョン
  • 詳しくはこの記事に書きました -> Sum/Union/Variant Type in Go and Static Check Tool of switch-case handling – Medium
  • 書いてみて,使ってみて実際便利だと思ってるんですが,まだちゃんとCIで回したりはしてないので,その他枠で雑に紹介です.

ところで linter 書くときって,AST ベースで解析するツールが僕の観測範囲では多いと思うですが, Goは必要であれば go/types パッケージを使って型情報まで使って 解析できるので大変便利ですね…! 標準ライブラリでカバーされてるところも +1

“go/ast ではしゃいでるのはもう古い! 時代は go/types !” みたいな煽りタイトルの解説記事を最近は待ち望んでます. go/types 関連は標準ライブラリの中でもかなり大きいものなのでなかなか僕もまだ全貌を理解できてないです.

その他のその他

gometalinter とか Go Report Card で紹介されているツール.

https://github.com/fzipp/gocyclo とかイマイチ恩恵を受けたことがないんですが,gometalinter とか Go Report Card で使われているツールは参考になるかもしれません.

reviewdog: linter の false positive と闘う

特に golint や errcheck など,上記でオススメしたlinterの中には,false positive なリポートが結構あって,めっちゃ便利でチェックしたいんだけど CI で fail にしづらい… というものがいくつかあります.

そこで拙作ツール reviewdog の紹介です.(宣伝)

もちろん Go 製です!

(画像は実際のPull Requestのコメントへのリンクになってます)

reviewdog は Go 言語の linter に限らず,任意のコマンドの結果を’errorformat’ という形式を使うことでパースして,diff で新たに追加された部分にたいする問題だけを 表示したり,GitHub にコメントすることが出来るツールです. 詳しくは -> reviewdog を飼ってコードレビューや開発を改善しませんか - haya14busa

結果をdiffでフィルターすれば,それらの問題についてのみPull Requestのレビュー時やコミット時にチェックすることが出来るので, もし false positive な結果な場合は単に無視すれば次回以降に同じ問題は報告されません.

もちろん,例えば先に紹介した unused などは全然diffと関係ない部分で新たに問題がおきたりすることもあり, 結果を diff でフィルターするというのはfalse positiveと闘う銀の弾丸ではないのですが, 多くの場合これで十分機能を果たすでしょう. (一応 diff 外の問題もうまくまとめて報告する仕組みも足したいな〜という気持ちはあります)

Go 製ツールである reviewdog は自分自身のコードでドックフーディングしていて,この記事で紹介した いろんなGo 用 linter を実行していて,上記の画像のように実際に便利に使っています. 参考: reviewdog/reviewdog.yml (最近はyamlでも管理できるようにしていて,より簡単にローカルで実行したり,git hook で実行しやすくしたりしたいなどと改善しています)

Goのテストカバレッジをレビューでも活用する

Go は標準で go test -coverprofile=coverage.out . などと実行するとテストカバレッジを取得することができます. ただ実は CI などでカバレッジを取得する際は注意点があり,複数のパッケージをまとめてカバレッジを計測することはできません. つまり例えば go test -coverprofile=coverage.out ./... とはできません.これは go test 内部ではパッケージごとにテスト実行用バイナリを作成してそれぞれ実行してるという設計になっていることに起因します. issue は上がってますが標準では対応するのはなかなか骨が折れそうです. (ちょっと修正してコントリビューションしてみようかと格闘しましたが構造的に地味に大変そうでした…)

そこで現状で,複数パッケージのテストカバレッジに対応するために色んな所で Makefile やGoのツールを使うといった解決方法が紹介されています. しかし!紹介されていて,確かにある程度はどれも動くのですが,実は多くのスクリプトはちょっと片手落ちなものになっています. 例えば go test の -coverpkg 引数を使わないと依存先のコードカバレッジが取得できてなかったり, 結果の coverage.out に重複行が生まれるケースがあったりします.

解決策

mattn さんの mattn/goveralls では上記の問題に対応したマルチパッケージ対応テストカバレッジ機能が備わっています.というか僕がPull Request しました. Coveralls に投稿する場合は goveralls を使うと良いと思います. (goveralls -service=travis-ci でよしなにやってくれる)

ローカルでは?

goveralls にコントリビューションしたあと,あれ…これローカルでもやりたいじゃん…ということに気付き, 既存のMakefileソリューションや似たツールはいくつかあるにはあったんですが,上記の問題などの対応とか いろいろ面倒だったのでマルチパッケージカバレッジ作成用go test のラッパーツールを作りました.

haya14busa/goverage: go test -coverprofile for multiple packages

1
$ goverage -coverprofile=coverage.out ./...

とかするといい感じに coverage.out が生成されます.便利.

codecov に投稿してレビューでも活用する - “おい、coverall もいいけど codecov 使えよ”

サブタイは b4b4r07 さんリスペクトです. ref: おい、peco もいいけど fzf 使えよ - Qiita

Codecov という Coveralls と似たサービスがあるのはご存知でしょうか? 僕は以前からたまに見かけたことはあったのですが,最近はじめて使ってみて,断然 coverall よりいいじゃん…!!! と感じました.

全体的に洗練されてる…というよさもあるんですが,一番いいところは Pull Request の diff に対するカバレッジを表示できて, ブラウザの拡張をインストールすれば GitHub の Pull Request 画面上でカバーされた行をオーバーレイで確認できるところが大変気に入りました.

もちろん Codecov 上のページでも見れます: Compare ⋅ haya14busa/reviewdog

Go 言語リポジトリのカバレッジを travis で計測して codecov へ投稿する例:

1
2
3
4
5
6
7
8
9
10
# .travis.yml
install:
  - go get github.com/haya14busa/goverage

script:
  - goverage -coverprofile=coverage.txt ./...

after_success:
  - goveralls -service=travis-ci -coverprofile=coverage.txt
  - bash <(curl -s https://codecov.io/bash)

codecov 公式のGo言語用リポジトリの例 では Makefile を使ってますが, 完全に上記のマルチパッケージサポートの問題を踏んでるので goverage を使うと良いと思います.

テストカバレッジは別に100%を目指さなくてもよいと思っていて,全体のカバレッジが何%以下とか何%下がったらステータスをfail にするといった機能がcoverallやcodecov にはありますが,この辺を有効にすると結構消耗するかと思います.

ただ,カバレッジを参考にすることは有用だし,ユニットテストは基本的に書くべきです. Pull Request などではカバーすべきところをしっかりカバーしたテストを期待したいし,, レビュー時にもそのあたりが可視化されると大変便利です.

実際僕は Go のレビューするときに手元でテストまわしてcoverageみて,ここテストそもそも無いから足して欲しいだとか, この行はカバーされてないけど,カバーすべき部分なのでテストケース足して欲しいとか言ったりするんですが, codecov を使うとこのフローがやりやすいし,レビュイーもPull Request を出した時点で自分で気付いて テスト足したりできると思います.

実は codecov 使い始めたのは最近で,僕自身がチーム開発として使った経験はまだないのですが, coverall よりも codecov 使うとこの辺いい感じに可視化されて人間が指摘しなくてもよくなったり, レビュワーとして指摘しやすくなったりすると思います.

せっかく Go という言語は標準でテストが書きやすく,カバレッジ計測もしやすいので,ぜひ皆さんもカバレッジを計測して,codecov 使ってみてはいかがでしょうか?

まとめ

Go のCIでまわすと便利なlintツールを紹介したり,テストカバレッジの取得方や codecov のオススメなどをしました. Go は go/ast, go/types など go/ 下の標準パッケージを使ってコードをパースしてASTを取得したり,型情報を取得したりなど するライブラリが用意されているので,必要に応じて自前でチェックツールを作ったりもしやすく面白いです.

この記事で紹介したものは,一般的に多く使われているみなさんが知っているようなものから,あまり知られてないものまであるかと思いますが, すべて少なくとも僕が使っていて便利だなぁ〜,と思ったものを紹介してみました. 他に便利なオススメツールなどがあれば教えてください!

個人OSSプロジェクトではレビュワー最初はいない問題などがあり,最近はPull Requestを開いてlint チェックさせたり,カバレッジみたり, 改めてブラウザ画面上でセルフレビューするなどしていて,まぁちょっと面倒もあるけど良い感じです. あとは設計レビューとかもしてくれるGoのツールがあればカンペキですね〜〜〜!!!1

もちろんチーム開発でのレビューでも,消耗しがちな非人間的な指摘は機械にやってもらって, もっと大事な観点をレビューするために,この記事で各種紹介した手法は役に立つかなと思います.

来年も Go 書いていくぞ!

Go で Vim プラグインを書く

この記事は Vim アドベントカレンダー 2016 の21日目の記事です.

最近は Go 言語が大好きすぎて,Vim plugin も Go で書きたい!!! という欲が出てきたので, Vim plugin を Go で書く方法について紹介します.

Go で Vim plugin を書くとは?

一口に Go で Vim plugin を書くといっても

  1. Go で書いたバイナリがメインで Vim script の autoload 関数などから呼ぶ.例: https://github.com/mattn/vim-filewatcher
  2. Go 側からも Vim script を呼ぶ,つまり Vim script で Vim の情報を取得するところなど含めて,ほぼ全部 Go で書く.

という 2 段階があると思います.本記事では2の方法も含めて紹介しますがまずは1から行きましょう.

1. Go で書いたバイナリをつかった Vim plugin の作り方

これは先程例にあげた https://github.com/mattn/vim-filewatcher がシンプルでわかりやすいです.

filewatcher/filewatcher.go で書いた Go をインストール時に cd filewatcher && go get -d && go build でビルドし, autoload/filewatcher.vim でこのバイナリを job をつかって呼んでいます.

go get -d を呼ぶことで依存するパッケージをダウンロードし,go build することで $GOBIN などを汚さずにプラグインディレクトリにバイナリを配置できます.

autoload/filewatcher.vim

1
2
3
4
5
6
7
8
let s:cmd = expand('<sfile>:h:h:gs!\\!/!') . '/filewatcher/filewatcher' . (has('win32') ? '.exe' : '')
if !filereadable(s:cmd)
  finish
endif

function! filewatcher#watch(dir, cb)
  return {'dir': a:dir, 'job': job_start([s:cmd, a:dir], { 'out_cb': a:cb, 'out_mode': 'nl' })}
endfunction

バイナリを呼んでいるVim script もとてもシンプルで, windows かどうか見ながらバイナリのパスを取得し, それを job で呼ぶだけです.簡単.プラグインの性質によっては job ではなく system() などを使ってもよいでしょう.

また,開発時には g:plugin_name#debug などを作ってそれを見て go run を呼ぶというふうに変えることもできます.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function! s:separator() abort
  return fnamemodify('.', ':p')[-1 :]
endfunction

let s:is_windows = has('win16') || has('win32') || has('win64') || has('win95')

let s:base = expand('<sfile>:p:h:h')
let s:basecmd = s:base . s:separator() . fnamemodify(s:base, ':t')
let s:cmd = s:basecmd . (s:is_windows ? '.exe' : '')

if g:plugin_name#debug
  let s:cmd = ['go', 'run', s:basecmd . '.go']
elseif !filereadable(s:cmd)
  call system(printf('cd %s && go get -d && go build', s:base))
endif

僕が作ったプラグインから引っ張ってきた例で autoload/filewatcher.vim ほどシンプルではないですが,もうちょっとなんとか出来るかもしれないですね. main パッケージのファイル (s:basecmd . '.go') を1ファイルにするとgo runで呼びやすいです.

2. Go 側からも Vim script を呼ぶ必要があるようなプラグインの作り方

mattn/filewatcher ではファイルの変更を検知してstdout にJSONを吐いて,それが job の callback に渡されるという形式で単体で簡潔してましたが, 場合によっては Go 側から Vim の状態を取得したり,Vim script を呼んだりしたい場合もあります. そういうプラグインを作るには,job を JSON モードで起動し, :h channel-commands を使うことによって実現できます.

:h channel-commands

1
2
3
4
5
6
7
8
9
10
11
JSON チャンネルを使用すると、サーバープロセス側はVimへコマンドを送信できます。
そのコマンドはチャンネルのハンドラーを介さずに、Vimの内部で実行されます。

実行可能なコマンドは以下のとおりです:           *E903* *E904* *E905*
    ["redraw", {forced}]
    ["ex",     {Ex コマンド}]
    ["normal", {ノーマルモードコマンド}]
    ["eval",   {式}, {数値}]
    ["expr",   {式}]
    ["call",   {func name}, {argument list}, {number}]
    ["call",   {func name}, {argument list}]

{数値}({number}) は id で,job -> Vim に渡すさいはマイナスを指定する必要があり, その渡した id と共に評価された値が返ってきます.

例えば Go 側で stdout に ["expr","line('$')", -2] を書き込むと, Vim がline('$') を評価してその結果が stdin に [-2, "last line"] といった結果が返ってきます.

便利すぎる…

ということでidの取扱などこのあたりの処理を毎回丁寧にやるのは面倒くさいので, https://github.com/haya14busa/vim-go-client というラッパーを作りました. ドキュメント: https://godoc.org/github.com/haya14busa/vim-go-client#Client

type Client が上記の channel-commands などのに相当するメソッドを持っており, type Handler がメッセージの受け渡しを担当します.

サンプル: _example/dev/job/job.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package main

import (
  "fmt"
  "log"
  "os"
  "time"

  vim "github.com/haya14busa/vim-go-client"
)

type myHandler struct{}

func (h *myHandler) Serve(cli *vim.Client, msg *vim.Message) {
  log.Printf("receive: %#v", msg)
  if msg.MsgID > 0 {

      if msg.Body == "hi" {
          cli.Send(&vim.Message{
              MsgID: msg.MsgID,
              Body:  "hi how are you?",
          })
      } else {
          start := time.Now()
          log.Println(cli.Expr("eval(join(range(10), '+'))"))
          log.Printf("cli.Expr: finished in %v", time.Now().Sub(start))
      }

  }
}

func main() {
  handler := &myHandler{}
  cli := vim.NewClient(vim.NewReadWriter(os.Stdin, os.Stdout), handler)
  done := make(chan error, 1)
  go func() {
      done <- cli.Start()
  }()

  cli.Ex("echom 'hi'")
  log.Println(cli.Expr("1+1"))

  select {
  case err := <-done:
      fmt.Printf("exit with error: %v\n", err)
      fmt.Println("bye;)")
  }
}

handler := &myHandler{} でハンドラを作って cli := vim.NewClient(vim.NewReadWriter(os.Stdin, os.Stdout), handler) で stdin/stdout を介してVim と通信できるclientを作成しています. あとはこいつを cli.Start() しておけば Vim から ch_sendexpr() などが呼ばれると handler に中身が渡されるし, cli.Ex("echom 'hi'") などを呼ぶと Vim 側で echom 'hi' が実行されます.

実例: vim-stacktrace

実際に vim-go-client を使ってひとつプラグインを書いてみました.

haya14busa/vim-stacktrace

stacktracefromhist.gif (1287×800)

Vim のスタックトレースをquickfix に流し込むプラグインでやっていることとしては6日目の記事の Vim scriptのエラーメッセージをパースしてquickfixに表示する - Qiita と近いです.

autoload 関数からjobに ch_evalexpr する部分([link]https://github.com/haya14busa/vim-stacktrace/blob/933f9d10c7ef99467c27609fcdd80be37c0712e8/autoload/stacktrace.vim#L12-L30()) を除いてほぼ全てがGoで実装されていて,現時点で Go の割合が 87.8 % です.

image

実装の中身としても,Vim のスタックトレースからは関数内における行番号しかとれず,ファイルの行番号が取得できない問題があるのですが, それをGoで実装したVim script parser (https://github.com/haya14busa/go-vimlparser) を使ってファイルをパースし,行番号を取得することができています. また,:CStacktraceFromhist などは Vim script の inputlist をGo側から呼んでいてインテラクティブにVimと協調して動作できることも示せました.

Go で書くよさ

実際に Vim script でやっているひともいたように,vim-stacktrace は Go が無いとかけなかったといった類のものではないですが,Goで書くといいことがたくさんありました.

  • 型がある安心感
  • テストが標準に備わっていて書きやすい (go test)
  • カバレッジも取れる! (go test -coverprofile)
  • Go のパッケージが使える (go-vimlparser, etc…)
  • etc…

カバレッジなどは現在Vim scriptのテスティングフレームワークではサポートされていないし,なかなか実装しようとしてもムズカシそうなのですが, Goでかけば標準でついてきます.とても便利.

coverall も使えます: Coverage Status

逆にPure Vim script と比較して悪いところや注意点があるとすれば

  • vim-go-client がまだ安定してない
  • channel-commands がエラーをちゃんと返してくれない(エラーがあれば “ERROR” とだけ返ってくる)
  • チャンネルの通信で少しだけオーバーヘッドがある
  • 現状vim/neovimに両対応できない

といった感じでしょうか.もうちょっとvim-go-client精錬させたいですね…頑張ります…

NeoVim のリモートプラグイン

neovim 向けには実は neovim/go-client というものが存在し,リモートプラグインをGoで書くことが出来るようです.

Vimconf 2016zchee さんが発表していた nvim-go はこれが使われています.

スライド該当部分: http://go-talks.appspot.com/github.com/zchee/talks/vimconf2016.slide#33

正直なところ neovim のリモートプラグインの先行アドバンテージ(?)は大きく,vim-go-client と比較してかなり高機能になってます. 理想としては Vim 8 でも neovim でも使えるものをかけるようにしたいのですが, neovimのリモートプラグインが高機能であることや, msgpack 依存であることからなかなか両方に対応することはムズカシイです…

うまく抽象化してロジックの部分だけ共通化して,vim8用/neovim用にメッセージのハンドラを管理してうんたん…みたいなことは出来るかも知れないので, 今後の研究課題という感じですね.あと僕がほとんどneovim使わないので nvim-go の仕様感とか知っている方はお話してくれると嬉しいです. (Vimconf で zchee さんとその話ができたのは便利だった…)

おわりに

正直まだまだGoで書かれたVim plugin は少なく発展途上ですが,実用的なプラグインを作成することもできたので,可能性を感じます. Go でかけばマルチプラットフォームに対応できるし,ライブラリがどうとか環境がどうとか気にすることなく動かせるので,Vim との親和性はかなり高いと思っています.

何よりGoはかわいい!書いていて楽しい!

まだまだ発展途上ですが,ぜひ皆さんもGoでVim プラグインを作ってみてください.

Golangにおけるinterfaceをつかったテストで Mock を書く技法

いい記事に感化されて僕も何か書きたくなった。

Golangにおけるinterfaceをつかったテスト技法 | SOTA

リスペクト:

今週のやつではなく先週のです.今週のは特に知見がなかった…grpc-goとか使えたらクライアント勝手に生成されるしいいよねgrpc流行ると便利そう(感想) くらい

Golangにおけるinterfaceをつかったテスト技法 | SOTA めっちゃいいなーと思ったんですが,テスト用 の mock を気軽に作るテクニックはあまり詳しく紹介されてなかったのでそのあたりの1つのテクニックを書きたい.

前提

僕もテストフレームワークや外部ツールは全く使わない.標準のtestingパッケージのみを使う. testify もいらないし, mock するために gomock も基本はいらない.

とにかくGolangだけで書くのが気持ちがいい,に尽きる.

テスト用 fake client をつくる

全体の動くはずのgist: https://gist.github.com/haya14busa/27a12284ad74477a6fd6ed66d0d153ee

例えばこういう実装のテストを書くときのことを考えます.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import (
  "context"
  "fmt"
)

type GitHub interface {
  CreateRelease(ctx context.Context, opt *Option) (string, error)
  GetRelease(ctx context.Context, tag string) (string, error)
  DeleteRelease(ctx context.Context, releaseID int) error
}

type GhRelease struct {
  c GitHub
}

func (ghr *GhRelease) CreateNewRelease(ctx context.Context) (*Release, error) {
  tag, err := ghr.c.CreateRelease(ctx, nil)
  if err != nil {
      return nil, fmt.Errorf("failed to create release: %v", err)
  }

  // check created release
  if _, err := ghr.c.GetRelease(ctx, tag); err != nil {
      return nil, fmt.Errorf("failed to get created release: %v", err)
  }

  // ...
  return &Release{}, nil
}

type Option struct{}
type Release struct{}

GitHub interface をテストでは mock したものを使いたい.そういうときには以下のように mock を作ると便利です.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type fakeGitHub struct {
  // インターフェース埋め込み
  GitHub
  FakeCreateRelease func(ctx context.Context, opt *Option) (string, error)
  FakeGetRelease    func(ctx context.Context, tag string) (string, error)
  // 埋め込みを使うので,例えば DeleteRelease はまだテストしないので mock
  // しない... いうことができる.
}

func (c *fakeGitHub) CreateRelease(ctx context.Context, opt *Option) (string, error) {
  return c.FakeCreateRelease(ctx, opt)
}

func (c *fakeGitHub) GetRelease(ctx context.Context, tag string) (string, error) {
  return c.FakeGetRelease(ctx, tag)
}

fakeGitHub という struct を作成し,インターフェースをとにかく満たすために GitHub interface を埋め込みます.

そして mock したいメソッドは新たに func (c *fakeGitHub) CreateRelease(...) (...) と 定義しなおし,実装の中身は fakeGitHub に持たせた FakeCreateRelease field に丸投げします.

このようにしてテスト用 mock を作るとそれぞれのテストで簡単に中身の実装を変えられるので大変便利です.

実際にテストしてみる例

main_test.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package main

import (
  "context"
  "fmt"
  "testing"
)

type fakeGitHub struct {
  // インターフェース埋め込み
  GitHub
  FakeCreateRelease func(ctx context.Context, opt *Option) (string, error)
  FakeGetRelease    func(ctx context.Context, tag string) (string, error)
  // 埋め込みを使うので,例えば DeleteRelease はまだテストしないので mock
  // しない... いうことができる.
}

func (c *fakeGitHub) CreateRelease(ctx context.Context, opt *Option) (string, error) {
  return c.FakeCreateRelease(ctx, opt)
}

func (c *fakeGitHub) GetRelease(ctx context.Context, tag string) (string, error) {
  return c.FakeGetRelease(ctx, tag)
}

func TestGhRelease_CreateNewRelease(t *testing.T) {
  fakeclient := &fakeGitHub{
      FakeCreateRelease: func(ctx context.Context, opt *Option) (string, error) {
          return "v1.0", nil
      },
      FakeGetRelease: func(ctx context.Context, tag string) (string, error) {
          return "", fmt.Errorf("failed to get %v release!", tag)
      },
  }

  ghr := &GhRelease{c: fakeclient}

  release, err := ghr.CreateNewRelease(context.Background())
  if err != nil {
      t.Error(err)
      // => failed to get created release: failed to get v1.0 release!
  }
  _ = release
  // ...
}

以下のような感じで,簡単にテスト用mockの実装を書いて,テストすることができます.

1
2
3
4
5
6
7
8
fakeclient := &fakeGitHub{
  FakeCreateRelease: func(ctx context.Context, opt *Option) (string, error) {
      return "v1.0", nil
  },
  FakeGetRelease: func(ctx context.Context, tag string) (string, error) {
      return "", fmt.Errorf("failed to get %v release!", tag)
  },
}

上記の例では1種類の実装しかテストしてないのであまり恩恵がわかりづらいかも知れないですが, 例えば error が帰ってきたときに正しくエラーハンドリングできてるかとか, 返り値をいろいろ変えたものをいくつか作ってテストする…といったことが上記のパターンを 使うことによって簡単にできます.Table Testing することも可能.

普通にわざわざstructごと作っていると,例えばテストの関数ないでは struct の method (e.g. func (c *client) Func()) を定義することができません.

そこで FakeFunc func() というfield を持たせて実装を丸投げすることによって, 簡単にいろんな実装のテスト用 mock を作成してテストができるということの紹介でした.

まとめ

僕は最初にこのパターンを教わってなるほどなぁ…と思ったんですが,いざ世にでてみると(?) ぜんぜんこのパターンを紹介しているものが見つからなかったので紹介してみました. (一応どっかの medium の英語記事にこれに似たパターンが紹介されてたのを見た気もする…)

ぜひ使ってみてください.

あまり関係ない追記

この記事の主旨とは関係ないけど,基本的にテスト用ライブラリは使わないとはいえ, たまににヘルパー関数ほしいなーというケースがあります.

でかい struct をテストで比較するときに,比較自体は reflect.DeepEqual で出来るのだけど, もし違っていたときにどこが違うかを表示するのが面倒くさいのでヘルパー関数提供してくれるライブラリがほしい…

某社でgoのテスト書いてたときもこういう大きめのstruct比較するケースでは便利diff表示用ライブラリを 使っていた気がしたんだけど,なんかOSSで見つからない気がする… prettycmp みたいな名前だった気がするが どうだったか… そもそも記憶違いな気もする…

追記: twitter で教えてもらいましたが https://github.com/kylelemons/godebug っぽいです. 便利. https://godoc.org/github.com/kylelemons/godebug/pretty#Compare

Reviewdog を飼ってコードレビューや開発を改善しませんか

reviewdog logo

GitHub: haya14busa/reviewdog: A code review dog who keeps your codebase healthy

英語記事: reviewdog — A code review dog who keeps your codebase healthy – Medium

reviewdog というlinter などのチェックツールの結果を自動で GitHub の Pull Request にコメントしたり, ローカルでも diff の結果から新たに導入されたエラーだけを表示するようにフィルタリングできるツールを作りました.

英語記事 を Medium に書いたし,README も書いたので 日本語記事はまぁいらないかなぁと思ったけど,柄にもなく Vim 関連以外で普通に便利ツールを書いてしまって,これは日本語でも簡単に共有しようかなぁと思いこの記事を書いています. (とはいえ機能の実現方法として Vim は関係はしてるんですけどね.)

特に和訳とかではなく,英語ではなかなか文章に落としづらかったことをだらだら書こうかと思います.英語は難しい… (だらだらと日本語を書いてるので日本語ができてるとは言ってない)

背景とかきっかけ

compiler や lint といったコードをチェックするツールはものによっては直さなくてもいいものをレポートすることがありますよね. 例えば Scala の compiler はオプションを与えたらかなりいろんな警告を出してくれますが, 別に直さなくてもいいケースがあるものがあったり, Play framework で使ってると ルーティングファイルやテンプレートファイルに対して,こちらが直せない警告を バンバンだしてきたりします.(これはバージョンを上げると直るっぽい?)

golint-set_exit_status を付けないと問題を見つけても exit status が 1 になりませんが, これはもともとあくまで コーディングスタイル の問題を見つけるものだという思想からでしょう.

Golint differs from govet. Govet is concerned with correctness, whereas golint is concerned with coding style.

Go の OSS プロジェクトでは CI で golint の問題があればビルドをFAILにすることが結構多いと思いますが, 本来直さなくてもいいところまで直さなくてはいけなくて消耗したりしていませんか?

去年 Google のインターンで Go 書いてたときは,実際 golint のエラーでテストがfail するとかではなく, bot が自動でコメントしてくれてあとは勝手に直せという感じで便利でした.

まぁ直さなくてもいい結果は少なくて大抵は直すのですが,直さなくてもいいものはゼロではないです. コメントで bot に直さなくてもいいものを指摘されたら単に無視してcloseすればいいので無駄に消耗せずにすんで便利.

また CI で落とすために指摘してくれると便利なことは多いけど設定で off にしてしまうケースもあるかなーと思います. 例えば JavaScript で関数の引数に使ってないものがあったら警告してくれると便利なケースもあるかなぁーという反面, コールバック用関数などでは渡ってくるのがわかってるからいいんだよ!というケースとか.

あとは既存のコードベースに新しく linter を導入しようとすると,既存のコードに対してエラー 出まくって導入するために直していくのが面倒だなぁ… 放置していると新しいコードに対しても lint が走らないので消耗する…ということもあるかと思います.

上記の問題を解決する1つの方法としては,とりあえずPull Request の差分に関して lint をかけたり して自動でbotがコメントしてくれる仕組みがあれば,100% 円満解決ではなくともかなりつらみが 解消されるかと思います.

世の中にはすでにそういうサービスは一応あって Hound CI とか SideCI がそうなんですが,使える言語やツールは限られています. たとえば Vim の linter を Vim プラグインのレポジトリに対して使えるようになることはないでしょう.

また,ローカルでも(差分に対して lint などを書けるという意味で)実行できないと Pull Request 出さないとチェックできないので不便です.

ということで作ったのが reviewdog になります.

あんまり技術的に面白いことがあったというよりは,本来あるべきものなのになかったので作ったという感じ. 汎用的に lint の結果をパースする手段をどのツールも提供してるものがないのが問題で, reviewdog は Vim の Quickfix 用の ‘errorformat’ という機能を Go で port することによって実現しました.

GitHub: haya14busa/errorformat: Vim’s quickfix errorformat implementation in Go

‘errorformat’ 形式を採用したのは僕が単に Vimmer だから… というのももちろんあるんですが, 仕様の全体を理解するのは結構大変とはいえ,簡単なerrorformatを書くのは簡単だし,それでいて 難しい複数行のエラーメッセージをパースできる利点があります.

emacs では compilation モードが Vim の quickfix に対応するようで,errorformat に対応するものは 正規表現とサブマッチのインデックスっぽいのですが,簡単な複数行エラーには対応してそうですが, Vim の ‘errorformat’ のほうが汎用的っぽいなーと思いました(詳しく見れてません)

‘errorformat’, 実装ポーティングして仕様を理解していくと,かなりよくできているなーという印象で Vim は時代の先を走ってると思いました.

また reviewdog 以外でも https://github.com/alecthomas/gometalinter のもっと汎用的なバージョンを言語を問わず errorformat を使って作るとかも可能なんじゃないかな〜と思います.(gometalinter 個人的にあんまり好きくないし)

インストールとか使い方書こうかと思ったけど疲れたのでREADMEとか英語のpost読んでください

あと雑だけど Google Doc の design doc な何かも一応ある reviewdog - Google Docs

CI サービス連携問題

reviewdog は Pull Request hook と実行環境さえあれば対応できてオープンなCIサービスの場合はGitHubへコメントするための API トークンを安全に環境変数などで保存する方法があれば対応できます.

travis や circle ci といったメジャーなCIサービスは両方対応していて,最初は全然このあたりは問題ではないなーと思ってたのですが, 実はOSS用のリポジトリに対して使おうとすると fork レポジトリからの pull-request では暗号化された環境変数は使えない! という問題があって,これどうすかなぁーということにかなり悩まされました. 考えてみれば当たり前で,echo $SECRET_VAR とかした悪意あるPull Request が開かれたら簡単に漏れちゃいます.

そこで,いろいろ CI サービスを探ってると https://drone.io/ の OSS バージョン https://github.com/drone/drone を見つけました.

help (Secrets · Drone) を読むと, yaml ファイルのチェックサムとセクション分けによって(完璧ではないものの)安全に 秘密の環境変数をforkからのプルリクでも使えるようになっているようです. 完全に便利なので travis とか circle ci でもこの機能ほしい…

drone.io OSS バージョンは https://drone.io/ とは別物という感じで環境変数の扱い以外も 結構便利っぽく,drone.io も reviewdog はデフォルトでサポートしました.

ただ OSS で雑にGitHubに上げたサービスに対して使うケースでも drone.io をどこかにホスティングしなくてはならないのが不便なところです… 個人的にはreviewdogのために Degital Ocean に月500円で drone.io 用サーバを立ててみて, 今の所かなり便利感ありますが,ワガママをOSS用にどっかホスティングしてほしい.

で,じゃあ結局forkからのPull Requestも受け付けるOSS用リポジトリにreviewdog導入 したい場合どうすればいいかというと,現状は Circle CI の Building Pull Requests from forks - CircleCI 機能をセキュリティリスクと引き換えに ON にするしかないかなーという感じです.

GitHub の Personal Access Token, しかも scope を適切に public_repo とかに設定しておけば, もし漏れても大したことはできないはずだし,rebokeもできるし,fork して悪意あるPull Request 作られたら流石に気付くし, まぁそもそもそんなこと GitHub の 権限すくないPersonal Access Token 得るためにやる人は 少ないのでは… という感じですね.ただ使う場合はもちろん自己責任でお願いします.

Circle CI か travis あたりが drone.io みたいに yaml のチェックサム+環境変数が使えるスコープを制限する機能がもし実装されたらそれを使っていきたい

(ところで他のCIサービスとして wrecker 試したんですが,fork からの pull-request で普通に秘密の環境変数使えてしまった気がする…問題では…あとPull Request hook によるビルドがなさそうだった)

終わりに

reviewdog 完全に便利という感じなのでぜひ使ってみてください〜

CI 用環境変数とか設定すれば Jenkins とかいろんな環境で導入できるはずだし, 一度導入したら撤退しずらいとかもないので,ギョームとかでも使えるような気がしますが リリース直後なのでいろいろ変わるかも知れないしそのへんはよしなにお願いします. GitHub Enterprise とか BitBucket 対応は気が向いたらするかもしれない. 特にGitHub Enterprise はGitHub 用の base の url 変えるだけで対応できる…?

ちょくちょく気になるところはあるのでちまちま改善していこうかなーと思います