Posts for: #Go

mongodb のトランザクション調査

今日のバドミントン練習はエアシャトルでリフティングを10分、連続最大回数は120回ほど、ショートサーブ30回した。フットワーク練習はお休み。筋肉痛もあったから、みなとのもり公園をジョギングで3周 1.4km、縄跳び15分で1712回だった。縄跳びも2ヶ月ぶり。8月に過去最高が1838回に対して、2ヶ月ぶりにやって2分弱の休憩、十数回のミスがあっても1700回を超えるんやと思って少し驚いた。8月に比べたらいまは涼しいから記録が伸びるのもある。ジョギングで足が筋肉痛になると走るよりも縄跳びの方が負担が小さい。昨日と同じように運動してストレッチして十分にカラダを動かした。

トランザクションとコールバック

お仕事は昨日の続きでリクエスト途中のサービス終了したときに web api ハンドラーの処理が途中でキャンセルされ、そのために内部のデータが不整合になることがわかった。データを不整合にしないため mongodb のトランザクション の仕組みを調べてデバッグしたりしていた。echo のミドルウェアで Unit of work を実装できないかをプロトタイプ実装していた。

基本は次のように callback 関数を渡せばトランザクションに失敗したときにリトライもしてくれて便利なのだけど、いろいろ api サーバーの既存アーキテクチャの都合もあってこれは使えないことがわかった。

session, err := client.StartSession()
if err != nil {
	return err
}
defer session.EndSession(ctx)
result, err := session.WithTransaction(ctx, callback)

テストカバレッジの LT 発表

今日のバドミントン練習はお休み。

LT 会

午後から LT大会 with カメラマン に参加してきた。副業でカメラマンをやろうとしている方が LT 中にプロフィール写真になるようなものを撮ってくれるという。会社紹介などのスライドで使えるかもしれないと思って参加してきた。趣味で4-5年カメラを勉強しているというだけあって、機材や知識も備えていてよかったと思う。LT のネタとして先月お仕事でやっていた go のテスト改善の一環でカバレッジについての紹介をしてきた。サンプルコードは次になる。

終わってから軽く立ち飲み屋で飲んで若い人たちの話しを聞いていた。若い人たちは活気があってやりたいことも多くて、いまの私からみたときのモチベーションの在り方に違いがあるようにも思えた。たまに若い人たちの中に紛れて話しを聞くだけでも自分が失くしたものを気付くきっかけになるようにも思えた。若い人たちはどんどん挑戦してがんばってほしい。

ストレッチ

今日は身体的に疲れているというよりも、開発合宿から続くお仕事のせいか、精神的に疲れていて、2週間ぶりにトレーナーさんと雑談するだけでも気分転換になってよかったと思う。今週はお仕事に集中していてまったく運動していないため、足の張りなどはほとんどなくなっていた。一方でバドミントンの試合をしてこけたときに痛めた腰だけはまだ張りが残っていてなかなか治りきらないことも実感した。今日の開脚幅は開始前149cmで、ストレッチ後155cmだった。

ストレッチを終えてから買いものしてオフィスへ戻って軽く作業していた。

出張前日の資料づくり

今日は1日中オフィスにこもって作業と出張準備をしていたのでバドミントン練習はお休み。夕方にお土産として モロゾフのプリンクッキー を購入するために本店へ行ってきた。これも神戸限定らしい。

パッケージ外のディレクトリにあるテストのカバレッジ計測

先日 深夜にバイナリのビルド・起動調査 をしたのに、最終的にはそんなことしなくてもよかった。デフォルトではパッケージ外のディレクトリにあるテストのカバレッジを計測しないことから普通にはできないと考えていた。しかし、調べたら次の SO で解決法をみつけた。結論としては -coverpkg ./... のように go test で実行している結合テストに対してオプションを指定するだけでよかった。

この調査を終えて最終的な makefile の coverage ターゲットは次のようになった。

coverage: lint
	rm -f coverage.*
	go test -tags=integration -race -cover ./... -coverpkg ./... -covermode atomic -coverprofile=coverage.out
	go tool cover -html coverage.out -o coverage.html

この make ターゲットを gitlab ci/cd から実行して coverage.out/coverage.html を自動生成する。coverage.out があるので必要に応じて好みのツールで統計情報を解析すればよい。たとえば nikolaydubina/go-cover-treemap でツリーマップを作るなら次のように実行する。

$ go-cover-treemap -coverprofile coverage.out > treemap.svg

近況報告の資料作り

普段は週末に報告資料を作っているのにもうやる気が無さ過ぎてお仕事を終えてから作っている。今回はネガティブな内容も報告に入れる。過去の経緯などを調査しながら3時間もあれば一通りのアウトラインはできた。明日がマイルストーンの最終日になるため、明日を終えてからでないと細かい数字は決定しない。マイルストーンの issue を調べたら qa テストでみつけた不具合の issue がそれなりに溜まっているようにみえる。本当は次のマイルストーンで5次開発は完了予定だが、もう1つ増やしてもよいかもしれない。いずれにしても12月/1月に本番導入のための準備もある。今の開発フェーズを完了しても次の開発フェーズには入らないのではないかという見通しもある。

