イベントログに書き込む

1時に寝て2時ぐらいから吐き気で3時間ほど苦しんで寝て9時に起きた。バテてきた。10時過ぎからオフィスへ行ってだいたい丸一日お仕事してた。

go のマルチプラットフォーム対応

いま作っているモジュールは windows/linux の両方で動くモジュールにする必要がある。go は標準でそういったモジュールの開発に対応していて Build Constraints にビルドタグの仕様が書いてある。例えば、ログ出力を linux では標準出力に windows ではイベントログに書き込みたいとか。windows のイベントログに書き込むのも準標準パッケージである golang.org/x/sys パッケージを使ってすぐにできた。私がイベントログの仕様をよく知らないのでイベントログそのもののインストール作業をどうするかという課題がある。これは詳しい人に教えてもらおう。ビルドタグには unix という設定が使える。これで linux/macos 対応になる。例えば、log 出力のインターフェースをそれぞれ分けたいときに次のように実装できる。windows は標準ロガーに加えてイベントログにも書き込むようにしてみた。この log パッケージを使う呼び出し側のコードはマルチプラットフォームの違いを意識する必要はない。とても簡単にマルチプラットフォームのコードを管理できる。go のビルドやコンパイラの仕組みを学ぶたびに感心する。

//go:build unix

package log

func Error(msg string, fields map[string]any) error {
	return mylogger.Error(msg, fields)
}

func Warn(msg string, fields map[string]any) error {
	return mylogger.Warn(msg, fields)
}
//go:build windows

package log

import (
	"golang.org/x/sys/windows/svc/eventlog"
)

const (
	// this logName should be installed in advance
	logName = "mylog"
	eventID = 1 // TODO
)

var elog *eventlog.Log

func init() {
	var err error
	elog, err = eventlog.Open(logName)
	if err != nil {
		mylogger.ErrorExit(err)
	}
}

func Remove() {
	eventlog.Remove(logName)
}

func Error(msg string, fields map[string]any) error {
	elog.Error(eventID, getMessage(msg, fields))
	return mylogger.Error(msg, fields)
}

func Warn(msg string, fields map[string]any) error {
	elog.Warning(eventID, getMessage(msg, fields))
	return mylogger.Warn(msg, fields)
}

お土産探し

いつもは東京出張のタイミングにあわせてお店やお菓子探しから時間をかけて、自分でも買って試食してみて選択している。今回は2週間前から土日もずっと開発していて余裕がない。前回は 本髙砂屋の金つば を持って行った。本髙砂屋は金つばの老舗なのだけど、洋菓子も販売していて和菓子のお店の隣に洋菓子のお店も構えている。 今回も本髙砂屋にあやかって洋菓子の エコルセ の季節限定で売っている詰め合わせバージョンを購入してみた。「貧すれば鈍する」とよく言ったものでお土産探しの時間すらかけられなくなってしまっている。15時半頃から出かけて本髙砂屋さんで購入した。今回は試食できていないけど老舗だからあまり心配はいらないとは思う。

http クライアントのリトライ機能

1時に寝て7時に起きた。今週末もお仕事をがんばる。今週中に開発タスクを完了させたいという思いがある。まだ今週は続いている。

ストレッチ

今日の開脚幅は開始前154cmで、ストレッチ後157cmだった。今週も座っている時間が長かったせいか、あまり数値がよくなかった。ここ2-3週間前から右すねの外側の筋に張りがあったが、これが左にも転移して左すねの外側の筋も張りが強くなってきた。両方とも張りがあるという意味ではバランスが取れたと言えるのかもしれない。いずれにしてもいままで張りがなかったところに違和感があるので引き続き注視していきたい。歩くときにやや気になるけれど、日常生活に支障が出るほどの障害ではない。少し前からオフィス通勤に自転車をやめて、通勤とお昼ご飯を買いに行くのが自転車から歩きになったことが影響しているのかな?言うても徒歩3分ぐらいの距離でそんなこと起きるかなぁと思ったり。

