ぽよメモ

レガシーシステム考古学専攻

これからコーヒー趣味を始める人に伝えたいこと3選

tl; dr

  • コーヒー器具の○杯用は小さいコーヒーカップ換算なのでマグカップで飲むなら信用するな
    • 余談:コーヒーサーバはオールガラスが洗いやすくておすすめ
  • 沸きたての熱湯でドリップするな、少し冷ませ
  • サーカスコーヒーはいいぞ

はじめに

この記事はコーヒーが趣味な筆者が偏見と経験だけを元に言いたいことを書いた物です。決して空いていたあくあたん工房アドベントカレンダー10日目の枠を埋めるためではありません*1

最近身の周りに家でコーヒーを家でドリップして飲む人が増えてきたような気がします。とても嬉しいので皆さんどんどん開拓して僕に色々教えて下さい。

この記事では始めたばかりの頃に引っかかりがち・やりがちのことについて軽く触れます。もちろん好みの部分もあるので、自分の手と舌と鼻を一番信用して下さい。なお、本記事で色んなものを紹介していますが、筆者にアフィリエイト収入等一切入りませんので安心して(?)ご覧下さい。

コーヒー器具の「○杯用」という表記

 僕は昔からこの表記が好きではないのですが、コーヒーをドリップするための器具にはどの程度の量をいれるのに良いか、という指標として「○杯用」などの表記が採用されています。大抵は1〜2杯用、もしくは3〜4杯用が一般的です。ところで、この「1杯」とは具体的に何mlに相当するんでしょうか?

www.ucc.co.jp

カップ一杯分(約140cc)

www.keycoffee.co.jp

1杯 120 ml

HARIOのドリッパーV60の説明書き https://www.hario.com/manual_pdf/VD.pdf

一杯分(120ml)

どうも120〜140mlくらいのようです。ここで我が家にあるコーヒーカップとマグカップを見てみましょう。

f:id:pudding_info:20201209162310j:plain
普通のコーヒーカップとこのすばマグカップ

高さは約半分というところです。口の広さは同じくらいですね。

f:id:pudding_info:20201209162530j:plain
コーヒーカップボーンチャイナで6年使っても綺麗

f:id:pudding_info:20201209174205j:plain
ソーサーもちゃんとついてるよ

小さく見えるコーヒーカップですが、僕は普段これに160~170ml入れて飲んでいます*2。既に120mlを大きく超えていますね……マグカップのときは230〜250mlくらい入れているため、だいたいマグカップ一杯が世に言う「2杯分」くらいに相当します。つまり、1~2杯分の器具だと実はマグカップ1杯分にしかならない可能性があり、来客対応などが難しくなる可能性があります。大は小を兼ねるということで、とりあえず3~4杯用のものを買っておくのが丸いと思います。

ところで1杯120mlという根拠はなんなんでしょうね……エスプレッソなどを飲むデミタスカップとしては120mlは多すぎますし、レギュラーコーヒーを飲む用としてはいささか量が少ない気がします。軽く調べてみてもコーヒーカップは満水時200mlくらい入るのが主流なように感じます。120mlだとかなり寂しい量になりそうですね。

余談:コーヒーサーバはオールガラス製が洗いやすい

コーヒーサーバとは、ドリッパーを使って抽出したコーヒーを一時的に入れておくポットです。Amazonなどで安いコーヒーサーバを探しているとだいたい以下の様な物がヒットすると思います。

www.amazon.co.jp

f:id:pudding_info:20201209171442p:plain
HARIO(ハリオ) V60レンジサーバー (上記サイトより引用)

とても安くて入手性も良く、持ち手が熱くならないなど良いこともあるんですが、とにかく洗いづらいし乾かしづらい
このプラスチックの部品とガラスの隙間に汚れや洗剤が溜まったり、そこだけ乾かなくてドリップしたときに温められた空気に押し出されて出てきたりします。

長年これが個人的に悩みで、しかしなかなか頑丈で壊れないので買い換えられずにいました。去年あるときついに割ってしまい、どうにかこのプラスチックの持ち手を避けようと色々探していたら各社それなりにオールガラス製のコーヒーサーバを出していることが分かりました。