テストライブラリの導入

今日のバドミントン練習はエアシャトルでリフティングを25分した。連続最大回数は277回できた。初めてエアシャトルで200回を超えた。しかも夜なのに。やや高めに打ち上げれば安定的にリフティングできるようになってきた。連続回数が増えることはラケットとシャトルの距離感、ラケットコントロールが上達していることの証左でもあるので200回を超えたときは嬉しかった。

テストライブラリの追加

結合テストを改善するために2つのライブラリを新たに使うよう導入した。

testify は名前だけ知っていたが、実際に使ってみるのは初めて。testify は大きく3つの機能もっている。

  • アサーション
  • テストスィート
  • モック

うちらのチームで使うのはアサーション機能だけになる。モックも将来的に使う可能性はある。アサーションを使うと自分でエラーメッセージを書く手間を省ける。次のようなコードがあった場合、

if expected != actual {
    t.Errorf("expected %d, but got %d", expected, actual)
    return
}

次のようにエラーレポートを testify に委譲できる。このぐらいの利便性でしかないが、テストの規模が大きくなったり、量が増えていけば読み書きのコストを削減できるかもしれない。

if !assert.Equal(expected, actual) {
    return
}

httpexpect はまったく知らなくて初めて使ってみたが、感触がよい。これもデフォルトのエラーレポート機能は testify のアサーション機能を使っているようにみえる。これまでは http リクエストに対して自前のユーティリティ関数と組み合わせて次のようなコードを書いていた。

res, err := doHTTPRequest(body, UserPath, http.MethodGet, "")
if res.StatusCode != http.StatusOK {
    t.Errorf("expected to get %d, but got %d", http.StatusOK, res.StatusCode)
    return
}
var actual entry.User
if err := convertBody(res, &actual); err != nil {
    t.Errorf("failed to convert: %s", err)
    return
}

httpexpect を使うと次のように簡潔に記述できる上にエラーレポートを httpexpect に委譲できる。これはかなりテストコードを読み書きする工数を削減できると思う。

var actual entry.User
e.GET(UserPath).
    WithJSON(map[string]any{ ... }).
    Expect().
    Status(http.StatusOK).
    JSON().
    Decode(&actual)

mongodb のテストデータ管理

今日のバドミントン練習はエアシャトルでリフティングを45分した。連続最大回数は146回できた。調子は悪くなく安定的に30回前後は続くものの、50回を超えたぐらいで失敗してしまう。打ち上げる高さの違いかな?と気付いて少し高めに打ち上げるようにしたら100回を超えた。エアシャトルはラケット面に対して適切な角度でコルクを打たないとあらぬ方向に飛んでいってしまう。高く打ち上げるほど、重力で落ちてくるときにコルクが下を向きやすくなるため、うまくシャトルを打ち上げやすくなる。ラケットコントロールをうまくできれば、エアシャトルをより低い高さでリフティングできるようになるかもしれない。まだ私はそのレベルには満たない。

json からの mongodb にテストデータを追加する

結合テストの改善をしていてテストデータを json で管理したい。これまで go の構造体でテストデータを定義して mongodb の client で insert するといったことをしていた。それも役に立つのだけど、共有のテストデータがどこにあるのか、ソースコードに書いてしまうと時間とともに散らばっていって把握できなくなっていく。テストデータを管理するためのディレクトリを設け、そこに json で記述してどのテスト関数で使うかといったメタ情報も定義できるようにした。次のコードは mongodb に json からデータをインポートするための原理を説明するための疑似コードのようなもの。

go の構造体で定義したテストデータと json で管理するのとどちらがよいかというのは議論の余地はあるし、一概に言えないとは思う。

type testData struct {
	Documents    []bson.Raw `bson:"documents"`
}

func InsertData(
	t *testing.T, client *mongo.Client, b []byte,
) (func()) {
	var data testData
    err := bson.UnmarshalExtJSON(b, false, &data)
	require.NoError(t, err, "failed to get json files: %v", err)

	ctx := context.Background()
	col := client.Database(dbName).Collection("mycollection")
	r, err := col.InsertMany(ctx, docsToInterfaces(data.Documents...))
	require.NoError(t, err, "failed to insert: %v", err)

	return insertResult, func() {
		t.Helper()
	    col := client.Database(dbName).Collection("mycollection")
		for _, id := range r.InsertedIDs {
			filter := bson.D{{Key: "_id", Value: id}}
			if _, err := col.DeleteOne(ctx, filter); err != nil {
				t.Errorf("failed to delete %s: %v", id, err)
				return
			}
		}
	}
}

呼び出し側のイメージ。defer で teardown を呼ぶことでテスト完了時に追加したテストデータを削除してくれる。

teardown := mongotest.InsertData(t, mongoClient, b)
defer teardown()

...

bson.Raw として読み込める json データは bson パッケージのユーティリティを使って dump できる。