http クライアントのリトライ機能

結論から言うと go-retryablehttp を使ってすぐできた。自分で作ってもよかったのだけど、retryablehttp がシンプルで私の求めている機能そのものにみえたのでこのライブラリを採用した。既存の http クライアントの機能を次の2箇所変更するだけで retryablehttp が使える。こういうインテグレーションの簡単さも嬉しい。ちゃんとバックオフとジッターの機能も実装しているし、デフォルトのそれらの機能をカスタムで置き換えることもできる。最低限のリトライ機能があればいいといった http クライアントならこれでよいと思う。こういうシンプルで使い勝手のよいライブラリをみつけると嬉しくなる。

-	res, err := c.raw.Do(req)
+	_req, err := retryablehttp.FromRequest(req)
+	if err != nil {
+		return []byte{}, fmt.Errorf("failed to create retryable request: %w", err)
+	}
+	res, err := c.raw.Do(_req)
+func NewRetryableClient(cfg config.Client) *retryablehttp.Client {
+	c := retryablehttp.NewClient()
+	c.Logger = log.GetLogger()
+	c.RetryWaitMin = cfg.RetryWaitMin
+	c.RetryWaitMax = cfg.RetryWaitMax
+	c.RetryMax = cfg.RetryMax
+	return c
+}

-		raw: http.DefaultClient,
+		raw: NewRetryableClient(cfg),

まずまずの開発進捗

1時に寝て何度か起きて7時に起きた。寝たと思うけど、あまり記憶にない。

勉強会は初のお休み

11月から5ヶ月続けてきた毎週の勉強会を今日はお休みすることにした。理由は先週から私が開発にずっと張り付いていて勉強会の段取りをする余裕がないから。勉強会をやってくれと依頼されているわけではない。私が勝手に段取りを組んでずっと継続していきた。続いていると途切れさせたくないインセンティブもあって5ヶ月の間ずっと続いていた。それが一旦途切れてしまった。クロッゾも言っているように、自分勝手な論理を振り回さず、いまチームにとって大事なことをするという側面では、私が開発に張り付くのが悪いわけでもない。今週中に開発を終了させるという目標に全力を尽くすために今週の勉強会はお休みとした。

ああ。これも魔剣と同じ、意地と仲間をはかりにかけるのはやめた by ヴェルフ・クロッゾ

コードレビューしつつ開発

メンバーのコードレビューを合間にしつつ、あるモジュールの開発も進める。windows/linux の両方で動くモジュールを作らないといけないのでビルド環境の考慮や ci/cd、検証もやや工数がかかる。将来的な拡張や開発の分散も考慮にして2つのリポジトリに分割してモジュール化したものを組み合わせる。今日の時点で Active Directory における sAMAccountName と LDAP の DN との変換処理以外は一通り動くものを実装できた。エラー制御や耐障害性のための仕組みはまだこれから実装することになるが、それは後回しにしても、いまはクリティカルパスが私の開発にならないよう、テストや検証の作業をブロックしてしまわないよう、段階的に機能を作り込んでいけるように考えている。あと土日の2日間を使って Active Directory の処理を実装できればクリティカルパスからは脱することができる。

Active Directory のキーワードで検索していると go-adsi/adsi をみつけたので明日はこのライブラリと背景の調査をしていく。感覚的には2日もあれば十分だろうと楽観的にみているが、実際にはどうだろう?

zeromq のメッセージングを go でやり取りする

0時に寝て何度か起きて7時に起きた。たぶんよく眠れたと思う。

zeromq のライブラリ選定

zeromq のクライアントをサンプル的に実装している。windows と linux の両方で使う予定なので pure go 実装の go-zeromq/zmq4 を使ってみることにした。zeromq のライブラリはいくつかあるが、大きく2つに分けられる。

  • c 言語の zeromq ライブラリのラッパー
  • zeromq のプロトコルを pure go 実装