僕が持っているのは以下のコーヒーサーバです*3 www.amazon.co.jp

f:id:pudding_info:20201209172638j:plain
撮影のためだけに引っ張り出してきた器具たち

オールガラス製で心配していたのは持ち手が折れてしまうとか熱くなってしまうとかですが、今のところ特にそういう問題は出ていません。たぶんこの持ち手が折れるようなときにはコーヒーサーバごと割れるので、持ち手がプラ製でも変わらんなというのが僕の感想です。とにかく洗いやすくなったし見た目もなんかオシャレなのでオススメです。

ドリップするときの温度

コーヒーの抽出というのは非常に難しく、かつ繊細な工程です。そしてドリップにおいて重要な要素の一つが湯温です。コーヒーのドリップ方法について調べると分かりますが、だいたい90℃!とか85℃!とかみんな言うことがバラバラで統一的な見解がありません。それは当たり前で、個人の好みや抽出器具、豆、煎り具合など多数の要素があるうちの一つの要素に過ぎないため、他の要素との兼ね合いで変わるためです。

一般に湯温が高ければ苦みが強い味になります。それに加えてえぐみや渋みの様な味も強くなります*4。多少まともな豆を買ってコーヒーを抽出する際にあまりに高い湯温にしてしまうと強い苦みや渋みが出てしまい、あれ?なんか安豆と変わらないのでは……?となりがちです。

T-falなどに代表される湯沸かし器を使ってお湯を沸かし、それを使ってドリップしている人が多いのではないでしょうか。最近は温度調節機能がついたものもありますが、多くの場合は沸騰させる機能しか無いはずです。この場合、完全に沸いたお湯をすぐ使うと100℃近いため、前述したように余計な成分を抽出してしまい本来ある豊かな香りやコクを損ないやすいです。

ドリップポットで少し冷ます

ドリップポットとかドリップケトルとかそういう呼び方をする道具があります。

f:id:pudding_info:20201209185052j:plain
以前バイト先で使っていた小さいドリップポット

これにはドリップ時にお湯を注ぎやすくするだけで無く、湯温を適度に下げるという役割もあります。あと見た目がオシャレになってなんかカッコイイ。

キッチン用の温度計が1000円台で買えるので、

  1. 電気ケトルからドリップポットに注ぐ
  2. 適当に85℃くらいに調整してドリップ
  3. 苦みが濃い→次回から温度を少し下げる
    苦みが薄い→次回から温度を少し上げる

こんな感じで良い具合の所を探ってみて下さい。

どこで豆を買うか

f:id:pudding_info:20201210204733j:plain
自分でドリップしながら撮影するのが難しすぎて諦めた写真

コーヒー豆はいろんなところで売っています。スーパーなどで売られるとてもリーズナブルな豆から、スターバックスやカルディなどのようなちょっとお洒落な雰囲気を漂わせた豆、専門店のお高い豆まで幅が広いです。

個人的にはやはり専門店に行って、その香りに触れて選んで欲しいなと思いますが、そうは言っても世間は大コロナ時代。やはり家から頼めるオンラインショップで済ませたいところ……。

というわけで筆者のおすすめコーヒー豆ショップを紹介します。

circuscoffee.shop-pro.jp

銀行振り込みしかありませんし、送料もかかりますが、一度は試して欲しいと思っています。初めて注文する方は以下の送料お得セットで少量試してみるのはいかがでしょうか。

circuscoffee.shop-pro.jp

挽いてもらわず豆のままであれば、ジップロックなどで密閉して冷凍庫で3週間〜4週間くらいもちます。ミルで挽くときに解凍なども不要です。是非お試しあれ。

まとめ

アドベントカレンダーの穴埋め筆者の丁寧なくらしの紹介いかがでしたでしょうか。 コロナで心がすさみがちな日々、コーヒーでほっと一息つくのは良い物ですよ。

明日のあくあたん工房アドベントカレンダーもご期待ください。