func dumpAsJSON(value any) {
	b, err := bson.MarshalExtJSON(value, false, false)
	if err != nil {
		slog.Error("failed to marshal as extended JSON", "err", err)
		return
	}
	fmt.Println(string(b))
}

ひとりではできないこと

今日のバドミントン練習はエアシャトルでリフティングを30分した。連続最大回数は80回だが、ほとんどの試行は20回も続かずに失敗してしまう。難しい。

隔週の雑談

顧問のはらさんと隔週の打ち合わせ。今日の議題はこれら。

課題管理と adr の関係の考察

数年前から adr の話題や導入した会社の記事などをみかけるようになった。私はこれまで課題管理をうまくやっていれば adr はそれほど重要ではないとあまり重視してこなかった。しかし、世の中的に認知されて流行っているものはなにかしら意義があるのだろうと最近は少し見直してきているところもある。このスライドによると2017年から流行り始めたらしい。

  • 開発に関する情報を一元管理する、または全文検索ことにビジネスチャンスはある
    • 検索のニーズや要件は多様で言語によっても違い、技術もまだまだ発展的でもあるからずっとビジネスの種になる気はする
    • gitlab issues のコメントの全文検索は有料機能なので slack 通知したチャンネルを全文検索している
      • 課題管理システムのプラグインでテキストをシステム間連携することで検索システムを別途構築できる可能性がある
      • 検索できなくてもデータのアーカイブをするという視点でもビジネスニーズに応える?
  • ベクトル検索 を検索がまだ一般的にはなっていない?
  • wiki と adr の違いの1つとして wiki になにを書いてよいかわからない問題がある
    • 文章を書けるようになるのは経験や習熟を必要とする
    • adr のようなテンプレートがあることで経験の浅い開発者も書きやすくなる狙いはある
    • 課題管理システムの wiki に adr のラッピングをして別機能としてみせるのはよいかもしれない
    • 業務の引き継ぎにも adr は役に立つのではないか?
    • adr 一覧をみたり、そこから検索することで検索効率や精度は上がるという想定
  • 大きい会社でも巨大な課題管理システムと wiki が1つだけあって一元管理しているという噂は聞く
    • ある会社では wiki は誤っている可能性があるから信用するなという教訓があった
      • wiki の情報は更新されていない可能性があるから参考にしながら必ず裏をとって業務をしなさいという話し
    • wiki を編集したら必ずレビューが必要になるプロセス
  • notion のプロジェクト管理の使い勝手はどうか?
    • テンプレートを使ってガントチャートやカンバンを作れる
      • 基本的に自分でカスタマイズできることの良し悪しがある
        • db をどんどん改良できるが、それだけに魔改造してしまう
        • 時間とともに複数人が改良すると保守できなくなっていく懸念がある
      • 中核機能をパッケージ側で提供することは堅牢性を担保する上で重要ではないか
    • タスク管理と wiki がシームレスなところはよい、はらさんは日報を書いてリンクしている
  • 会社のインフラに日々の開発情報を書いていくと退職後に参照できない
    • フリーランスのような働き方をするにはナレッジデータベースをローカルに作りたいという欲求がある
    • 組み込みの課題管理システムにより、自身のナレッジデータベースをローカルに残すという目的はよいかもしれない

仕事は楽しいかね? の考察

まとめを見返しながら、だいたいの項目は理解または支持できる。1つだけ次の項目が私には欠けていることに気付いた。

毎日1つ試し続ければ必ず上達や進歩ができる

  • 新しいことを始めると、いまやっていることを継続する時間がなくなったりしないか?
  • プロダクト開発ではがんばってもあまり成果が出ないような時期もある
    • リファクタリングやテスト追加などはまさにそう
    • そういったときも1つでも変化をもたらせれば日々の生活が変わるのか?
  • はらさんのお奨めは梅原さんの 1日ひとつだけ、強くなる。 世界一プロ・ゲーマーの勝ち続ける64の流儀
  • 本書を読んではらさんのよかったところは「試してみることに失敗はない」
    • それまでも試すことはやっていたはずなのに躊躇してしまう理由として失敗したら時間の無駄だと考えてしまっていた
    • 失敗したら嫌だと考えてしまうところがあったが、この考え方があるから vision pro を購入できる
    • やりたいことをすぐやってみるという思考につながった
  • 面倒くさいときもなるべくパソコンを使うようにしてなにかしら調べる
  • 私はオフィスにいれば、家よりはなにかしら作業するモチベーションになる
  • 目標達成や成果をあげるには環境がもっとも大事
    • 個人の感情を信頼しない
      • 人間は面倒だとすぐ怠けてやめてしまう
    • 環境を整えることでワークフローを洗練させて習慣化する
      • 日記になにか書かないといけないから調べものをする
      • 嫌々ながらでもやっているうちに習慣になってくるとワークフローが洗練していく
      • 人間の運用を変えていくなにかは普遍的な価値またはビジネスチャンスがある
    • 調子の悪いときにどうやって早く脱却するかが大事
      • ひとりでは言い訳を作ったり怠けてしまう
      • このときに他人の助けがいるのではないか?
        • 話しを聞いてもらうだけでも前へ進むきっかけになる

