あぼぼーぼ・ぼーぼぼ

のんびり生きたい

『その仕事、全部やめてみよう』を読んだ

www.amazon.co.jp

谷を埋めるのではなく山をつくる「ラストマン戦略」は最近はわりと同意できる部分があって、ふむふむと読めました。

パフォーマンスを高めるために力を抜く、の部分もなるほど〜と。400メートル走の例は分かりやすくて、スピードに乗ると全力を出さなくてもペース維持ができたり、自分に合ったペース配分を見つけたり、みたいなことは活かせるなと思いました。400メートル全力で走れる人間はいないですからね。

ただ「ひよコード」のくだりあたりから本書の主題がよく分からなくなりました。山が高く尖ってる(欠点もある)例も、言いたいことはなんとなく分かりつつも、その欠点なら無くした(谷を埋めた)ほうがいいのでは?と思うような例もいくつか。「長所に目を向ける」「個性を活かす」あたりの考え方は同意なんですが。

Goの静的解析の事始めとしてエラーラップフォーマットのLinterをつくった

こちらのもくもく会も活用して、簡単なLinterをつくりました。

gostaticanalysis.connpass.com

つくったもの

github.com

Goの静的解析の事始めとして、エラーのラップフォーマットの間違いを検知するLinter errwrapfmtをつくりました。

errwrapfmtは文字列リテラルに %w があったらエラーをラップしようとしていると判断して : %w(コロン半角スペースが前方にあるか)というフォーマットになっているかどうかチェックするLinterです。文字列リテラル(正確にはBasicLit)に対してシンプルに正規表現をかけてます。

このLinterをつくろうと思ったきっかけは、xerrorsによるエラーのラップでこのフォーマットを間違うと、スタックトレースが一部冗長になるという問題を知ったからです。

Goではエラーを扱う標準パッケージのerrorsはGo 1.18現在スタックトレースに対応していません。スタックトレースを出力したければpkg/errorsxerrorsを使う必要があります。

xerrors.Errorf()はdeprecatedになりfmt.Errorf()を使うようにgodocに書かれていますが、fmt.Errorf()を使えばそこでスタックトレースは途切れます。スタックトレースを出力したい場合依然としてxerrors.Errorf()を利用することになると思っています。xerrors.Errorf()は : %w でないとラップされたエラーがスタックトレース上で冗長に表示されます。そしてこれはコンパイラでも検知されないため、事前に検知するには静的解析が有効です。

※スタックトレース周りの話は Goでスタックトレースを出力するには - あぼぼーぼ・ぼーぼぼ にもうちょい詳しく書きました。

参考にしたもの

今回はgo/analysisを活用したのでだいぶ考えることが減りました。つくりたいLinterのロジックに集中できましたし、(独自の記法ではあるけど)analysistestでかなり簡単にテスト駆動開発ができるのが良かったです。

たくさんの参考情報をもとに理解を進めつつ、今回つくりたいものに必要最低限な知識は獲得できたかなと思います。具体的にはプログラミング言語Go完全入門 14.静的解析とコード生成がめちゃめちゃ役立ちました。

あとはAST周りだとGoの言語仕様を途中まで読んでいたので、EBNFを理解してNotationが読めるようになっていたのも大きかったです。やっぱり言語仕様に戻ってこれるのは大事です。それからGoAst ViewerでASTの可視化をしてみたりも。

書籍だと『エキスパートたちのGo言語』の第1章に循環複雑度を計測するCLIツールをつくるところがあり参考になりました。今回あえてgostaticanalysis/skeletonを使わずにつくってみましたが、結果的に似たような構成になりました。というのも書籍のなかで実装方法の参考にされていたloopclosure.go(などgo/analysis/passesにある実装)がそういう実装になっているからです。また業務でもお世話になっているstaticcheckなどのhttps://github.com/dominikh/go-toolのコードも参考にしました。

静的解析楽しいぞい

errwrapfmt自体はあとgolangci-lint対応くらいしかやりたいことがないのですが、このくらい簡単なものを作ってみて「静的解析は結構楽しいぞ」と思えたので、またなにか作りながら学習したいですね。

Goでスタックトレースを出力するには

スタックトレース周りについて調べたので放流。

概要

  • Goでエラーを扱う標準パッケージ errors はGo1.18現在スタックトレースに対応していません。Go2では標準パッケージerrorsにスタックトレースがサポートされる予定です(proposal)。それまでの間、スタックトレースを出力するためには別のパッケージを使う必要があります。
  • その場合の有力な選択肢として pkg/errorsxerrors があります。基本このどちらかを使えば良いですが、どちらを使えば良いかは色々な情報があり迷いどころです。これらの特徴を整理して、スタックトレースを扱いたい人が読めばなんとなく何をどう選べばいいかがわかります。

