blog.syfm

徒然なるままに考えていることなどを書いていくブログ

CLI ツールのフラグ設計むずかしい

CLI ツールを作る・保守していく上で最も難しいのはフラグ設計かもしれない。 一つのことをうまくやる、サブコマンドを持たないレベルまで小さいものであれば比較的容易だけど、サブコマンドを使うレベルになってくると途端に難しくなる。

どういった点が難しいのかをざっと思いつくものについて上げてみる。

  • フラグ名の割り当て
  • 適切な名前
  • 提供する機能の粒度
  • 後方互換

フラグ名の割り当て

例えば --version の短縮名として -v--help の短縮名として -h がしばしば使われる。なので、ユーザは初めて使う CLI ツールであってもとりあえず -v-h を叩いてみることが多い (特に使い方がわからない場合は --help-h を必ずと行っていいほど叩くと思う)。
ここで、-h がヘルプではなく別の機能に割り当てられていた場合、ユーザは違和感を覚えると思う。直感的な操作を提供できるか、というのは CLI ツールの設計においてかなり重要だと感じるのでできる限り直感に沿った機能を提供したい。

しかし、その一方でフラグ名の割り当ては慎重に決定する必要がある。例えば、ヘルプはユーザが CLI ツールの使い方を忘れるたびに叩かれるのでそれなりに使用される頻度は多いが、バージョンを表示する --version はそれと比べて圧倒的に頻度が少ないはず。使用頻度が少ないコマンドに短縮名を使ってしまうと、その文字を頭文字に持つ他のフラグの短縮名を提供したくなったときに困ることになる。例えば、v が頭文字につくものでしばしばフラグ名に使われているものとして --verbose がある。--verbose が具体的にどういった機能を提供するかはそれぞれの CLI ツールに依るが、少なくとも冗長・補助的な出力を増やすフラグであることがほとんどで、--version よりも使用される頻度が高いと思う。

短縮形で使用できる文字にはかなり制限がある (一文字しか使えないので) ため、短縮形を使うべきフラグは非常に使用頻度が高いものや、その CLI ツールにおいてコアな機能を提供するものにのみ使うべきだと個人的には思う (= 基本的に短縮形は使わない)。

適切な名前

例えば --port のような、フラグ名の指しているものの値を変えるような場合であればフラグ名を決定するのは容易だが、振る舞いを変えるようなフラグ名の場合は適切な名前を決めるのが難しくなる。これは変数名をつける時と同じ問題だが、CLI ツールの場合、フラグ名の長さは短ければ短いほど良いのでより難しい。 しかも、振る舞いを変えるフラグの場合、あまり慣習のような名前もない。そのため、日常的に使われている CLI ツールを見てもあまり参考にならなかったりする。

提供する機能の粒度

これが一番難しい。本当に難しい。例えば、出力のフォーマットを JSON 形式で行いたいというユースケースがあるとする。ぱっと思いつくフラグ名は --json といったフラグを提供して、これが有効になっていれば通常の出力ではなく JSON 形式に切り替えるようにする。しかし、JSON だけで良いのかという問題もある。例えば、kubectl は --output というフラグを提供していて、これは --output=jsonJSON 形式、--output=yamlYAML 形式で出力を行うことができる。

ではこの CLI ツールにも --output--format のようなフラグを作って複数のフォーマットに対応したほうが良いのだろうか?正直一長一短だと思う。複数のフォーマットに対応するということは一つのことをうまくやる UNIX 哲学に沿っていないようにも感じられる。また、--output フラグには値を指定しなければいけないため、bool のフラグよりもほんの少しだけ使い方が複雑になる。しかし、その一方で出力内容が複雑な場合、構造化された出力形式がないとパイプを組み合わせたフィルタ処理がしにくく、やはり UNIX 哲学に沿えてない設計になってしまう。また、--json のような特定のフォーマットに依存したようなフラグを定義すると他のフォーマットをサポートすることになった場合に比例してフラグの数が増えることになる。また、--json という名前だけでは 出力 フォーマットかどうか知ることができない。ツールが複雑になっていくと、例えば入力を JSON 形式の文字列で受け取りたいといった別なユースケースが生じてフラグ名が衝突する可能性もある。かといって --json-out のような名前にするとフラグ名が長くなってしまい、打つのがめんどうになる。短縮名を用いることはできるが前述したとおり、それもまた衝突する可能性がある。