go の結合テスト向けカバレッジ計測の考察

以前リリースパーティーで go 1.20 で結合テスト向けのカバレッジ計測の機能が入ったことを聞いていた。次のブログ記事でやり方が紹介されている。

将来的に結合テストを作るときにカバレッジ計測のカスタマイズを施したバイナリを使って api server を起動する仕組みにすればこの機能を使えることに気付いた。

まずは単体テストを実行して任意のディレクトリ (tests/coverage) にカバレッジ計測のための中間データを生成する。

$ mkdir -p tests/coverage
$ go test -cover ./... -covermode atomic -args -test.gocoverdir="$PWD/tests/coverage"
$ go tool covdata textfmt -i=./tests/coverage -o coverage.out

coverage.out がさまざまなツールの入力となる統計情報となる。例えば、このファイルを使って次のようにしてソースコードのヒートマップの html を作成できる。

$ go tool cover -html coverage.out -o coverage.html

nikolaydubina/go-cover-treemap を使うと treemap でカバレッジのヒートマップを確認できる。

$ go-cover-treemap -coverprofile coverage.out > treemap.svg

カバレッジ計測向けのバイナリをビルドする。そのバイナリを起動するときに環境変数 GOCOVERDIR に単体テストのカバレッジの中間データが含まれるディレクトリを指定する。

$ go build -cover -covermode atomic -o bin/api ./cmd/api/...
$ GOCOVERDIR="$PWD/tests/coverage" ./bin/api -verbose

このバイナリを使って起動した api server に対してリクエストを呼び出すことでカバレッジを計測してくれる。結合テストからバイナリ起動した api server に対してリクエストしたときに GOCOVERDIR に中間データが追加されていく。結合テストを完了したら最終的なカバレッジの統計情報を生成する。 

$ go tool covdata textfmt -i=./tests/coverage -o coverage.out

textfmt のヘルプをみたら同じディレクトリじゃなくてもよいみたい。

$ go tool covdata textfmt -help
...
Examples:

  go tool covdata textfmt -i=dir1,dir2 -o=out.txt

  	merges data from input directories dir1+dir2
  	and emits text format into file 'out.txt'

練習場所のビル探し

今日のバドミントン練習はリフティングを昼夜あわせて85分 (メイビスで75分、エアで10分) した。フォア持ちとバック持ちを交互に切り替えリフティングで398回続けられた。目標の200回を越せるようになった。

バドミントンの練習場所探し

お昼休みに公園へ行って20分ほど軽く練習した。そのときに連続回数が198回だった。あと2回足りなかったのが悔しかったのと、夜にがんばったらできそうかなという見通しをもっていた。

夜は近所で練習場所によさそうなビルを探してまわってみた。まずは少しおしゃれな区役所のビルの軒下へ行ってみた。

たまたま時間帯が悪かったのか、この場所が風の通り道になっているのか、練習を始めて5分ほどやって風の影響が強くて難しそうだったのですぐに撤収した。人通りが少なく照明もよい感じなのだけど、バドミントンの練習は風が強いとどうにもできない。

付近を散策しながら旧居留地にある別のビルへ。従業員の通用口がやや近いところが懸念。従業員が出てきたときにこいつ何しているの?と思うかもしれない。私の視点からもややパーソナルスペース/練習スペースを狭く感じるところはある。風の影響は受けにくい構造にはなっている。ここで15分ほど練習して267回継続できた。初めて200回を超えた。その後、あまり回数が続かなくなったのでまた散策して別のビルへ。

ここも広くて明るくて夜は施錠しているようにみえる。ここは従業員と会うこともない。よい場所なんだけど、照明が明る過ぎて見上げたときにシャトルと照明が重なってしまうと眩しい。照明が明る過ぎても練習しにくいことに気付いた。ここでも20分ほど練習したのに200回を超えなくて次へ移動する。照明が目に入るせいかもしれない。

最後はホームのビルへ。他のビルで練習してみてホームのビルのよさを実感した。もっとも練習スペースが広い。そして照明を背中側にして練習するスペースも十分にある。そうすると、見上げても照明とシャトルが重なることはないので眩しくて失敗してしまう状況を避けられる。構造的に風の影響も受けにくい。ここで20分ほど練習していたら398回、266回、349回と200回超えを3回できた。

フォア持ちとバック持ちのリフティングをしていて安定的するようになってきた。うまくラケットのスィートスポットで打てば真上にシャトルが上がる。50回ぐらいならほとんど動くこともなく上げられる。だいたい100回に1-2回ぐらい、失敗してシャトルをラケットのフレームに当てたりしてシャトル操作が乱れる。ドタバタしてリカバリする。これまではそのときに焦ってしまってシャトルを落とすことが多かった。なぜ200回以上続くかというと、その稀に失敗したときのシャトル操作をリカバリできるようになってきたから。つまり5回ぐらいミスしたときにリカバリできれば200回は続くということになる。これはメイビスフィールドのシャトルを使ったときの話し。