c 言語のライブラリのラッパーだとビルド環境をプラットフォームごとに用意しないといけない。pure go ならクロスコンパイルも簡単。例えば、linux 上で windows のバイナリをビルドするには次のようにする。go ならたったこれだけでよい。

GOOS=windows GOARCH=amd64 go build -o bin/myapp.exe ./cmd/myapp/main.go

但し、zeromq の pure go 実装は開発があまり活発ではないし production ready でもない。まだまだベータ版というか、stable な実装にはなっていないようにみえる。うちの用途はとてもシンプルなメッセージングに使うだけなので動けば問題ないだろうという想定で最初の選択肢として go-zeromq/zmq4 を試してみる。これはまだチュートリアル的に動かしているレベルなのでコードが誤っているかもしれないが、ひとまずメッセージのやり取りができるところまで確認した。windows でも動く。これから1ヶ月ほどかけて実運用レベルのデータやテストを実施して本当に使えるかどうかの検証をしていく。

メッセージを送る側は Push というソケットを使う。送信は chan を使って非同期に送る必要性はないのだけど Pull にあわせて汎用性をもつ実装にするとこんな感じかな。

func Push(
	ctx context.Context, cfg config.Queue, msgCh <-chan zmq4.Msg,
) (<-chan error, error) {
	push := zmq4.NewPush(ctx, zmq4.WithDialerRetry(time.Second*3))
	if err := push.SetOption(zmq4.OptionHWM, cfg.SendHWM); err != nil {
		return nil, fmt.Errorf("failed to set socket option: %w", err)
	}
	endpoint := "ipc://" + cfg.Path
	if err := push.Dial(endpoint); err != nil {
		return nil, fmt.Errorf("failed to dial: %w", err)
	}
	if addr := push.Addr(); addr != nil {
		return nil, fmt.Errorf("dialer with non-nil addr")
	}

	errCh := make(chan error, messageChanSize)
	go func() {
		defer func() {
			push.Close()
			log.Debug("push queue was closed", nil)
			close(errCh)
		}()
		for msg := range msgCh {
			if err := push.Send(msg); err == nil {
				errCh <- nil
				continue
			} else if errors.Is(err, context.Canceled) {
				log.Info("push queue is closing ...", map[string]any{
					"err": err,
				})
				return
			} else {
				errCh <- err
			}
		}
	}()
	return errCh, nil
}

メッセージを受け取る側は Pull というソケットを使う。Recv() でキューからメッセージの到着をブロックする。context をキャンセルすると Recv() が即時でエラーを返すので終了処理も制御しやすい。

func Pull(ctx context.Context, cfg config.Queue) (<-chan zmq4.Msg, error) {
	pull := zmq4.NewPull(ctx)
	endpoint := "ipc://" + cfg.Path
	if err := pull.Listen(endpoint); err != nil {
		return nil, fmt.Errorf("failed to listen: %w", err)
	}
	if addr := pull.Addr(); addr == nil {
		return nil, fmt.Errorf("listener with nil addr")
	}

	ch := make(chan zmq4.Msg, messageChanSize)
	go func() {
		defer func() {
			pull.Close()
			close(ch)
			log.Debug("pull queue was closed", nil)
		}()
		for {
			log.Debug("-- waiting messages ...", nil)
			msg, err := pull.Recv()
			if err == nil {
				ch <- msg
			} else if errors.Is(err, context.Canceled) {
				log.Info("pull queue is closing ...", map[string]any{
					"err": err,
				})
				return
			} else if err == io.EOF {
				log.Debug("got EOF", nil)
			} else if err != nil {
				log.Error("failed to recieve", map[string]any{
					"err": err,
				})
			}
		}
	}()
	return ch, nil
}

rocky linux も悪くない

2時に寝て7時に起きた。夜うまく眠れなくて夜更かしした。3月になってしまったか。2月の記憶がほとんどない。

rocky linux をベースイメージとして使う