後方互換

OSS として公開しているともっとも大変なのが後方互換性の維持だと思う。自分しか使っていないような CLI ツールであれば master に雑に push して後方互換性を破壊しても困る人はいないが、多くの人に使われるようになった CLI ツールでは後方互換性を維持することが必須となる。セマンティックバージョニングであればメジャーバージョン前であれば定義的には後方互換性を壊しても問題ないが、告知なしに互換性を壊されるとツールを使うのをやめるユーザも多いと思う。そもそも多くの人にとってセマンティックバージョニングはどうでも良く、手元ですんなり動くことが最も重要なはず。

例えば社内のライブラリであれば開発チャンネルで @here したり、マイクロサービスであればクライアントをすぐに洗い出せるので合意も取りやすい。しかし、CLI ツールは社内のソフトウェアとは異なり、ユーザの顔が見えないので後方互換性を崩したい場合は事前に告知をした上で、メジャーバージョンを上げる際にのみ行わなければいけない。

前述したフラグ名の問題も後方互換性の問題と組み合わせるとより辛くなる。迂闊にネームスペースの大きいフラグ名を付けてしまうと次のメジャーバージョンまでそれを変更することができず、さらにそのフラグ名にふさわしい機能が増えた時に割り当てることができずに妥協した名前になってしまう。特にサブコマンドがある CLI ツールの場合、グローバルフラグ (コマンド名の直後に現れる、CLI ツール全体で有効なフラグ) とローカルフラグ (サブコマンドでのみ有効なフラグ) があることが多いので定義するフラグがグローバル・ローカルどちらにあるべきかを慎重に選ばないといけない。

うーん、むずかしい 😇

一年を振り返り… (2019)

去年は 👇

syfm.hatenablog.com

去年の 6 月ごろからほとんど毎日、雑に日記をつけていたのでこの振り返りもだいぶ書きやすくなった。

journey.cloud

Journey というアプリを使っている。各プラットフォーム向けにアプリがあり、Google Drive にエクスポートできたりするのが良いし、普通に使いやすい。毎日 22 時頃にリマインダーを飛ばしていて、一日の振り返りを雑に書く。だいたい一行二行くらいだけど気が向いてるときとか印象的なことがあった日は長文を書いていたときもあった。酒飲んでいて眠いときとかは書かないことも良くあった。このくらい雑だと意外と習慣化できる。

1 月

f:id:ktr_0731:20191222001322j:plain
今年もここから

今年も年越しを友人らと過ごし、初日の出を見てきた。ここ数年は元旦に雨の日がなくて良い。
会津では基本的に研究を進めながら、友人らとスマブラやったり (自分はクソ弱い) 、go-fuzzyfinder という OSS の開発をしたりしていた。去年の 10 月から一切働いておらず、無駄に時間はあったので自由気ままな生活をしていた。

OSS については、Evans が年明けまでに 500 stars はいけなかったけど年明け直後に突破した。また、12 月には 1000 stars を突破することもできた。Evans についてはまた別の機会に記事を書きたい。

読んだ本

五等分の花嫁(1) (週刊少年マガジンコミックス)

五等分の花嫁(1) (週刊少年マガジンコミックス)

日の出待ちにいた漫画喫茶で読んでた。最初からずっと二乃派です。

A Philosophy of Software Design

A Philosophy of Software Design

  • 作者:John Ousterhout
  • 出版社/メーカー: Yaknyam Press
  • 発売日: 2018/04/06
  • メディア: ペーパーバック
今年読んだなかで二番目に良かった本。書評に関しては別途記事を書いてるのでそちらも併せて。

syfm.hatenablog.com

go-fuzzyfinder を開発しているときに文字列処理に興味を持ったので読んだ。

2 月

f:id:ktr_0731:20191222004926j:plain
今年は雪が少なかった

この時期は卒論執筆も終盤に差し掛かり、結構忙しかった記憶がある。また、それは卒業が目前に迫っているということでもあったので、最後の思い出づくりに卒論の合間に会津で好きだったところにもう一度行ったりしていた。ごはんはあおやま、HERO'S DINER、よしのや食堂、YAKi 家、丸忠あたりが、居酒屋は天竜、盃爛処、福住あたりが好きです。

引っ越しもこの時期にした。4 年間住んでいた家は毎日夕焼けを眺めることができたのですごく気に入っていた。会津はなにもないけど風景が本当に綺麗。