結論

  • フォーマットミスを意識したくない場合はpkg/errors、標準のerrorsパッケージに似たAPIで使いたい場合はxerrorsがおすすめ。
  • errors.New()による生成や、fmt.Errorf()によるラップは、それまでのスタックトレース情報が無くなるためスタックトレースを出力したいなら基本使わない。

スタックトレースを使いたい場合の主な選択肢

準標準パッケージであるxerrorsか、pkg/errorsの2つが一般的な模様。いくつか出典を紹介します。

実用Go言語

Go 1.16では標準ライブラリであるerrors.New()やfmt.Errorf()を使ってエラーを生成した場合、スタックトレースを出力する方法はありません。スタックトレースを出力させたい場合pkg/errorsやgolang.org/x/xerrorsといったライブラリを使う必要があります。

書籍『実用Go言語』p106

プログラミング言語Go完全入門

スタックトレースを付加する - pkg/errors.WithStackすると付加される - pkg/errors.Wrapでも可 - xerrorsでも付加されるがerrorsでは付加されない(Go 1.13)

プログラミング言語Go完全入門 7. エラー処理 @tenten

Gophers Slack

Steve Coffman: So I know that both pkg/errors and x/xerrors have stacktraces, but in the post-Go 1.13 world that didn't bring that along, is there a package that only provides errors with stacktraces? Tim Heckman: I'm not sure I understand the question, because my initial answer are the two packages you identified. Gophers Slackの#newbiesにて

どれを使えばいい?

プロジェクト内で統一されていればどちらでも良いと思います。ただいくつか論点があります。

更新頻度

pkg/errorsは、かつてよりメンテナンスモードに移行することがアナウンスされており、2022年5月現在リポジトリはアーカイブされています。

xerrorsは、エラーのラップに関する機能がGo 1.13で標準パッケージのerrorsやfmt.Errorfに組み込まれたことにより一部機能がdeprecatedになっています。ですが標準パッケージのerrors自体はスタックトレース非対応のままなので、deprecatedな機能も引き続き使うことになります。

pkg/errorsがアーカイブされているならxerrorsを使った方がいいか?というと、そうではないと思います。エラーハンドリング周りは現在特に機能追加の必要性もなくAPIが安定しているため、両方とも安心して使うことができると思っています。

標準パッケージerrorsへの移行コスト

Go 2では標準パッケージであるerrorsにスタックトレースがサポートされる予定です。標準パッケージでやりたいことができそうなら、「繋ぎ」として使うものはGo 2におけるerrorsパッケージへ移行しやすい形だと嬉しいです。

これについては、pkg/errorsがWrap()、WithMessage()、WithStack()、Cause()と割と独自のAPIを提供しているのに対して、xerrorsはerrors同様フォーマットを用いたAPIが基本なのでどちらかといえばxerrorsの方が移行コストが低そうです。ただpkg/errorsもシンプルなAPIなのでそこまで大きな差はないと思います。

使いやすさ・運用しやすさ

Goはエラーハンドリングをたくさん書くので、使いやすさは大事なポイントです。

両者の特徴に関しては Golang Error Handling — Best Practice in 2020 による整理が分かりやすいです。この記事ではpkg/errorsと比較するとxerrorsには次の2つの問題があると言っています。

  1. 基本的な使い方であるフォーマットによるエラーのラップで、フォーマットを間違うとエラーメッセージが変になるし、その間違いがコンパイルエラーにならないので気づきにくい。
  2. エラーのnilチェックが必要

1について、エラーをラップするフォーマットは : %wであり、コロンを忘れたり foo %w 、スペースを忘れて foo:%w と書くと、エラーメッセージに重複が発生します。また、このフォーマットの違いはコンパイルエラーにならないため気づくことが難しく、Linterを自作するなどしないとレビュアーの負担にもなります。

例えばxerrorsを使った以下のコードをGo Playgroundで動かすと、正常にスタックトレースが出力されます。

package main

import (
    "database/sql"
    "fmt"

    "golang.org/x/xerrors"
)

func foo() error {
    return xerrors.Errorf("foo failed: %w", bar()) //ココ
}

func bar() error {
    return xerrors.Errorf("bar failed: %w", sql.ErrNoRows)
}

func main() {
    fmt.Printf("%+v\n", foo())
}

foo failed:
    main.foo
        /tmp/sandbox3601724689/prog.go:11
  - bar failed:
    main.bar
        /tmp/sandbox3601724689/prog.go:15
  - sql: no rows in result set

このコードで ココ とコメントした箇所のフォーマットを "foo failed: %w" から "foo failed:%w" に変更して動かしてみると、以下のようにスタックトレース中のメッセージが変になってしまいます。

foo failed:bar failed: sql: no rows in result set:
    main.foo
        /tmp/sandbox2690898140/prog.go:11
  - bar failed:
    main.bar
        /tmp/sandbox2690898140/prog.go:15
  - sql: no rows in result set