コンテナを作るときのデフォルトの os は、私の中では alpine だが、依存ライブラリの制約があって、いま作っている docker image は rockylinux をベースイメージに使う。rockylinux を docker pull した感覚は速いのでそんな大きなイメージでもない。minimal だと40数 MiB 程度のサイズ。すでに成果物として rpm パッケージがあり、その rpm が依存解決できないといけないため、rhel ベースの os をベースイメージにしないといけない。rpm パッケージに systemd の起動スクリプトなども入っているのでそのまま使えばいいかとも思ったのだけど普通には systemd は動かない。rockylinux のドキュメントにも systemd はこうやったら動くよと書いてあるものの、実際に起動してみるとエラーになる。細かくは追いかけていないけれど、cgroup v2 で未解決な問題があるらしく、cgroup v2 を使うカーネルでは systemd が動かないらしい。

コンテナを動かすプラットフォームがハイパーバイザーも兼ねるので systemd を動かす必要性はないけれど、サードパーティの起動スクリプトを自分たちで保守するのも嫌だなという印象はあるのでもしかしたら悩ましい問題なのかもしれない。

開発の追い込みに集中

23時に寝て2回ほど起きて7時に起きた。土日コードを書いて疲れていたので月曜日は早めに業務を切り上げて実家のいろいろをやっていた。

開発の追い込み

サーバーサイドとフロントエンドはほぼ開発が完了し、これからテストするだけといきたいところが、想定外のことがあってまだそうはなっていない。

予想外のことは起こるものだ。ガトーは良くやっている by エギーユ・デラーズ

と言っているほどの、予想外というわけでもないが、思った通りに進捗しなくてリリースの危機を迎えている。なにが起ころうとプロジェクトの全責任はマネージャーにある。いま溜まっている issue をメンバーに再分配して乗り切ろうと定例会議で話した。私が2人分ぐらいの作業をすれば1週間もあれば取り戻せる程度の遅れではある。ただ残り時間が1ヶ月しかないだけ。また週末働いてその足で東京に行くのだろうなと直近の未来を想像していた。

上司道 野村監督から学ぶリーダーの器のつくり方

お仕事にテンパっているものの 第86回上司道 野村監督から学ぶリーダーの器のつくり方 に参加した。上司道 に参加するのは2回目。

以前 野村ノート を読んだことがある。 野村監督は言語化にこだわりのある方で選手としても監督としても一流だった氏の実践知の言語化は参考になるかもしれないと思って読んだ。本書は期待した通りでプロ野球に限らず、一般のビジネスパーソンにとっても汎用的に役に立つアイディアがいくつもあったように思う。

例えば、野村監督が捕手に求めるものとして次がある。判断というのは知識と経験を根拠になんらかの基準をもってくだすといったことが書いてあった。判断の前に分析と観察と洞察の3つの段階を語れる人がどのぐらいいるのだろうか。

  1. 分析
  2. 観察 (目に見えるものをみる)
  3. 洞察 (目に見えないもの = 心理を読む)
  4. 判断
  5. 記憶

今思うのは、小さいことを重ねることが、とんでもないところに行くただ一つの道だと感じている。

これはイチローのコメントだが、この言葉は野村監督の野球観に通じていて感銘を受けたと書いてあった。そんな風に野村ノートがおもしろかったので、その延長上で野村監督に関するイベントなら参加してみようと思った次第。

実際のイベントについては、期待値も高かったのかもしれないが、私の求めていたイベントの内容ではなかった。野村監督と付き合いの長い番記者が野球人としての野村監督というよりも、一般人としての野村監督の在り方を伝えるような著書やそのイベント内容だったと思う。あと野村監督の話しを聞きに行ったのだけど、半分以上は講師が自分のことを多く話すのでその点もアンマッチだったと言える。上司道のイベントは2回目なんだけど、これまでどちらも私の求めていたものではなかった。もしかしたらマネジメントやリーダーシップのイベントで話すのはなかなか難しいのかもしれない。それはいろんな業界・業種の人たちにとって参考になるリーダーシップのようなものはないのかもしれないなと感じた。ドメイン知識も含めてのリーダーシップの話しをしないと、宗教のような徳を積んで治めなさいといったありがたい話しの一般化になってしまう気がする。