卒論を書き終わったあと、ktr0731/go-fuzzyfinder という、fzf のような機能を提供する Go ライブラリを公開した。100 stars 超えていて、それなりに需要があったんだな〜と思った。

読んだ本

やはり俺の青春ラブコメはまちがっている。12 (ガガガ文庫)

やはり俺の青春ラブコメはまちがっている。12 (ガガガ文庫)

  • 作者:渡 航
  • 出版社/メーカー: 小学館
  • 発売日: 2017/09/21
  • メディア: 文庫
俺ガイルの最新刊が出たので読んでいた。前巻と間が空きすぎて忘れていたのでそちらも読み直した。

アニメ観てハマったので全巻買って読んだ。

冬目景作品の新刊。冬目景にしてはかなり早く続刊を出していて驚いた。

[試して理解]Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識

[試して理解]Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識

図やコードが多くて分かりやすかった。普段このレイヤーはあまり意識しないのでこういった本は非常にありがたい。

3 月

f:id:ktr_0731:20191222011223j:plain
サンシャイン 60 展望台

3 月は東京への完全に引っ越しも一段落し、新しい街を歩いたり、東京にいる友人にあったりしているだけの生活をしていた。あとは卒業式とその前日の LT イベントのために 2 日だけ会津に戻ったりもしていた。4 年間本当にあっという間だった。
鎌倉、江ノ島に行ったりもしていた。最後に行ったのは中学校の修学旅行だったのでだいぶ記憶が薄くなっていたけど、当時見た場所や入った店とかをなんとなく思い出せて懐かしい気分になった。すごく良かったけど、昔行ったときより人がかなり増えていた気がした。

読んだ本

現代法学入門 (有斐閣双書)

現代法学入門 (有斐閣双書)

  • 作者:
  • 出版社/メーカー: 有斐閣
  • 発売日: 2005/03/01
  • メディア: 単行本

社会がアラートループ事件とか Coinhive 事件とかで不穏だったので自衛のために最低限の知識はつけておきたいと思ったので読んだ。この手の本にしてはかなり読みやすく、題材も面白かった。

金融ドメインをざっと理解したかったので手にとったが、実際は Fintech 寄りの本だったのであんまり読んだ意味はなかった気がする。

Kubernetes についてハンズオン的に進めながら読める本。サンプルは Azure だけど GCP でも問題なく進められる。

4 月

f:id:ktr_0731:20191222013655j:plain

社会人一年目として働き始めた月。この月は仕事以外でなにをやったのかあまり覚えていないけど多分何もやってなかったんだと思う。自分自身、環境が急激に変わることがあんまり好きではないのでそのせいで消耗していたのかもしれない。入社エントリでも書いたけど新卒研修を途中で切り上げて早期配属を希望したのもこの月で、月末にはチームに合流し GW を迎えた。

Google Photo の写真眺めててユーフォの劇場版めちゃくちゃ良かったのを思い出した。あとなぜか新宿御苑の写真があった。なんで行ったんだっけ?

読んだ本

京都アニメーションでアニメ化予定の本。結構面白かった。

響け! ユーフォニアム 北宇治高校吹奏楽部、決意の最終楽章 前編 (宝島社文庫)

響け! ユーフォニアム 北宇治高校吹奏楽部、決意の最終楽章 前編 (宝島社文庫)

  • 作者:武田 綾乃
  • 出版社/メーカー: 宝島社
  • 発売日: 2019/04/17
  • メディア: 文庫
響け!ユーフォニアムの完結編の前編。前編なのでとにかく不穏だったという印象が強い。

5 月

f:id:ktr_0731:20191227022421j:plain

GW に土善旅館で開発合宿をしていた。自分は Evans のフルリプレイスをしていたような気がする。土善旅館については前々から聞いていたけどかなり備品が揃っていて何一つ不自由なく開発できて最高だった。ブログも書いた。

syfm.hatenablog.com

月半ばに Go Conference 2019 Spring があり、自分のプロポーザルが採択されていたのでスライド作りや発表練習を結構していた。go-fuzzyfinder について話し、結構好評だったので嬉しかった。

読んだ本

主観で判断するのではなく本質を見ましょうね、といった内容の本。

6 月

f:id:ktr_0731:20191227023033j:plain
日光東照宮に行った