次にエアシャトルを使って同様にリフティングをしてみると最高で50数回ぐらいしか続かない。これはラケットのフレームまたはフレーム近くのガットで弾いてしまうと、コルク部分がプラスチックなために反発せずに落としてしまったり、あらぬ方向へ飛んでいったりしてリカバリがとても難しい。つまり、ラケットコントロールをうまくやらないとエアシャトルでリフティングを継続するのはメイビスフィールドよりもずっと難しい。次の目標としてはエアシャトルで200回を越せるようにラケットコントロールの練習をするのがよいように思える。

テストとビルドタグ

go ではテスト用途のパッケージを httptestiotest といった、通常アプリケーションとしてパッケージを提供しているものもある。しかし、通常のパッケージにしてしまうと、アプリケーションをビルドしたときにテストコードもバイナリにも含まれてしまう。依存パッケージの管理やバイナリサイズを減らす上で不要なコードはビルド対象外になる方が望ましい。自分たちが書いたソースコードがアプリケーションに含まれることはサイズの視点では問題ないが、テスト向けの依存パッケージもアプリケーションに含まれると意図せずバイナリサイズが大きくなってしまったり、パッケージの依存解決に時間がかかったりしてしまう懸念がある。そのため、ビルドタグを用いてテスト用途のパッケージはテストのときしかビルドしないように制御する。

例えば integration というビルドタグを設ける。テスト用途の util パッケージを定義するときは次のようにソースコードを記述する。

//go:build integration

package util

ビルドタグを指定せずに結合テストを実行しようとすると次のようなエラーが発生する。

$ go test ./tests/...
# example.com/tsets/mypackage
package example.com/tsets/mypackage_test
	imports example.com/tests/util: build constraints exclude all Go files in path/to/tests/util
FAIL	example.com/tests/mypackage [setup failed]

結合テストを実行するには次のように明示的にビルドタグを指定して、テスト用途のパッケージをビルドしてテストが実行されるようにしないといけない。

$ go test -tags=integration ./tests/mypackage/...

非同期処理のキャンセルと中断待ち

バックエンドで非同期のバッチ処理を実装してサーバーが shutdown したときに実行中のバッチ処理も安全に止めないといけない。

context でキャンセルするのは簡単だけど、非同期で動いている goroutine が中断してから終了したことを main プログラム側で待つ同期をどう実装しようか悩んでいた。たまたま 【Go】Contextの魅力を感じる の記事をみたら sync#WaitGroup と組み合わせたサンプルコードがあって、これまでも sync#WaitGroup を使ったコードを何度も書いてきて知っていたはずなのに context と組み合わせるという発想はなかったなと気付きを得た。それからライブラリとして汎用のバッチ処理の controller を実装した。

type Controller struct {
	ctx    context.Context
	cancel context.CancelFunc
	wg     *sync.WaitGroup
	mu     sync.Mutex
}

func (c *Controller) Register() (context.Context, *sync.WaitGroup) {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.wg.Add(1)
	ctx := context.WithValue(c.ctx, ctxKey{}, 0)
	return ctx, c.wg
}

func (c *Controller) Stop() {
	c.cancel()
}

func (c *Controller) Wait() <-chan struct{} {
	c.mu.Lock()
	defer c.mu.Unlock()
	quit := make(chan struct{}, 1)
	go func() {
		c.wg.Wait()
		slog.Debug("completed to wait group in batch.Controller")
		quit <- struct{}{}
	}()
	return quit
}

func NewController(parent context.Context) *Controller {
	ctx, cancel := context.WithCancel(parent)
	return &Controller{
		ctx:    ctx,
		cancel: cancel,
		wg:     new(sync.WaitGroup),
	}
}

parent の context でキャンセルさせることもできるし、この controller の api からキャンセルさせることもできる。

この controller を使うアプリケーションのコードは次のようになる。

c := batch.NewController(ctx)

func() {
	ctx, wg := c.Register()
	defer wg.Done()

    for {
	    // do something
        select {
        case <-ctx.Done():
            slog.Debug("canceled", "err", ctx.Err())
            return
        default:
        }
    }
}()

timer := time.NewTimer(10 * time.Second)
select {
case <-timer.C:
	slog.Debug("expected to complete waiting, but occur timeout")
case <-c.Wait():
	slog.Debug("completed to wait")
}

context.Done() に習って Wait() が channel を返すことで timer と組み合わせて、バッチ処理の中断を待たずに終了するといった制御もできる。後になってふりかえると、timer の制御も Wait() メソッドに入れてしまってもよい気もするが、それは呼び出し側の要件によって変わってくるからこのままの方がライブラリのコードの見通しがよくてよい気もする。朝から設計して、昼間に実装して夕方にテストを書いて、帰りの新幹線でアプリケーションに組み込みしていた。1日でちゃちゃっと作ったわりにはよく出来たと思う。

excel が utf-8 で保存する csv ファイルは bom 付き

昨日の定例会議で csv ファイルのエンコーディングの話しになって excel 2016 以降では utf-8 で csv ファイルを保存できるようになった。もう cp932 対応しなくてよいといった話題が出た。そのときにうろ覚えだったものの、excel は byte order mark (bom) を付けていた気がして、実際にファイルを作ってもらったらそうだった。bom の有無は file コマンドや od コマンドでなどで調べられる。