*1:執筆時点でまだ18日目が空いており、このままではまたよく分からない記事を捻出することになるので誰か書いて下さい

*2:おそらく満水時に200ml入る設計になっています。

*3:3〜4杯用を買えと言っておきながらこれは300mlしか入らないんですが、これは僕が普段はネルドリップ用のポットを使っていて、こっちは一人用をエアロプレスでいれるためにしか使っていないためです。

*4:この辺りは豆にもよるので一般論として受け取って下さい

日本語論文をLaTeXで書いてtextlintで校正するテンプレートを作った

はじめに

 この記事の対象者は主に修論・卒論を書く人です。英語論文についてはとくに言及しません。

 初めて論文を書くことはとても大変です。しかし、そのひどい初々しい論文を校正する人はもっと大変です。しかも大抵の学生はいっぱい書いてから見て下さい!とお願いしてきます。もはや何から手を付けて良いかすらわからない……。こんなひどい状況を打破するべく、体裁の指摘を自動でやってくれて、内容の指摘に集中できるようになるテンプレートを用意しました!
 これは実は原型のほとんどが実際にM2の時に研究室内で使っていた物なのですが、

  • 去年は忙しくて公開できるような形に持っていく余裕が無かった
  • でもこのまま忘れてしまうには惜しい
  • ↑を作る上で利用させていただいたOSSのメンテナになったので宣伝したくなった

という事情で今年公開することにしました。Pull Requestお待ちしています。

github.com

テンプレートの使い方

リポジトリをフォークする、又は以下の画像にあるUse this templateというボタンをクリックしてください。

f:id:pudding_info:20201205021153p:plain
Use this templateボタン

Creative Commons Zero(CC0)*1で公開していますので、著作者表記などに配慮することなく自由にご利用頂けます。

テンプレートの特徴

LaTeX on Docker

 Word論文はリジェクト!とまでは言いませんが、こうした自動化の恩恵を受けやすいのは単純なテキストで書けるLaTeXです。分からない人は覚えて下さい。
 昔はLaTeXの環境構築は大変だと言われていました(要出典)。今もこだわると大変ですが、どうしてもヒラギノを使わないと死んでしまう人以外はDockerで構築するのが良いでしょう。WindowsでもMacでもLinuxでも安定した結果を得られます。

 僕は以下のリポジトリで管理し、tagを付ける度にDockerhubでビルドするようにしています。このテンプレートではこのイメージを利用していますが、任意のイメージに差し替えることも出来ます。

github.com

https://hub.docker.com/r/pddg/latex

Makefile

 どんなコマンドを打つか思い出すのが面倒なので、このテンプレートではmakeを使います。Windowsではscoopを使ってmakeをインストールする他、開発環境をWSLに持ってしまうなどがおすすめです。

 make すればpdfができて、 make clean すれば中間ファイル含めて全て削除される、そういうシンプルな世界がオススメです。ちなみにGNU Makeの使い方を解説した僕の資料もあるので、是非使ってみてください。

Git管理

 今は2019年なのでGitで管理してGitHubにpushするのは、もはや特殊でも何でもありません。
 現在はGitHub Actionsという超便利なものがあります。これを使って、このテンプレートでは以下の様なワークフローを組みます。textlintやreviewdogについては後述します。

f:id:pudding_info:20201122223159p:plain
論文執筆のワークフロー

VSCode対応

 Visual Studio CodeMacWindowsLinuxのいずれでもよく使われており、非常に便利なエディタです。加えて、最近はDockerやWSLなどとの連携も強化されており、ポータブルな開発環境の構築が簡単になってきました。VSCodeLaTeX用拡張であるLaTeX WorkshopはビルドをDockerコンテナ内で実行することに対応しており、以下の様な環境をすぐに構築出来ます。

f:id:pudding_info:20201204235728g:plain
VSCodeで編集するときの様子

 PDFからソースへ、逆にソースからPDFの該当箇所へジャンプできます。また、保存時に自動ビルドが走ります。

 Dockerがインストール済みのホストにVSCodeをインストールして、必要なエクステンションを入れれば上の画像のような環境が完成するようになっています。