親族との旅行で日光に行っていた。日光東照宮とか二荒山神社とか、あのあたりはかなり多くの歴史ある神社仏閣があって巡っているだけでかなり楽しい。 宿泊した湯西川も良かった。鬼怒川よりさらに奥地なのでさらに寂れていた。途中通った鬼怒川温泉街の寂れ具合も最高だった。会津地方の芦ノ牧温泉街感ある。 *1

仕事の方では企業側の人間として母校の会津大学の LT イベントに参加して発表したりしてきた。後輩や、院進や留年した友人らも元気そうで良かった。 また、配属チームで 5 月から開発していた静的 MPM 決済が無事リリースでき、一段落できた。

jp.merpay.com

読んだ本

夢で見たあの子のために(4) (角川コミックス・エース)

夢で見たあの子のために(4) (角川コミックス・エース)

読んだけどどんな内容だったか忘れてしまった。

亜人(14) (アフタヌーンKC)

亜人(14) (アフタヌーンKC)

相変わらずかなり熱い展開が続いていて先が読めない。

ブラック・ラグーン(1) (サンデーGXコミックス)

ブラック・ラグーン(1) (サンデーGXコミックス)

社会勉強するために全巻買って再読した。

青のフラッグ 1 (ジャンプコミックスDIGITAL)

青のフラッグ 1 (ジャンプコミックスDIGITAL)

エグい。

響け! ユーフォニアム 北宇治高校吹奏楽部、決意の最終楽章 後編 (宝島社文庫)

響け! ユーフォニアム 北宇治高校吹奏楽部、決意の最終楽章 後編 (宝島社文庫)

  • 作者:武田 綾乃
  • 出版社/メーカー: 宝島社
  • 発売日: 2019/06/22
  • メディア: 文庫
響け!ユーフォニアムの完結巻。めちゃくちゃ面白かったので早く映像でも見たい…。

クレジットカード決済ドメインについて扱っている本で、メルペイのシステムのクレジットカード決済ドメインと似ているので頭の中で単語や全体像を整理できて良かった。

7 月

f:id:ktr_0731:20191227023334j:plain
友達と映画館行ったらたまたまやってたユーフォの原画展

入社して 3 ヶ月が経ち、雇用が切られなかったので入社エントリを書いたりしていた。 この月は体調が悪い日が多く、日記にも体調が悪かったとか WFH したとか書いてあったけどなにがあったのかよく分からない。いつの間にか治っていた。

天気の子が公開されたので観に行ってきた。どこか既視感を感じたけど面白かった。前々から新海誠

が好きだったみたいだけど最近六本木ヒルズ森タワーもお気に入りに追加されたっぽい。

読んだ本

入門 監視 ―モダンなモニタリングのためのデザインパターン

入門 監視 ―モダンなモニタリングのためのデザインパターン

今月から OnCall に入ることになり、運用もしていくようになったけど、運用の経験・知識がなかったので監視の基本的な知識を身に着けたくて読んだ。

8 月

f:id:ktr_0731:20191227023648j:plain

月半ばにはメルペイ側のエンジニアとしてサイバーエージェントとの合同イベントで登壇していた。Go modules に関する知見は今まであまりなかったので良い発表だったかなと思った。 あとはお盆だったので実家に久しぶりに帰り、地元の友人と飲みに行ったりしていた。地元は魚の美味しい地域なので最高。

読んだ本

BANANA FISH ANOTHER STORY (1) (小学館文庫)

BANANA FISH ANOTHER STORY (1) (小学館文庫)

アニメ版の BANANA FISH を観たので、そのアフターストーリーを読むために買った。

小説 天気の子 (角川文庫)

小説 天気の子 (角川文庫)

  • 作者:新海 誠
  • 出版社/メーカー: KADOKAWA
  • 発売日: 2019/07/18
  • メディア: 文庫
映画の補完として読んだ。

9 月

f:id:ktr_0731:20191229212817j:plain
客室からの眺めが本当に最高だった

この月は ISUCON があった。自分たちのチームは毎年旅館で ISUCON をやっているので、今年も伊豆の方へ旅行に行った。かなり最高なので来年もやりたい。前回は伊香保、今年は伊豆 (土肥) だった。どうせ行くなら毎回別な温泉地が良いので来年も熟考したい。