人間力は定性的なもので言語化が難しい。それよりも実践知はもう少しスキル寄りなものだと私は考えていて、習慣だったり洞察だったりなら誰でも訓練すれば身につけられるのではないかと思う。少なくとも野村ノートからはそういった片鱗が私には読み取れた。

go 1.20.1 を使い始めた

0時に寝て何度か起きて7時半に起きた。あまりうまく眠れなかった。

go 1.20.1 へのアップグレード

リファクタリングの区切りがついたらやろうと思っていて遅れた。すでに 1.20.1 がリリースされている。先日 Go 1.20 リリースパーティ に参加して、いろいろ聞いていると改善されているところや新しい機能を試してみたいものがいくつかみつかった。単純にアップグレードするだけでも最大でビルド時間が10%短縮される可能性がある。1.18 と 1.19 で generics 対応でビルド時間が遅くなっていたのが 1.17 相当に改善されたらしい。やっぱり generics はコンパイラを複雑にするものなのでコンパイル時間が長くなる傾向があるんやなと話しを聞いていて思ったりもした。ついでに依存ライブラリなどのバージョンアップもまとめてやった。単体テストと結合テストが普通には揃ってきたのでバージョンアップなども安心して実行できる。

rocky linux のネットワーク設定

テスト環境に使っている rocky linux の ip アドレスを変更する必要があったので調べてみた。ちゃんとした公式のガイドが出てきてすごいなと感心した。

ip アドレスを確認するコマンドが ifconfig から ip に変わっていたり、ネットワーク設定のためのツールも私が知っているものとは全然変わってしまっている。

$ ip addr

Network Manager のサービスで設定されているようでそのための設定ファイルは次の場所に保存されていた。

/etc/NetworkManager/system-connections/my-device.nmconnection 

これを書き換えるツールとして nmtui という tui ツールと nmcli という cli ツールの2つがある。tui とか懐かしいなとか思いながら操作していた。ssh 経由で設定していたので cli でいきなり設定を反映するよりも tui で設定ファイルを書き換えて os を再起動するのがよいだろうと考えてやってみた。ドキュメントに書いてある通りに操作したら意図した ip アドレスを設定して変更できた。

コードを書いていると後から改善点に気づく

1時に寝て7時に起きた。起きてからドラクエタクトをだらだらやってて10時前に起き上がってそれからオフィスへ行って活動してた。

リファクタリングのリファクタリング

昨日書いたマージリクエストの変更点についてドキュメントの更新だけしようと作業していたら、その後、一緒に修正した方がよい追加の機能拡張に気付いて3時間ほど追加の実装をしていた。その後、お昼ご飯を食べているときに午前中に書いた実装を改善した方がよいところに気付いて、さらに追加で1時間ほどリファクタリングしていた。コードを書いていて、一旦はできたつもりになってその時点ではよさそうに思うのだけど、時間が経ってから考え直すと考慮漏れやもっとよい実装のアイディアを思い付いたりする。私の頭が悪いだけかもしれないが、最初からよい実装や設計を行うことは多くの人にとって難しいことだと思いたい。何度も考えて作り直したり改善したりしているうちにもっとよい方法に気付く。