まとめ

 まずこのテンプレートによって2つの問題が解決します。

  • 環境構築が難しい
    • DockerとVSCodeが使用可能であればすぐにPDFを生成できる
  • 使い方が難しい
    • 最低限 make を覚えていればPDFが生成できる

書き始めるまでに脱落する人をできるだけ救えることができれば幸いです。

 実はもう一つ、タイトルにもある重要な特徴があるので次で紹介します。

textlintで校正する

 論文では全体の体裁を意識して記述する必要があります。基本的に表記揺れや「ですます調」「である調」の混じった文などは許されません。しかし、これらを人間が意識して校正するのは多大な労力を必要とします。そこでこれらについてある程度ツールで指摘出来るようにします。これを実現するのがtextlintです。

github.com

環境構築

 このような校正自体をローカルで実行する必要はないかなと思っています。PushしたときにCIでちゃんと警告されればそれでいいかなと。

 一応Node製のツールなので、LaTeXより楽に環境を整えることができます。単にNode JSをインストールしてnpm installすれば環境構築は完了です。

textlint-plugin-latex2e

 textlintは内部的に文章をASTへ変換して使用します。デフォルトではLaTeXファイルを読み込めないため、このプラグインを利用します。

github.com

昨年メンテナとして加えて頂き、サードパーティー製のLaTeXパーサへの移行のお手伝いをしました。
 textlintのASTとLaTeXパーサが解析したASTをすり合わせる、というような細かい作業をしており、おそらく潜在的なバグがあります。もしうまく動かないときは積極的にissueを上げて頂ければと思います*2

Rule

 textlintで警告を出すために様々なルールを追加していきます。
とは言ってもtextlint-rule-preset-japaneseまたはtextlint-rule-preset-ja-technical-writingをインストールすれば終了です。これはいくつかのルールをまとめたもので、個別のルールに対して設定を書くことも出来ます。全部書くと多すぎるので、言及の必要があるものだけ書きます。

github.com

github.com

これらは様々なルールを一括で導入できて便利な反面、この部分ではこのルールを無視したいと思ったとき、(後述するtextlint-filter-rule-commentsを利用すると)毎回% textlint-disable ja-technical-writing/ルール名として記述する必要があり、ちょっと面倒です。不要なルールがあったり、各ルールの最新版が常に反映されているわけではないなど、自分でルールを管理した方が全体を把握でき便利な場合もあります。とりあえず上記プリセットを突っ込んで使ってみて、不満があるなら自分で再構築するのが良いかと思います。

 各ルールの設定は、上記プリセットを導入している場合と、そこに含まれるルールを個別に導入した場合で書き方が異なります。

導入しているとき

{
    "rules": {
        "preset-ja-technical-writing": {
            "ルール名": {
                "設定名": ""
            }
        }
    }
}

導入していないとき

{
    "rules": {
        "ルール名": {
            "設定名": ""
        }
    }
}

textlint-rule-sentence-length

https://github.com/textlint-rule/textlint-rule-sentence-length

一文当たりの単語数ではなく文字数に制限をかけます。英語の文章を含む場合、ignoreする必要があるかもしれません。

textlint-rule-no-mix-dearu-desumasu

https://github.com/textlint-ja/textlint-rule-no-mix-dearu-desumasu

 である、ですます調を統一します。指定しない場合、全体から見て多い方が優先されるので、論文ではである調に統一します。

{
    "rules": {
        "no-mix-dearu-desumasu": {
             "preferInHeader": "である",
             "preferInBody": "である",
             "preferInList": "である",
             // 文末以外でも、敬体(ですます調)と常体(である調)を厳しくチェックするかどうか
             "strict": true
         }
    }
}

textlint-rule-ja-no-mixed-period

https://github.com/textlint-ja/textlint-rule-ja-no-mixed-period

 「。」と「。」のミスはprh(後述)で修正するため、こちらはどちらかというとそもそも文末のピリオド抜けを検知するために使います。