また、10 年来のネット上での友人らとのオフ会が実現したのもこの月だった。当時たくさんいた、ネットでのみ関わりがあった人たちももはやほとんどいなくなってしまったけれどそれでもこうして集まれる友人がまだ 10 人弱いるのはびっくりすると同時にすごく嬉しい。また来年開催したいね、といった話をしたので来年も楽しみ。

読んだ本

進撃の巨人(1) (週刊少年マガジンコミックス)

進撃の巨人(1) (週刊少年マガジンコミックス)

無料キャンペーンやっていたので読み始めたけど、読む時間がなくて 1-2 巻くらいしか読めなかった…。

10 月

f:id:ktr_0731:20191229220027j:plain
六本木ヒルズ展望台

この月は特に印象に残ったイベントはなく、静かな月だった気がする。日記を見る感じ、ハードワークをしていたわけではないけど結構仕事に手一杯で、家に返ったらぼーっと過ごして眠くなったら寝るみたいな生活をしていたっぽい。本も読めていなかった。
ただ、これはあんまり良くないなーと思ったので会社の OKR の個人 KR に OSS 活動や技術インプット・アウトプットを入れて意識して時間を有効に使うようにした。

11 月

f:id:ktr_0731:20191229221427j:plain

この月は仕事がかなり慌ただしく、いろいろなことが目まぐるしく動いていた。というか仕事のことと飲んでいたことくらいしか覚えてない…。あとは苦しい出来事があり、急だし予定も何も立てていなかったけど 2 日くらい有給を取って実家に帰ったりしていた。東京は会津より実家に帰りやすく、2 時間くらいで行けるので良い。

読んだ本

20世紀少年―本格科学冒険漫画 (1) (ビッグコミックス)

20世紀少年―本格科学冒険漫画 (1) (ビッグコミックス)

突然カンナのラビット・ナボコフが読みたくなったので買って再読した。

サマータイムレンダ 1 (ジャンプコミックスDIGITAL)

サマータイムレンダ 1 (ジャンプコミックスDIGITAL)

本屋でなんとなく目に入ったので買った漫画。ここ数年読んだ漫画の中で一番面白い (そもそもそこまで読んでないけど)。やっぱループとかタイムリープものがめちゃくちゃ好きなんだよなー。

5 月からゆっくり読み進めていた本で、今年読んだ本の中で一番良かった。以下の記事でも挙げている。

syfm.hatenablog.com

12 月

f:id:ktr_0731:20191229224702j:plain

社内の開発合宿で箱根に行ったりしていてすごく楽しかった。自分は The Amazon Builders' Library の記事とか go-fuzzyfinder のメンテ・機能開発とかをしていた。 仕事は相変わらずゴタゴタしていたけどなんとか仕事を納めつつ、実家に帰り中学の同窓会をした。高校の同窓会は全然ないけど中学は結構な頻度でやってる気がする。次は GW かな、みたいな話もした。 あとは大晦日にいつものメンバーで集まって年を越すだけ。

読んだ本

ブルーピリオド(1) (アフタヌーンコミックス)

ブルーピリオド(1) (アフタヌーンコミックス)

かなり面白い。印象に残るセリフが多く、すごく共感できる。

まとめ

社会人になり、モノリシックなソフトウェア開発ではなくマイクロサービスの開発に関わるようになったことや、運用・保守に関わるようになったことでとにかくあらゆることを学ぶことができた濃い一年だった。 所属しているチームも居心地がよく、残業もほとんどしていないので精神衛生 (心理的安全性?) もかなり良く楽しく働けている。

社会人になっても学生の時と同じくらいプライベートでもコードを書いたりアウトプットができているのでこれからも継続したい。逆にインプットはもっと増やしたいと思う。ずっとコード書いていたい。

ただ、基本的に仕事中心になってしまっていたのはあんまり良くないと強く感じているので来年はもう少しプライベートな時間を有効に使えるようにしたいなーと思う。

*1:芦ノ牧のほうがより寂れているけど…

pkg/errors から徐々に Go 1.13 errors へ移行する

Go 1.13 からの新しい errors

Go 1.13 からエラー処理が強化され、errors パッケージに As と Is 関数が追加されました。これにより、今までは pkg/errors のようなライブラリを使用しなければ実現するのが難しかった、型情報を保持したままの wrap/unwrap が可能になりました。