多くの開発者はもっとよいアイディアを思い付いたとしても実際にリファクタリングをしようとしない。動いているコードを修正して壊れるリスクや他に優先度の高い作業があるとか、いろいろやらない理由を言う人たちもいる。私はそのやらない理由を議論している合間にリファクタリングしてしまう。というのは、誇張した比喩で実際にはもっと時間がかかることもあるけど、設計も見直す数千行レベルの変更を気軽に行う。もしそれで壊れたらどうすると聞かれてもシンプルに謝るだけ。謝ってから直す、テストを書く、再発防止のための仕組みを考える。やることはそれだけ。問題のあるコードを見て見ぬ振りをする開発者の方が普通という感覚がある。10年以上開発をやってきて思うこととして、そういう価値観を上位の開発者が壊していって模範を示すことがよい開発文化への第一歩となる。しんどいことに対して口だけでは人は動かない。個々の開発者が問題だと思ったらどんどん書き直していく、リファクタリングしていくという文化は一朝一夕ではできない。そういう開発文化を醸成していくと大きな技術的負債が溜まるといったことはなくなるのではないかと考えている。

リファクタリング一段落

1時に寝て7時に起きた。前日も21時頃までオフィスにいて、今日も午後からコードを書いていて22時ぐらいまでやってた。コードを書いていると時間がどんどんなくなる。本当は三宮.dev の勉強会があったんだけど、このリファクタリングは週末にやってしまわないとやばいという野生の勘でキャンセルした。

ストレッチ

今日の開脚幅は開始前156cmで、ストレッチ後160cmだった。だいたいいつも通り。先週から右すねの外側の筋に張りがある。先週よりちょっとよくなった気もするけれど、まだ張りが継続している。あと今週は右腰に張りがあった。今週はリファクタリングしていて座ってきる時間がいつもより長かったためだろうと推測する。どんなに忙しくてもストレッチだけは休まないようにしている。ストレッチに通い始めて2年以上経つが身体的に体調が悪いということは記憶にほぼない。日記にはその週のどこそこに張りがあるとか調子が悪いといったことを書いたりしているが、それは日常生活を送る上で支障が出るようなレベルではない。そうならないように予防している。健康を維持する上でストレッチは大きな影響を与えているため、中長期の展望から忙しくても継続するようにしている。

機能拡張とリファクタリング

今日は休日出勤して go のコードを書いていた。ある機能を作るときに内部的には汎用の api にしてしまって他のコレクションのデータ型でも再利用できるようにしたい。先週ずっと go の generics を使って mongodb 周りのコレクションとそのクライアントのリファクタリングをしていた。go の generics の理解も進んで crud なインターフェースを generics でどのように定義して実装すればいいかわかってきた。その過程で web api のアプリケーション層と mongodb のインフラ層 (データ層) の役割分担も明確になりつつある。どちらも generics を駆使して型チェックされた上でソースコードを共通化し、汎用 api としてリファクタリングしながら設計している。その集大成としてアプリケーション側で汎用的な機能を追加するときに、理想的には1つのコードを追加・変更すれば、別のデータ型でもすべて同じように動くといった機能として実装した。4つのコレクションのデータ型で同じ振る舞い (機能) を共通化する。その実装も丸1日やれば完了できるぐらいに設計の効率化ができてきた。

go はオブジェクト指向言語ではないので generics を駆使しても、java でいうところの抽象既定クラスを用いたテンプレートパターンの実装ができない。それぞれの構造体で基本的にはコピペとなる構造体のメソッドを定義しないといけない。もちろん別のヘルパーに移譲するといったことはできるけど、状態をもっていないコードの再利用はたしかに安全ではあるけれど、状態を参照できないからそのために値をコピーするといった整合性の懸念やボイラープレート的なコードを書く必要がある。これは設計におけるトレードオフになるので go の処理系の設計に不満があるわけではない。但し、generics でできることにはまだ機能不足がある。とくに直和型の扱い。できないのかな?とググると proposal の issue がみつかるので今後の機能拡張に期待したい。

正直マネージャーが開発に工数使っていて何やってるんだとみられているかもしれない。1-2週間集中してリファクタリングしてみて、いまのアプリケーションの設計の勘所が以前よりも理解が進んだ。コードレビューだけではわからないフィードバックがある。結合テストもいくつか追加したので今後の開発で役に立つはず。これで私の (リファクタリング) 開発は終了しようと思う。

今日作ったマージリクエストの diff が次になる。

51 files +1314 -648