$ file book1.csv
book1.csv: Unicode text, UTF-8 (with BOM) text, with CRLF line terminators

ファイルの先頭に 0xef 0xbb 0xbf の3バイトがつく。

$ head -1 | od -t x1 book1.csv
0000000    ef  bb  bf  68  65  61  64  65  72  31  2c  68  65  61  64  65
0000020    72  32  2c  68  65  61  64  65  72  33  2c  68  65  61  64  65
...

印字可能な文字ではないため、テキストにするとわからないが、byte 列だと bom が付いているのがわかる。16進数の 0x68 が ‘h’ になる。bom がついていると header1 という文字列比較したときに別の文字列になってしまうので取り除かないといけない。

文字列: 'header1'
byte列: 'efbbbf68656164657231'

いろんな対応方法があると思うが、so の回答 でみつけたこの方法がコードの見通しもよくて気に入った。次のように io.Reader をラップするようなコードになる。

func newBOMAwaredCSVReader(reader io.Reader) *csv.Reader {
	transformer := unicode.BOMOverride(encoding.Nop.NewDecoder())
	return csv.NewReader(transform.NewReader(reader, transformer))
}

Transformer という仕組みがあって、準標準ライブラリの golang.org/x/text/encoding で実装されている。so の回答をみただけでコードを追加するのもよくないかと思って unicode#BOMOverride が返す bomOverride transformer のコードを読んで把握した上でテストを書いてマージリクエストを送った。実際の変換処理は次のようなもの。

  • fallback として encoding.Nop を使う
  • 最初に呼ばれたときに d.current (fallback) をセットして、2回目以降は fallback が呼ばれる
  • 与えられた byte 列が 2byte 以上のときに bom のチェックを行う
  • bom があるときはそのバイト数を読み飛ばして d.current (fallback) で変換する
func (d *bomOverride) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
	if d.current != nil {
		return d.current.Transform(dst, src, atEOF)
	}
	if len(src) < 3 && !atEOF {
		return 0, 0, transform.ErrShortSrc
	}
	d.current = d.fallback
	bomSize := 0
	if len(src) >= 2 {
		if src[0] == 0xFF && src[1] == 0xFE {
			d.current = utf16le.NewDecoder()
			bomSize = 2
		} else if src[0] == 0xFE && src[1] == 0xFF {
			d.current = utf16be.NewDecoder()
			bomSize = 2
		} else if len(src) >= 3 &&
			src[0] == utf8BOM[0] &&
			src[1] == utf8BOM[1] &&
			src[2] == utf8BOM[2] {
			d.current = transform.Nop
			bomSize = 3
		}
	}
	if bomSize < len(src) {
		nDst, nSrc, err = d.current.Transform(dst, src[bomSize:], atEOF)
	}
	return nDst, nSrc + bomSize, err
}

自分で実装してもなにも難しくないけど、すでに実績のあるコードがあるならそれを再利用した方が保守コストを削減できる。

同期飲み会

新卒入社した会社の同期との飲み会。2ヶ月前と同じメンバー で飲んできた。4人で飲む予定が、また1人が障害対応でドタキャンになったから3人で飲んでた。日本酒原価酒蔵 という、おいしい日本酒を原価で提供するお店があるみたい。プレミアム飲み放題プランだったのでよいお酒をいろいろ飲み比べできた。どれもおいしかったし、ちょっとずついろいろ飲み比べできておもしろかった。飲み放題メニューに神戸のお酒がなかったのがよいことなのか残念なのか。前半は辛口の日本酒を飲んで、だんだん酔っ払って味がわからなくなるから、後半にコクや甘みの強い日本酒を飲むとおいしく飲めると教えてもらった。本当にその通りで日本酒の飲み方のよい勉強になった。お店で飲んだお酒のカードがもらえる。酔っ払って忘れてしまっても後で思い出せる。

特徴的なお酒で記憶に残っているもので 三井の寿 がスラムダンクに出てくる三井寿に由来して命名されていて、その背番号である14番と同じアルコード度数14度、日本酒度 (辛口甘口の度合いを表す) +14 と意図的?にあっているとのこと。日本酒の中ではトップクラスの辛口らしい。たしかに過去に私が飲んだことがないキレだった。もう1つは 讃岐くらうでぃ という、カルピスのような風味に近い珍しいお酒。甘いので後半に飲むとよい。うちらは酔っ払って訳がわからなくなってきたときに口直しのように飲んでた。そういう飲み方をするものかどうかはわからないが、飲みやすいのでついつい飲んでしまい、さらに酔ってしまう典型的なお酒。同期飲みは記憶をなくす感じで飲む。この歳になってこんな飲み方する相手いないなと思うとまた楽しい。

pipe による関心の分離

go の Pipe は go 言語プログラミングのよいところをいくつか同時に実感できる、実用度の高いユーティリティだと思う。go 言語は writer や reader をインターフェースとして定義している。細かいメソッドの違いでいくつかの writer/reader インターフェースを区別していたりもするが、基本的には write/read のメソッドをもっていればよい。