{
    "rules": {
        "ja-no-mixed-period": {
             "periodMark": "",
             "allowPeriodMarks": [],
             "allowEmojiAtEnd": false,
             // --fix時に直すかどうか
             "forceAppendPeriod": true
        }
    }
}

textlint-rule-no-doubled-joshi

https://github.com/textlint-ja/textlint-rule-no-doubled-joshi

 連続して同じ助詞を使用しないようにします。「AにBにします」みたいな文章に対してエラーを出します。当初全角カンマをうまく取り扱えず誤検知が起きていたため、要望を出して改善して頂きました*3。バージョン3.7.0以降を推奨します。

{
    "rules": {
        "no-doubled-joshi": {
            "min_interval" : 1,
            "strict": false,
            "allow": ["",""],
            "separatorCharacters": [
                ".", "", "", ""
            ],
            "commaCharacters": [
                "", ""
            ]
        }
    }
}

textlint-rule-prh

https://github.com/textlint-rule/textlint-rule-prh

 表記揺れを修正するためのルールです。「.」と「。」、「サーバ」と「サーバー」など、間違えやすい語句を検知して修正します。

{
    "rules": {
        "prh": {
            "rulePaths" :["prh.yml"]
        }
    }
}

表記揺れの一覧とその修正はprh.ymlとして別ファイルに書き込みます。

version: 1
rules:
  # Githubやgithubを修正する
  - expected: GitHub
  # ピリオドとカンマを修正
  - expected:pattern: /[。。]/g
  - expected:pattern: /[、、]/g

LaTeXではリンクがリンクとしてパースされない(単なる文字列として処理される)ため、リンクを含む場所ではignoreする必要があるかもしれません。必要な表現をどんどん足していくことで、表記揺れを自動で検知できるようになります。

Filter

 設定したルールに対して例外を設けることが出来ます。

textlint-filter-rule-whitelist

https://github.com/textlint/textlint-filter-rule-whitelist

 例えばprhでGitHubの誤字を修正したい場合、リンクまで検知してしまうという問題がありましたが、ここでホワイトリスト製で例外を管理できます。他のルールで引っかかった語もこれで排除することが出来ます。

{
    "filters": {
        "whitelist": {
            "allow": [
                "github.com"
            ]
        }
    }
}

textlint-filter-rule-comments

https://github.com/textlint/textlint-filter-rule-comments

 一部だけlintをオフにしたい場合に使えます。

{
    "filters": {
        "comments": {
            "enablingComment": "textlint-enable",
            "disablingComment": "textlint-disable"
        }
    }
}

例えば英文では文字数制限を解除したい場合、以下の様に書きます。

% textlint-disable sentence-length

\section{英語}
    hoge hoge fuga fuga

% textlint-enable sentence-length

\section {日本語}
    ほげほげふがふが

ルール名を指定しない場合、全てのルールを無視します。ルールはカンマ区切りで複数指定することが出来ます。

GitHub ActionsでCI/CD

 textlintしてreviewdogを走らせ、masterにマージしたら自動でPDFをビルド、タグを打つとそれをリリースします。プライベートリポジトリでは

  • Freeアカウント:2000分/月
  • Proアカウント:3000分/月

のビルド時間が無料枠になります*4GitHub Student Developer Packに登録することでPro相当の恩恵を得られるので、大学のメールアドレスを持っている人は登録しましょう*5。また、大学や研究室単位でTeamやOrganizationを使用している場合、上記に当てはまらない可能性があるため、そのOwnerに確認しましょう。

 Travis CIやCircle CIを使う場合と比べて、トークン等を自分で取得・指定する必要がなく楽です。テンプレートにはすでにGitHub Actionsのworkflowが組み込まれており、特に設定することなく自動で実行されます。

Lint

 LaTeXのコードを静的解析し、ルールに違反する文章を検出します。このテンプレートではPull Requestを発行/更新するとこのワークフローがトリガーされるようになっています。このワークフローの実行にはおよそ30~40秒かかります。詳細は .github/workflows/lint.yaml を見てみて下さい。
 違反した文章を修正しやすくするよう、reviewdogからコメントをもらえるようにしています。更新は全てPull Requestを経由することで、masterブランチは常にビルド可能かつルールに違反していない文章のみが存在するようになります。