diff の行数だけ数えると今週だけで1万行近くは変更したと思う。

mongodb のトランザクションの考え方

1時に寝て8時に起きた。3時頃に気分悪くて起きて少し吐いてそれからまた寝た。丸1日機能拡張とリファクタリングのために go のコードを書いていた。

mongodb のトランザクション管理

2つの web api から同じコレクションの異なるフィールドを更新したい。mongodb で厳密なトランザクションを管理するようなアプリケーションではないけど、なるべく整合性を維持できるように努めることはやっておきたい。mongodb でトランザクションに近いことを実現する方法として次の記事が参考になった。

この記事では findOneAndUpdate() という api を使って、更新時に必ず変更されるフィールドを含めることで find したときのそのフィールドの値が変わっていればエラーになってくれることでトランザクション相当の機能が提供されると書いてある。必ず変更されるフィールドとして ObjectId を使えば他の更新処理を検出するのに役立つだろうとある。いま私が開発しているアプリケーションでは同じフィールドを複数の web api から更新するわけではないのでここまで厳密なトランザクション管理は必要にない。

既存の処理が Replace を使って実装されていたのを Update を使うように変更する。Replace と Update の違いはドキュメント全体を更新するのか、一部のフィールドのみを更新するのかの違いになる。具体的には go のドライバーにおいて次のメソッドの使い分けになる。

これらのメソッドを使うことで find と replace/update の操作を1回の処理でできるから効率もよいらしい。

結合テストのリファクタリング

0時に寝て7時に起きた。前日は久しぶりに飲んでいい気分で寝てた。朝コンビニ寄ったらいつもほぼ満席のイートインが閑散としているなと思いつつ、オフィスへ行って始業報告して、今日祝日だよとツッコミもらった。全然気付いていなかった。まだ新しい天皇誕生日に慣れてない。

go のテストにおける setup/teardown 関数の実装

昨日から結合テストのリファクタリングをしていてせっかくオフィスに行ったので続きをする。結合テストだとデータを作ったり削除したりをテスト単位にやりたい。例えば、ユーザーを作成してテスト実行し、終わったら作成したユーザーを削除したい。次のように setup 関数を書き、返り値として teardown の関数を返す。このときに t.Helper() を呼び出しておくと、エラーがあったときにどの呼び出し元のテストでエラーが発生したかがわかる。

func setupUsers(t *testing.T, users []mongodb.User) func() {
	t.Helper()
	user := mongodb.NewUserCollection(url, username, password)
	for _, u := range users {
		if err := user.Save(u.ID, u.Attributes); err != nil {
			t.Fatalf("failed to create a user: %s", err)
		}
	}
	return func() {
		t.Helper()
		if err := user.Truncate(); err != nil {
			t.Fatalf("failed to truncate the user collection: %s", err)
		}
	}
}

使い方は簡単で setup 関数を呼び出すと teardown 関数が返ってくるのでそれを defer で呼び出すとよい。

teardown := setupUsers(t, usersTestData)
defer teardown()

しかし、この setup 関数をサブテストと一緒に並行実行すると意図したように動かない。どうやらサブテストを並行実行すると親テストが呼ばれた後に呼び出されるという仕組みになっていて、サブテストを実行する前に teardown が呼ばれてしまうので意図した制御にならない。How to handle parent test teardown with parallel subtests in golang の回答 によると、サブテストからさらにサブテストを実行するとよいとある。この場合、親テストとサブテストは同期的に実行され、サブテストの中でさらにサブテストを並行実行するという考え方で意図した振る舞いになる。並行実行するときは次のようなコードになる。

func TestMy(t *testing.T) {
    tests := []struct{
        // test cases
    }

	teardown := setupUsers(t, usersTestData)
	defer teardown()

	t.Run("wrap for setup process", func(t *testing.T) {
		for _, tt := range tests {
			tt := tt
			t.Run(tt.name, func(t *testing.T) {
				t.Parallel()
                // do test
            })
        }
    })
}