2のエラーのnilチェックが必要について、xerrorsではnilをラップするとエラーを生成しますが、pkg/errorsではnilをラップしようとしてもエラーを生成せずnilを返します。なのでpkg/errorsのほうが場合によってはnilチェックを省略することができます。

// xerrorsを使ったfoo()はラップしようとしたerrorがnilでも新しいerrorを返す
func foo() error {
    return xerrors.Errorf("foo failed: %w", nil)
}

foo failed: %!w(<nil>):
    main.foo
        /tmp/sandbox1009969294/prog.go:10

// pkg/errorsを使ったfoo()はラップしようとしたerrorがnilならnilを返す
func foo() error {
    return errors.WithMessage(nil, "foo failed")
}

<nil>

これらの違いから、僕は冒頭の結論に至りました。

スタックトレースを使いたいなら気を付けること

errors.New()でエラーを生成したり、fmt.Errorf()でエラーをラップすると、それまでのスタックトレース情報が無くなります。

前述したGo Playgroundのコードに出てくるfoo()の中でfmt.Errorf()を使ってラップすると、以下のようにbar()の位置情報が記録されません(fmt.Errorf()を使ったサンプルコード)。

foo failed:bar failed: sql: no rows in result set:
    main.foo
        /tmp/sandbox526836328/prog.go:11
  - bar failed: sql: no rows in result set

また、同様に今度はbar()の中でerrors.New()を使ってエラーを生成した場合のスタックトレースも以下のようにbar()の位置情報が記録されません(errors.New()を使ったサンプルコード)。

foo failed:bar failed:
    main.foo
        /tmp/sandbox3786666462/prog.go:11
  - bar failed

『実践システム・シンキング』を読んだ

www.amazon.co.jp

『なぜあの人の解決策はいつもうまくいくのか?』を読んだ - あぼぼーぼ・ぼーぼぼ で知ったシステム思考について何冊か読んでみたかったので購入。

Kindleのメモ機能とか対応してなかったので残念。内容は1,2,3章がシステム思考の基礎やプロセスの説明が詳細に書かれていて、ベースは『なぜあの人の解決策はいつもうまくいくのか?』による説明と同じですんなり入ってきました。

逆に違うところは、システム思考のプロセスがより詳細で実践的な説明になっています。時間軸分析から始まるのは同じですが、その後「レファレンスモード」「ステークホルダー分析」「変数抽出」と順を追って因果ループ図をつくるための準備が分かるようになってました。因果分析のところも、文章を変数に変換するところなど実践的なプラクティスが多かったです。

4章が架空のケースを追体験する章になっていて、ここが本書で説明されたシステム思考プロセスの復習にもなり一番面白かったです。

5章で定量化に触れられて、ツールの話も入ってきます。僕はループ図書くときに最近は Mindmeister を使っています。個人的にKumuより使いやすくてお気に入り。

『問いかけの作法』を読んだ

www.amazon.co.jp

一人では出せない成果を出すために集合知(集団脳)を活用するうえで「問い」というのが大事そうだ、というのはいくつか本を読んだりしてわかったのですが、本書はその問いを現場で実践する際のプラクティスがたくさん書いてありました。問いの「組み立て」だけではなく、その前後の「見立て」「投げかけ」も含めた「問いかけ」という行為に関する実践的な本です。

実践的な本なので、実際にチームのミーティングで色々試してみています。例えば「パラフレイズ」でチームの中で定義が曖昧になっていそうな言葉にユサブリをかけたり、質問の組み立てで主語を「私たち一人一人」にして方向性を調整したり。いずれにしてもチームのポテンシャルを引き出すためにはどうしたらいいか?を考えチャレンジするのは楽しいもんです。

「問い」だけ大事にしようと思うと、アフターフォローとしてのこの辺りがおざなりになってしまうだろうと、読んでいてハッとしました。

しかし忘れてはならないことは、質問に答えてくれた相手の反応に、ポジティブなフィードバックを返すことです。

(中略)

また、ミーティングの流れのなかでは、深い価値観に迫るような質問など、相手にとって「答えにくい質問」を投げかけざるを得ない場面もあります。そのようなときには、以下の例のように、質問に向き合ってくれたことに対するポジティブフィードバックがとても重要です。

(中略)

質問に対して「良い答え」が得られたときにだけ、質問の内容に対してポジティブなフィードバックをしていると、無自覚なうちにチームはだんだんと「正解」を探すようになり、ファクトリー型のチームに後戻りしてしまいます。 p354

問いかけるのは、正解を引き出すためでもなく、相手を試すためでもなく、相手の個性を引き出してチーム全体の集合知を高めるためだと改めて意識したいですね。

『テスト駆動開発』を読んだ

www.amazon.co.jp