↓修正箇所をこんな感じで指摘してくれます。

f:id:pudding_info:20191208002634p:plain
reviewdogによるレビュー

Build

 masterブランチへのpushでビルドを実行し、その成果物をアップロードしてダウンロード出来るようにします。1分~1分半程度かかります。

f:id:pudding_info:20191208003135p:plain
ビルドしたPDFのダウンロード

 この成果物はデフォルトでは90日間有効なようです*6。それ以上の期間更新したい場合、次章のようにタグを付けて自動リリースしましょう。

Release

 タグを付けたらGitHub Releasesに新しいリリースを作ってPDFをアップロードします。1分~1分半程度かかります。

f:id:pudding_info:20191208003250p:plain
GitHub ReleasesからPDFをダウンロード

 こちらはファイルの保持期限が特にないため、第一稿、第二稿、完成などのような段階ごとにタグを打っておくと分かりやすくて良いでしょう。

注意点

 インライン数式に$を使用すると大幅なパフォーマンス劣化を生じます。インライン数式は \( x^2 \) のように記述することでパフォーマンス劣化を避け高速にlintを実行できます*7
 $ によるインライン数式が多数利用されたTeXソースに対するlintは全く終わらなくなってしまいます。そのため、同梱しているGitHub Actionsのワークフローではタイムアウトを10分に設定しており、これを超えると強制的にワークフローが終了します。これによりGitHub Actionsの無料ビルド時間を食い潰さないようになっていますので、もしタイムアウトしたらインライン数式によるパフォーマンス劣化を疑ってみて下さい。

原因を特定したので近く修正版を出します。これにより、インライン数式によるパフォーマンス劣化は起きない見込みです。

[追記 2020/12/7] こちらの問題を修正したv1.0.3がリリースされました。こちらのテンプレートもそれに合わせてバージョンアップしv1.0.0をリリースしました。

まとめ

 LaTeXソースを静的解析するtextlintのプラグインを利用することで、校正作業の一部を自動化する方法を示し、そのテンプレートを紹介しました。体裁は自動で指摘し、内容を人が集中してレビューすることでレビュアーの負荷を少しでも下げることができれば幸いです。
 私自身はアカデミアから遠く離れてしまい、既に論文を書く身ではなくなっていますが、textlint-plugin-latex2eへのコントリビュートは続けていきますのでissueやPRお待ちしています。


*1:creativecommons.jp

*2:ただしメンテナーはサポート担当ではないので、それを誤解されませんよう…

*3:https://github.com/textlint-ja/textlint-rule-no-doubled-joshi/issues/26

*4:他のプライベートリポジトリと共有であることに注意が必要です。

*5:https://help.github.com/ja/github/teaching-and-learning-with-github-education/applying-for-a-student-developer-pack

*6:docs.github.com

*7:これを書きながらインライン数式が(textlint-plugin-latex2eの不具合により)ASTを破壊している気がしてきたのでたぶん直します

Protocol BuffersのGolang用API v2を使ってgRPCでHello Worldする

v2とは???

以下の記事にまとまっています。

qiita.com

blog.golang.org

Protocol BufferのGo言語用APIの新しい実装です。v1.20から始まるという奇怪なバージョニングになっています。しかも、後方互換性のない変更でありAPIv2とまで呼ばれているのに、モジュールの方はメジャー番号が1というカオスな状況です*1

github.com

2020年3月に出たばかりであり、まだまだ情報が少ないです。特に上の記事にもあるように新しいprotoc-gen-goはgRPC用のスタブを生成しなくなっており、そもそもこの新しいモジュールを使ってHello worldするのに試行錯誤したため、ここにメモを残しておきます。

ちなみに色々調べた後gRPCの公式ドキュメントを見たら普通に書いていました。最初から一次情報に当たれ、という話でした……

grpc.io