type Writer interface {
	Write(p []byte) (n int, err error)
}

type Reader interface {
	Read(p []byte) (n int, err error)
}

この汎用的な writer/reader のインターフェースに従っていれば、さまざまな要件に対して抽象化ができる。オンメモリでデータを扱うこともあるし、OS のファイルシステム上のファイルとして扱うこともあるし、オブジェクトストレージ上のオブジェクトとして扱うこともある。そういった具象オブジェクトの如何によらず writer/reader のインターフェースに従うことでデータ処理とリソース管理を分離できる。これはプログラミングパラダイムにおける 関心の分離 と呼ばれていることを実現する。

バッチ処理において複数データを読み込みながら処理を行い、処理中にエラーが発生したら、そのエラーデータを特定のファイルに書き込みしたい。エラーが発生したときだけ特定ファイルを生成する。そういった要件は次のように実装できる。

func write(w io.WriteCloser) {
	defer w.Close()
	/*
		if true {
			return
		}
	*/

	records := [][]string{
		{"first_name", "last_name", "username"},
		{"Rob", "Pike", "rob"},
		{"Ken", "Thompson", "ken"},
		{"Robert", "Griesemer", "gri"},
	}

	cw := csv.NewWriter(w)
	for _, record := range records {
		if err := cw.Write(record); err != nil {
			log.Fatalln("error writing record to csv:", err)
		}
	}
	cw.Flush()
	if err := cw.Error(); err != nil {
		log.Fatal(err)
	}
}

func read(r io.ReadCloser) {
	rr := bufio.NewReader(r)
	line, _, err := rr.ReadLine()
	if err == io.EOF {
		return
	}
	/*
		writer, err := os.Create("./test.txt")
		if err != nil {
			log.Fatal(err)
		}
		defer writer.Close()
	*/
	writer := os.Stdout
	if _, err := writer.Write(append(line, []byte{'\n'}...)); err != nil {
		log.Fatal(err)
	}
	if _, err = io.Copy(writer, rr); err != nil {
		log.Fatal(err)
	}
}

func main() {
	r, w := io.Pipe()
	go write(w)
	read(r)
}

Pipe を使うことで読み込みの処理と書き込みの処理を分離できる。ここでいう読み込みはバッチ処理、書き込みはエラーデータの保存処理に相当する。普通に実装すると、バッチ処理中にエラーデータの保存処理も記述することになる。業務のコードだと本質ではないリソース管理が煩雑になってしまう。2つの処理に分割するため、どちらか一方は非同期で実行する必要がある。ここで goroutine のシンプルさが際立つ。さらに pipe を使えば、reader 側は writer が Close するまでブロックするから終了処理の同期も自然なコードで実現してくれる。

非同期/並行処理の難しいところを go のインターフェースによる抽象化、簡潔な goroutine 記法、pipe に隠蔽された自然な同期処理の3つで簡潔に表現している。複雑さに対してこれほど簡潔なコードはそうそうないと思う。このコードを他の言語で実装しようとしたらもっと複雑になるはず。このサンプルコードとともに若いメンバーにリソース管理のリファクタリングをコードレビューで指摘したものの、そのメンバーには理解できなくて実装できなかった。そして、その処理を私が作り直すことになったというのが、今日のお仕事だった。本当は難しい非同期/並行処理をいくら簡潔にしても、その経験がないと人間には理解できない。まさに本質的複雑さを表しているように思える。

責任を扱うコミュニケーションの在り方

水曜日から継続していたメンバーのコードレビューをお昼頃に完了して、それからは自分の時間に集中して開発に取り組めた。途中、晩ご飯休憩もしながら翌2時頃までコードを書いていて issue を2つ fix した。

隔週の雑談

顧問のはらさんと隔週の打ち合わせ。ここ1-2ヶ月はあまり議題がなくて近況について雑談した。いまの開発フェーズが終わらないと、私が開発以外にリソースを割いていないので他のことに取り組む余裕はないかもしれない。開発プロジェクトにおける、議題の1つで次のインタビュー記事の内容について雑談した。

責任をすぐに相手に投げてしまわないこと。責任をこちらで負えるように日頃から信頼貯金を貯めていくことが大事です。責任を負うこと自体はたいへんだけど、仕事は楽になるんです。

でも、人は責任を負いたくないと考えてしまうんですね。気持ちは分かりますよ、責任を負うのには胆力が必要ですから。そして責任を負いたくないから「いつまでですか?」と聞いてしまう。その途端、スケジュールを決めるのは相手の裁量になる。責任から降りて、自分から立場を下にして、受発注の関係にしてしまっています。

何が事業貢献なのか分からなくなっていた伊藤直也さんが再認識したユーザーエクスペリエンスへのコミット