同僚が楽しそうに本書を紹介してくれたので読みました。

3部構成になっていて、第1部は多国通貨、第2部はテスティングフレームワークであるxUnitを題材にTDDのプロセスを体験します。こういう本を読む場合、題材が自分の知識外の知識を必要とするものだと結構読むの大変だったりするんですが、本書の題材はそこまで難解じゃないので、TDDのプロセスに集中することができました。

ひとつ意外だったというか、安心できたのは第1部の最後に出てくる以下の記述。

そもそも多国通貨のコードは書き終わったと言えるのだろうか。答えはノーだ。 (中略)
私は「完了」という言葉を信じていない。完璧さを求めるための手法としてTDDを使うこともできるが、そこはTDDが最も活躍する場所ではない。大きなシステムに関わっている場合、毎日触るような部分は日々鍛えられ、変更も自信も持って行える。システムの周辺部分の、あまり変更しない箇所では、テスト抜けは多いし設計も適当になるが、それでも自信の程度は変わらない。 p250

ここを読んだときに、ぼくの大好きな漫画『BLEACH』に出てくる科学者涅マユリの名言を思い出しました。

完璧であれば、それ以上は無い。
そこに創造の余地は無く、それは知恵も才能も立ち入る
隙がないと言う事だ。
我々科学者にとって、完璧とは絶望だよ。
(中略)
今まで存在した何物よりも素晴しくあれ、
だが、決して完璧であるなかれ。

あと勇気を貰えるのはTDDは「"自分ひとり"でできる"技術"だ」というメッセージですね。

付録CではTDDの歴史を紐解きながら現在の解釈まで書かれていて、本編顔負けの発見に富んだ付録だったと思います。ソフトウェアテストにおけるTDDのカバー範囲を「アジャイルテストの4象限」で説明されていたり、単にテストを先に書く書き方という誤解とか。

『システム運用アンチパターン』を読んだ

www.amazon.co.jp

◯◯アンチパターン本としては『SQLアンチパターン』が思い浮かぶのですが、「システム運用」って結構広いので何が書かれているかわくわくしながら読みました。

広いだけあって、章ごとに結構毛色が変わっていて且つ基本的に独立してるので、1日2章読むみたいに決めて読んでいきました。章ごとに面白くてあっと言う間に読み終わった章、興味が薄いのでサーっと読み進めた章、活かせそうな考え方が多くてメモを取りながらじっくり読んだ章と様々でした。

3章「盲目状態での運用」では、運用の可視化をするうえでプロダクトを理解していないと価値のないメトリクスが集まったダッシュボードができあがったり、文脈のないログが溢れたりするアンチパターンを知ったり。

7章「空の道具箱」では、自動化の取り組みを行う際の主な領域として待ち時間・実行時間・実行頻度・実行のばらつき、の4つが紹介されていました。自分の業務の中から手動でやっている作業を棚卸しして、この4つの観点で考えてみるのは面白そうです。普段漠然と行っていた手動作業に一定の評価観点が追加され、「今自動化し(て)ない理由」が本当に合理的かどうかの判断を助け、自動化を進められる気がします。また、ケネビンフレームワークなどを活用してタスクの複雑さを分類して理解するといったアプローチが紹介されていて、実際に自動化は進めたいがどう優先度を付けていいか分からないとか、タスクの安全性をどの程度担保したらいいか不安だ、といった際に活用できそうです。

10章「情報のため込み:ブレントだけが知っている」では、「意図せず」情報を溜め込んでいる人が組織によって形成されること、ドキュメントを大切に思ってはいるが大切にしていないこと、など耳が痛い話が続きます。ゲートキーパーは聞けば教えてくれるので、良心があり、その行動を評価しがちですが、「その情報を手に入れるために、あなたのチームが提供できるほかの方法はありますか?」と問いを立ててみると前に進めそうです。

11章「命じられた文化」では文化チーフという概念が登場します。

文化チーフとは、組織の文化的価値観を体現する社員のことです。組織の中での階層にかかわらず、会社の中で影響力のある人物とみなされます。チームやグループの感情面でのリーダーとみなされることもあります。 p272

なんだかすごそうな人に見えますが、この観点から組織を見たり自分を見たりするのは良い内省のきっかけになると思います。12章「多すぎる尺度」では優先順位の文脈から目標設定や目標達成のための話も出てくるため、例えば自分の過去半年を振り返って「チームの目標達成を至上命題とする文化チーフとして振る舞えていただろうか」と考えたりするのは効果的でしょう。自分はマネージャーではないですが、マネージャーでなくともチームに関わる話として読み進めると発見があります。

というわけで、章ごとにテーマがほぼ独立していて全体をまとめるのが難しい本ですが、DevOpsの観点から組織や自分の仕事を見つめ直すきっかけに溢れた本だと思いました。