環境

  • macOS Catalina 10.15.6
  • Go 1.15.2
  • protoc v3.13.0
  • google.golang.org/protobuf/cmd/protoc-gen-go v1.23.0
  • google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.0.1

Hello World

プロジェクトの準備

適当にgo mod initしてプロジェクトを作ります。

$ mkdir go-protobuf-v2-sample && cd go-protobuf-v2-sample
$ go mod init github.com/pddg/go-protobuf-v2-sample

ツール類をインストールします。protocのインストール方法は何でもよいです。GitHub Releasesからダウンロードして展開するのが楽でしょう。

$ PROTOC_VERSION=3.13.0
$ PROTOC_PKG=protoc-${PROTOC_VERSION}-osx-x86_64.zip
$ wget -q -O tmp/ https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VERSION}/${PROTOC_PKG}
$ unzip -q -o tmp/${PROTOC_PKG}
$ ./bin/protoc --version
libprotoc 3.13.0

protocのプラグインをインストールします。僕はGoのプロジェクトでGo製のCLIツールが必要な時はgexを使っています。

github.com

$ gex --add google.golang.org/protobuf/cmd/protoc-gen-go
$ gex --add google.golang.org/grpc/cmd/protoc-gen-go-grpc

サービスを定義する

proto/hello.proto に名前を与えると Hello ○○ と返してくれるだけのサービスを定義します。

syntax = "proto3";

option go_package = "github.com/pddg/go-protobuf-v2-sample/hello/pb";


message HelloRequest { string name = 1; }

message HelloResponse { string message = 1; }

service HelloService { rpc Hello(HelloRequest) returns(HelloResponse); }

gRPC用のコードを自動生成する

v1まではgithub.com/golang/protobuf/protoc-gen-goを使い、 --go_out=plugins=grpc:パス といった形式で指定することでコードを自動生成していました。
v2からは2つのプラグインgoogle.golang.org/protobuf/cmd/protoc-gen-go とgoogle.golang.org/grpc/cmd/protoc-gen-go-grpc)を使い、それぞれ役割の違うコードを2つ自動生成します。

今回は ./hello/pb 以下に自動生成したファイルを置くことにします。

$ mkdir -p hello/pb
$ ./bin/protoc \
        -I proto/ \
        --plugin protoc-gen-go=bin/protoc-gen-go \
        --plugin protoc-gen-go-grpc=bin/protoc-gen-go-grpc \
        --go-grpc_opt paths=source_relative \
        --go-grpc_out hello/pb/ \
        --go_opt paths=source_relative \
        --go_out hello/pb/ \
        proto/hello.proto

これで hello/pb/hello.pb.gohello/pb/hello_grpc.pb.go という2つのファイルが生成されます。
hello.pb.go は、gRPCを使わずGoでProtocol Buffersを利用するために必要なコードが生成されるようです。HelloServiceの定義はここにはありません。
hello_grpc.pb.goには、gRPCを利用するための定義が記述されています。サーバ側が満たすべきインターフェースと、クライアントの実装が含まれます。

サーバを実装する

github.com/pddg/go-protobuf-v2-sample/hello/pb にある HelloServiceServer というインターフェースを満たすようにサーバを実装します。 hello/hello.go として記述します。 以下のサンプルを参考に実装しました。

github.com

package hello

import (
    "context"

    "github.com/pddg/go-protobuf-v2-sample/hello/pb"
)

// HelloServiceの実際の実装。
type helloServer struct{
    pb.UnimplementedHelloServiceServer
}

// Hello ○○するハンドラ
func (hs helloServer) Hello(ctx context.Context, request *pb.HelloRequest) (*pb.HelloResponse, error) {
    return &pb.HelloResponse{Message: "Hello " + request.Name}, nil
}

// 実装したサーバを初期化する関数
func NewHelloServiceServer() pb.HelloServiceServer {
    return &HelloServer{}
}

cmd/server/main.go を作成し、サーバを起動するコマンドを実装します。オプションとしてリッスンするportとホスト名を取れるようにしました。
簡単のため、http2ではなくtcpでリッスンします。