私自身、このインタビュー記事を読むまで相手に仕様や納期を決めてもらうことを、相手に責任を負わせるという視点をもっていないかった。しかし、その通りでもあると新たな気付きを得た記事でもあった。うちらはプログラマーという、システムを作る専門家なのでビジネス課題や解決したい業務課題について、その課題意識をもっている人 (ビジネスオーナー) から教えてもらうのを至極当然のように考えてしまう。そして、それ自体はいまの業務の在り方としてそうならざるを得ないところもある。

システム開発はそれぞれの専門職が分業によって行う。とくに規模が大きくなればそうなる。この構造そのものは一般的といえる。しかし、一緒にプロジェクトをやっていく働き方や考え方によってはその責任を分散させられるというのがこの記事に書いてあることだとわかる。そして、私自身これまで意識せずにそういった行動をいくらか取ってきているところもあり、それは「気付き」のレベルによって起こるものだとずっと考えていた。気付くからより多くの、より本質的な、より優れた改善のための行動ができる。そして、私の考え方も間違ってはいないと思うが、私は他人よりリスクを取りがちな性格があるから責任をこちらで負うという意識をあまりもっていなかった。つまり、私は自分の価値観でこうしたいと思って勝手にやっていたことを、別の見方では相手から責任を受け取って進めているという解釈もできる。

ちょうど 兵庫県知事の百条委員会の答弁 もみていて感じたことだが、私はこういったコミュニケーションを本質的に好んでいない。自分の責任ではないという議論をしても、モノゴトは前に進まない。世間では百条委員会の後も知事の説明は十分ではなかったとみられている。その所以である。誰かが責任をもってモノゴトに取り組む必要がある。その責任の所在を明確にすることも大事ではあるが、自身の責任ではないという主張だけでは物足りなさを感じる。普段の業務においてもそういう姿勢やコミュニケーションを取る人が少なからずいる。はらさんの経験でもその点においては同意していた。よくある状況だと思える。自身に責任を負うことをためらわないコミュニケーションを取る人とそうではない人の2通りがあるのだと気付いた。そして、私は後者の人とコミュニケーションを続けていると疲弊したり苛々したりすることがある。それゆえに私自身も結果に対して潔くあろうと努めるし、潔い人たちとウマがあうのだろうともわかってきた。

課題管理の文脈においては、コミュニケーションのやり取りから責任の綱引きがどのような場所でどのぐらい起きるのか。人間であれば読み取れるが、ai はその意図を解釈できるか。そういった業務の責任という概念を見える化することに意味はあるかもしれない。責任の押し付け合い、または責任分散、本質的にどうあるべきだったかをなんらかの指標をもって数値化できればおもしろいのではないかと思えた。

go における簡単な式の評価

テスト自動化のツールを作っていて、テストデータでちょっとした式の評価をやりたくて調べたらまさに次の記事で解決した。

この記事では go/types パッケージに定数や式の評価を行う機能がその使い方が紹介されている。簡単に使える。ふとサードパーティのパッケージならどうなるんだろう?とインターネットを検索したものの、自分ではみつけられなかった。chatgpt に問い合わせたら次のようなコードを紹介してくれた。そして、たしかにほとんどは正しくて意図したように動いた。次のコードでは http フレームワークの echo の定数を参照している。types.Package を生成するためのモジュールの読み込み方法について標準ライブラリはそのためのユーティリティが用意されているが、サードパーティのパッケージは用意されていなかった。

import (
    "fmt"
    "go/token"
    "go/types"
    "golang.org/x/tools/go/packages"
)

func eval(expr string) (types.TypeAndValue, error) {
	cfg := &packages.Config{
		Mode: packages.NeedTypes | packages.NeedImports,
		Fset: token.NewFileSet(),
	}
	pkgs, err := packages.Load(cfg, "github.com/labstack/echo/v4")
	if err != nil {
		return types.TypeAndValue{}, err
	}
	if len(pkgs) == 0 || pkgs[0].Types == nil {
		return types.TypeAndValue{}, fmt.Errorf("failed to load echo package")
	}
	mainPkg := types.NewPackage("main", "main")
	mainPkg.Scope().Insert(types.NewPkgName(token.NoPos, mainPkg, "echo", pkgs[0].Types))
	return types.Eval(
		cfg.Fset,
		mainPkg,
		token.NoPos,
		expr,
	)
}

このコードで expr に次のように echo の定数を指定するとその値を参照できる。

echo.MIMEApplicationJSON

動的型付けのノリで関数も実行できたりするのかな?と試してみたら (当たり前だが) できなかった。chatgpt になぜ関数実行できないかを尋ねたら次であるとのこと。静的型付けのコードを実行するには、本来コンパイルしないといけないのだから式の評価よりもずっとやることがある。

packages.Load を使用してサードパーティパッケージをインポートできているにもかかわらず、expr からそのパッケージの関数を呼び出しても結果が取得できない原因は、go/types パッケージがサポートしているのは、型や定数のチェック、構文解析、式の評価であって、関数の実行そのものはサポートされていないためです。

go/types の Eval 関数はあくまでコンパイル時の型検査や式の評価を行うもので 関数の実行や実行時の評価 (ランタイムの処理) は行いません。これは、Eval が式を評価して型と値を返すだけであり、動的な関数の実行などはできないためです。