サードパーティのライブラリに依存せずに実現できるのは非常にありがたい反面、移行コストが高そうでなかなか移行できていないプロジェクトも多いかと思います。また、pkg/errors ではスタックトレース情報を保持してくれますが、Go 1.13 errors では保持してくれないといった問題もあるため、完全移行が難しいといったケースもあると思います。

pkg/errors の Go 1.13 errors 対応

しかし最近、以下の Pull Request がマージされ pkg/errors で wrap されたエラーは Unwrap() error を実装するようになりました。

github.com

これを利用すると pkg/errors で wrap されたエラーであっても Go 1.13 の IsAs が機能するようになります。 例えば今までの pkg/errors だと以下のコードは false を出力しますが、この変更が入っていると true を出力するようになります。

package main

import (
    "errors"
    "fmt"

    pkgerr "github.com/pkg/errors"
)

func main() {
    berr := pkgerr.New("err")
    err := pkgerr.Wrap(berr, "wrapped")
    fmt.Println(errors.Is(err, berr))
}

この変更を適用したい場合、まだリリースタグが切られていないためコミットハッシュを指定して pkg/errors を更新する必要があります。最近の pkg/errors はあまり開発がアクティブではないため、コミットハッシュで固定してもあまり問題にはならないと思います。

2020 年 1 月 13 日追記: v0.9.0 がリリースされました。以下を実行すると更新することができます。

$ go get -u github.com/pkg/errors

これにより、既存コードのエラーの wrap 処理はそのままにしつつ errors.Is や errors.As といった標準ライブラリの機能を使えるようになりました。

pkg/errors にどういった変更が入ったのか

そもそも、pkg/errors にどういった変更が入ったことでこういった振る舞いをするようになったのでしょうか?
Go 1.13 の errors パッケージの IsAs のドキュメントには以下のような記述があります。

The chain consists of err itself followed by the sequence of errors obtained by repeatedly calling Unwrap.

また、 Is の実装を読んでみると、3 つのことを繰り返し行っています。

  1. 比較可能であれば比較する
  2. Is(error) bool を実装していれば呼び出す
  3. Unwrap 関数に err を渡して unwrap する
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 supporting 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
        }
    }
}

Unwrap も非常にシンプルで、ただ型アサーションをしているだけです。

func Unwrap(err error) error {
    u, ok := err.(interface {
        Unwrap() error
    })
    if !ok {
        return nil
    }
    return u.Unwrap()
}

基底のエラーに到達するまで繰り返し同値判定を試みていることがわかります。 ここで重要なのは渡されたエラーが Unwrap() error を実装していればそれが呼び出されるということです。 実際、pkg/errors の Go 1.13 errors に対応した Pull Request の変更も Unwrap() error を実装しただけの非常にシンプルなコードとなっています。

github.com

Go 1.13 errors に対応したライブラリの例

pkg/errors 以外にも Unwrap() errorIs(error) boolAs(interface{}) bool を暗黙的に実装するようになったライブラリがいくつかあります。ここではその一部を紹介します。

標準ライブラリ

当然ですが、標準ライブラリのいくつかのパッケージにも Go 1.13 errors への対応が入っています。
例えば、 net/url パッケージには Error という、発生したエラーに加えて補助的な情報も持つことのできるエラー型がありますが、この型が保持している基底のエラーを Unwrap で取り出せるようになりました。

golang.org

同じように os パッケージの PathErrorLinkErroros/exec パッケージの Error にも Unwrap() error メソッドが追加されています。

cloud.google.com/go/spanner

GCP のライブラリの spanner パッケージは Go 1.13 errors に対応しており *1、返ってくるエラーは Unwrap() error を実装しています。
また、 GRPCStatus() *status.Status というメソッドも実装しているため、以下のように組み合わせることもできます。

var s interface{ GRPCStatus() *status.Status }
if errors.As(spannerErr, &s) {
  fmt.Printf("%v\n", s.GRPCStatus())
}

たとえ pkg/errors で wrap されていたとしてもこのコードは正しく動作します。

まとめ

このように、pkg/errors が使われているコードベースであっても最新のコミットを取り込めば大きな変更なしに Go 1.13 errors に移行できることがわかりました。 pkg/errors は最近はあまりアクティブではなく、Go 1.13 errors でのエラーハンドリングが主流になっていくと思うので移行を検討して観る価値はあると思います。

*1:まだバージョンは切られていませんでした…