package main

import (
    "flag"
    "fmt"
    "log"
    "net"

    "google.golang.org/grpc"
    "google.golang.org/grpc/reflection"

    "github.com/pddg/go-protobuf-v2-sample/hello"
    "github.com/pddg/go-protobuf-v2-sample/hello/pb"
)

var (
    port int
    host string
)

func main() {
    flag.IntVar(&port, "port", 8080, "Port number")
    flag.StringVar(&host, "host", "0.0.0.0", "Listen host name")
    flag.Parse()

    listenAddr, err := net.Listen("tcp", fmt.Sprintf("%s:%d", host, port))
    if err != nil {
        log.Fatalf("Failed to listen '%s:%d'\n", host, port)
    }
    log.Printf("HelloServiceServer is listening on tcp://%s:%d\n", host, port)

    s := grpc.NewServer()
    helloServer := hello.NewHelloServiceServer()
    pb.RegisterHelloServiceServer(s, helloServer)

    // Enable reflection
    reflection.Register(s)

    if err := s.Serve(listenAddr); err != nil {
        log.Fatal(err)
    }
}

せっかくv2で実装するのでreflectionを有効化しました。これにより、grpcurlなどのクライアントツールで proto ファイルを読み込む必要がなくなります。 このコマンドを bin/hello-server としてビルドします。

$ go build -o bin/hello-server ./cmd/server

クライアントコマンドの実装

cmd/client/main.go に記述します。オプションとしてサーバのアドレスを渡せるようにしました。

package main

import (
    "context"
    "flag"
    "fmt"
    "log"
    "os"
    "time"

    "google.golang.org/grpc"

    "github.com/pddg/go-protobuf-v2-sample/hello/pb"
)

var server string

func main() {
    flag.StringVar(&server, "server", "localhost:8080", "Server address")
    flag.Parse()
    if flag.NArg() == 0 {
        flag.Usage()
        os.Exit(1)
    }
    args := flag.Args()

    ctx, cancel := context.WithTimeout(context.Background(), 10 * time.Second)
    defer cancel()

    conn, err := grpc.DialContext(ctx, server, grpc.WithInsecure())
    if err != nil {
        log.Fatal(err)
    }

    client := pb.NewHelloServiceClient(conn)
    response, err := client.Hello(ctx, &pb.HelloRequest{Name: args[0]})
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(response.Message)
}

このコマンドを bin/hello-client としてビルドします。

$ go build -o bin/hello-client ./cmd/client

動かしてみる

適当にターミナルを立ち上げ、サーバを起動します。

$ ./bin/hello-server -port 8080 -host 0.0.0.0

別のターミナルからクライアントコマンドを使用して接続し、Hello worldしてみます。

$ ./bin/hello-client -server localhost:8080 world
Hello world

やったぜ。

grpcurlで叩いてみる

grpcurlはコマンドラインで使えるgRPC用の汎用ツールです。curlのような感覚で使えます。

$ gex --add github.com/fullstorydev/grpcurl/cmd/grpcurl

grpcurl サーバアドレス サービス名.呼び出す関数 という形式で使えます。 データを送信したい場合、JSON形式でPOSTできます。レスポンスもJSON形式で返ってきます。
今回のHelloServiceを叩く場合は以下の様になります。

$ gex grpcurl -plaintext -d '{"name": "world"}' localhost:8080 HelloService.Hello
{
  "message": "Hello world"
}

まとめ

これまでのprotoc-gen-goが単体でGo用のProtocol Buffersのコード・gRPCのスタブコードの両方を生成するスタイルよりも、役割が明確になったのはよかったのではないでしょうか*2
v2の恩恵をまだreflectionくらいしか理解していないので、もう少し色々探索していきたいですね。

[追記]
実装全体を以下で公開しました。

github.com


*1:モジュールパスが異なるので、一応Go Moduleのセマンティックバージョニングにはちゃんと従っている事になります

*2:ただ、正直github.com/protocolbuffers/protobuf-goのバージョニング戦略はイケてないな〜と感じてしまいました