Plan 9とGo言語のブログ

主にPlan 9やGo言語の日々気づいたことを書きます。

Goのエラーハンドリングを集約するライブラリを作った + Goが管理するスタック周りの挙動について

お試しで、Goのエラーハンドリングを省略するための try というライブラリを作っているので紹介します(最後にちょっとした告知があります)。

github.com

これを使うと、よくある if err != nil を次のように記述できます。

// HandleとCheckは必ず同じスコープに書いて下さい

cp, err := try.Handle()
if err != nil {
    // 下のCheckでエラーが発生したらここに飛ぶ
    log.Fatalln("Error:", err)
}
s := try.Check1(os.ReadFile("/tmp/xxx"))(cp)
u := try.Check1(url.Parse(string(s)))(cp)
fmt.Println(u.Path)

try パッケージのAPIデザインはError Handling — Problem Overviewを参考にしています。

動作としては、まず Handle で戻り先を記憶しています。このとき、最初の Handle 呼び出しでは必ず errnil になるように返します。なので次の行にある if err != nil のブロックは実行されません。

その後、Check1 を呼び出していますが、ここで戻り値の最後がエラーであれば cp を生成したときの Handle まで巻き戻り、HandleCheck1 が受け取ったエラーを返します。なので今度は if err != nil のブロックが実行されて、ここでは log.Fatalln を呼んでいるのでプログラムが終了します。

Go Playgroundも作ってみたので実際に動かしてみてください。いくつか既知の問題があるけれどだいたい動いたので、こうして記事を書いていますが、下に実装を書いているように Goチームが保証していない挙動を利用している ので、環境や将来のバージョンで動かなくなる可能性があります。また、実績もまだ手元で動いたものしかないので、安定稼働が求められる場面では絶対に使わないで ください。

どちらかというと、この記事では try を実現するために調べたことを紹介するのが目的です。

tryは何をやっているのか

基本はC言語setjmp または longjmp と同じことをしていますが、前提が多いので順番に説明します。

ABIInternal

Goは amd64arm64 などのCPUアーキテクチャごとにABIを定義しています。Go 1.25では ABIInternal という仕様があり、以下のドキュメントにまとまっています。

この仕様では、例えば amd64 アーキテクチャで関数を実行しているとき、以下のようなスタックのレイアウトになります。

関数を実行しているときのスタックレイアウトです

ここで次の関数を呼び出すと、戻り先のアドレス*1と親フレームのBPが保存されているアドレスを下位のスタックに追加します。

関数呼び出し直後のスタックレイアウトです

これで、あとは実際に記述したコードを実行していきます。実行途中でスタックの操作が行われれば SP レジスタも更新されます。

関数呼び出し前処理が終わった時点のスタックレイアウトです

言い方を変えると、BP レジスタは現在のスタックフレームが管理するスタック領域の底アドレスを保持します。スタックは上位アドレスから下位アドレスに向かって伸びるので、底アドレスはメモリアドレス的には上位アドレスです。BP レジスタの値は関数を実行している間は基本的に変わりません。SP レジスタはスタックフレームの下位アドレスを保持します。こちらは関数が実行されている間、スタックの利用状況に応じて増減します。

ここで try を実装するために重要なのは以下の3点です。

  • BP レジスタより8バイト上のアドレスには、関数がリターンするアドレスが入っている
  • BP レジスタが指すメモリアドレスには、親フレーム(関数呼び出し前)の BP レジスタの値が入っている
  • SP レジスタには、その時点で利用しているスタックの先頭アドレスが入っている

このように BP レジスタを使ったリンクリストになっているので、この構造を使うと呼び出しスタックを順に辿ることができます。ということは、スタックを辿って必要な値を探し、SP レジスタが指すアドレスを書き換えれば任意の場所にジャンプできるわけですね。

なので Handle では、以下5つのレジスタCheckpoint 構造体*2に詰めています。arm64 もだいたい同じレジスタを持っています。

amd64 arm64 意味
SP RSP スタックポインタ
BP R29 ベースポインタ
DX R26 コンテキストポインタ
(PC) LR Handle の戻り先アドレス
(BP) (R29) Handle を呼び出した関数のスタックポインタ

こうしておいて、Check 関数でエラーが発生したら詰めておいたレジスタの値を復元しています。そうすると Check 関数からリターンしたとき(細工しておいた)戻り先になっているので、Handle から戻るように動作するのでした。

ABIInternalとABI0

Goアセンブリについてですが、2025年時点では、値のやりとりをレジスタベースで行う ABIInternal と、スタックベースで行う ABI0 の2種類があります。ABIInternalコンパイラ内部で使うABIで、一般のユーザーが使うものではありません。

アセンブリのコードを手書きする場合は ABI0 を使うことになります。以前書いたGoアセンブリの書き方ABI0 に準拠しています。なので上で説明したレジスタを操作するときは ABI0 として記述しつつ、ABIInternal なレイアウトのメモリを読むことになって非常に厄介ですが、幸い SPBP レジスタの扱いは変わらないようでした。

ところで、ABI0 はスタックベースなので、例えば 0(FP)8(FP) といったスタック上のメモリを使って値の受け渡しをします。しかしGoで書いたコードは ABIInternal なので、AXBX といったレジスタで値をやり取りします。この違いがあるので、そのままではうまく値を渡せません。このギャップを埋めるためにABI Wrapperといって、ABIInternalABI0 を仲介する層が存在するようでしたが、あまり詳しくは調べていません。

実際のコードはたぶんこの辺り。

スタックの伸長と縮小

次に、スタックの伸縮について。Goではゴルーチンごとに固有のスタック領域を持っています。最初は2KBで開始して、必要になれば伸長されていきます。この処理は、関数呼び出しの先頭部分にコンパイラが以下のようなコードを挿入することで行っています。

// 本当はアセンブリ言語で書かれているけど雰囲気は同じはず
currentSP -= (関数に必要なスタックサイズ)
if currentSP < currentGoroutine.stackPointer {
    runtime.morestack_noctxt() // スタックを伸長する
}
// 以下、実際に書いた関数の処理が続く...

このとき、伸長するときにスタック領域そのものが移動するので、当然ですがメモリアドレスも変わります。スタック上にある unsafe.Pointer 型の値なら、スタックが移動するとき runtime が一緒にアドレスを書き換えてくれるのですが、uintptr 型の値やヒープにある unsafe.Pointer は変わらないようでした。

var i int
p := unsafe.Pointer(&i) // pはスタック領域に置かれている
u := uintptr(p)
println("before:", &i, p, u)
grow() // スタック伸長が必要な処理を行う
println("after:", &i, p, u) // &iとpは移動後のスタックを指すがuは古いスタックのまま

なので、スタックが移動したことを検出して、Handle によって退避した SP などの値も修正しなければなりません。このために、Handle では「Handle を呼び出したスタックフレームの BP レジスタ」を保存しておきます。HandleCheck は同じ関数内で呼ばれる想定なので、Check 関数が呼ばれたときにスタックフレームを遡って Handle と同じフレームを探し、そのアドレスを比較することで移動した距離を反映しています。

おわりに

この他にも、Handle 関数がインライン展開されるように、関数実装の複雑さを減らすなど細かい調整を行っていますが、面白い話題としてはこのくらいでしょうか。

こういった黒魔術はあまり使わないほうがいいとは理解しつつ、仮にもしこれが受け入れられるならエラーハンドリング周りの改善に繋がったらいいなと思って実装してみたのでした。もし何か不具合等がみつかったら日本語でもいいのでissueに起票していただけると助かります。実際にいま分かっている問題もあって、-covermode=atomic オプションをつけてテストを実行するとよくわからない落ち方をします。

# まだ調べてないけど不思議な落ち方をする...
go test -race -covermode=atomic

宣伝

最後に、筆者が現在所属しているはてなでは、10月31日にhatena.go#2というイベントを開催します。実務でGoを使ったときの面白い話が聞けると思うので、お時間が合う人はぜひ参加をお待ちしています。

connpass.com

*1:関数呼び出しを行った次の命令

*2:上のコードで cp としている値

GitHubでサプライチェーン攻撃を防ぐ設定

ここ数ヶ月でサプライチェーン攻撃に関連していくつかベストプラクティスが出ていたので、GitHubリポジトリに適用しておいたほうがいいものをまとめた。

被害を受けないために

Dependabotにcooldownを設定する

過去のサプライチェーン攻撃では、ほとんどは問題のあるリリースが公開されてから数時間で発見されているので、自分のリポジトリが汚染されないためにリリースから一定期間はアップデートを保留するという手段が取られるようになったと記憶している。もともとRenovateには minimumReleaseAge オプションがあったのだが、Dependabotでも cooldown オプションが使えるので設定する。

このオプションを設定しても、上の記事中に

Key benefits

  • Stay responsive to critical security patches.

とあるように脆弱性の修正は cooldown の設定よりも優先してアップデート対象となるらしいので、ある程度日数を開けても問題ないと思う。設定例は以下の通り。

version: 2
updates:
- package-ecosystem: github-actions
  directory: /
  schedule:
    interval: weekly
  cooldown:
    default-days: 5

上記では、サプライチェーン攻撃に関していえばパッチバージョンだから安全という訳でもないので default-days を設定しているが、この他にもマイナーバージョンやパッチバージョンごとに期間を設定できる。

Actionsのバージョンをハッシュで強制する

GitHub ActionsではGitのタグを使ってバージョンを特定しているので、もともと安全だったタグが攻撃者によって悪意のあるコミットに向けられてしまって汚染されることがある。こういった攻撃を防ぐため、GitHubは公式にSecure use reference#Using third-party actions

You can help mitigate this risk by following these good practices:

  • Pin actions to a full-length commit SHA

のようにハッシュによるバージョン参照を推奨していたのだが、2025年8月にハッシュで参照することを強制する Require actions to be pinned to a full-length commit SHA オプションが追加された。

公式がハッシュによる参照を推奨しているので上記のオプションも有効にするといいと思うけれど、このオプションはワークフローが依存するすべてのアクションでハッシュ指定を強制するので、外部のアクションにタグ指定がひとつでもあるとエラーになる。例えば筆者は一部のワークフローでgovulncheck-actionを使っているのだけど、2025年10月時点では govulncheck-action にはタグを使ってバージョンを参照するコードが含まれているので、上記オプションを有効にするとエラーとなってワークフローを実行できないので状況を確認しつつ有効にしていくといい。

被害を広げないために

CodeQLで検査を行う

CodeQLはGitHubが公式に提供するコードスキャナで、2025年10月時点ではCodeQLの設定は以下2通りの方法がある。

CodeQLはリポジトリの設定画面から Advanced Security タブの Code Scanning セクションで有効にできる。2025年10月時点では、上記で Require actions to be pinned to a full-length commit SHA オプションを設定していると

Code scanning with GitHub Actions is not available for this repository

と警告が出てセットアップを行えない状態となるが、その状態でも自分でワークフローを記述すれば高度なセットアップとしてCodeQLが動作する。高度なセットアップでは以下のようなGitHub Actionsのワークフローとして記述する。

## この内容はブログ記事に掲載するため簡略化しているので、リポジトリの設定から
## Advanced Security/Code Scanningセクションで「Advanced」を選んで出てくるテンプレートを参照してください。

name: "CodeQL Advanced"
on:
  push:
    branches:
    - main
  pull_request:
    branches:
    - main
  schedule:
  - cron: '17 2 * * 6'
jobs:
  analyze:
    name: Analyze (${{ matrix.language }})
    runs-on: ubuntu-latest
    permissions:
      security-events: write
      packages: read
      actions: read
      contents: read
    strategy:
      fail-fast: false
      matrix:
        include:
        - language: actions
          build-mode: none
        - language: go
          build-mode: autobuild
    steps:
    - name: Checkout repository
      uses: actions/checkout@v4
    - name: Initialize CodeQL
      uses: github/codeql-action/init@v3
      with:
        languages: ${{ matrix.language }}
        build-mode: ${{ matrix.build-mode }}
    - name: Perform CodeQL Analysis
      uses: github/codeql-action/analyze@v3
      with:
        category: "/language:${{ matrix.language }}"

ワークフローとして記述する内容については以下のドキュメントにまとまっている。

コード署名を必須にする

名前とアイコンだけでは不安があるのでコード署名も有効にできればより良いが、これは過去に記事を書いた。

blog.lufia.org

Immutable Releaseを設定する

いちどGitHub Releasesで公開したリリースを変更できなくするオプションも追加された。

これを有効にした状態でリリースを作成すると、リリースした内容を変更できなくなる

このオプションを使うには、リポジトリの設定で General タブにある Release セクションで Enable Release Immutability を有効にする。Immutable Releasesを有効にした状態でリリースを作成すると、該当するリリースには鍵付きのアイコンに Immutable というラベルが付与される。

鍵アイコンにImmutableと文字が並んでいます

ただし不変となるのはコミットの内容とリリース成果物だけで、リリースのタイトル、リリースの説明文、プレリリース状態は依然として変更可能となっている。

Immutable Releasesでアセットを追加する

上記の通り、Immutable Releasesが有効になっていると、リリースにファイルのアップロードが行えなくなる。成果物をリリースに含めたい場合にどうするのかといえば、バージョンをいちどドラフトで作成して、成果物のアップロードが完了してから正式リリースに変更する手順を踏む必要がある。GitHub Releasesで各バージョンの状態は

  • Draft
  • Pre-release
  • Release

の3つで推移するので、手動でリリース成果物を追加した後で Pre-release または Release に変更すればいい。

手動またはGitHub CLI等で人間がアップロードする場合はそれでいいのだけれども、GitHub Actionsのワークフローを使って成果物のアップロードを行っていた場合は問題がある。具体的には以下のワークフローについて

on:
  release:
    types:
    - published # リリースが公開されたときにワークフローをトリガーする

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v5
    - run: make zip
    - uses: actions/upload-artifact@v4
      with:
        name: artifacts
        path: "*.zip"
  deploy:
    needs: build
    permissions:
      contents: write
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v5
    - uses: actions/download-artifact@v5
      with:
        path: artifacts
        merge-multiple: true
    - name: release
      run: |
        gh release upload "$TAG" artifacts/*.zip
      env:
        TAG: ${{ github.ref_name }}
        GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

このとき、Immutable Releasesが有効になっていると gh release upload によるアップロードが行えなくなる。上にも書いたようにドラフトであれば成果物の追加が行えるけれども、ドラフトの状態でワークフローをトリガーするイベント が、少なくとも今の時点では存在しない*1

ではどうするのかというと、公式ドキュメントにUsing immutable releases and tags to manage your action's releasesがあって、このドキュメントではGitのタグとして作る方法が紹介されていたのでこの方法でリリースを行うことにした。ワークフローの差分は以下の通り。

diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index ef97f43..c1db329 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -1,9 +1,9 @@
 name: Release
 
 on:
-  release:
-    types:
-    - published
+  push:
+    tags:
+    - v[0-9]+.[0-9]+**
 
 jobs:
   build:
@@ -47,7 +46,10 @@ jobs:
         merge-multiple: true
     - name: release
       run: |
-        gh release upload "$TAG" artifacts/*.zip
+        gh release create "$TAG" \
+          --title="$TAG" \
+          --generate-notes \
+          --draft artifacts/*.zip
       env:
         TAG: ${{ github.ref_name }}
         GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

リリースを行いたいときGitのタグをプッシュすれば、ワークフローによってドラフトのリリースが作られる。このとき、上記ワークフローではリリースノート生成と成果物のアップロードまで行うように変更しているが、リリースノートの内容は後からGitHubのUIで変更できるし、リリースと関連付けられていないタグなら削除できるので、タグ付けを間違えた場合はドラフトリリースとGitのタグそのものを削除してやり直せばいい。

ただ、誤って先にGitHubのUIでリリースとして作ってしまった場合は(もう消せないしアップロードもできないので)新しくタグを作るしかなくなってしまうけれども、Immutable Releasesを使わないよりはメリットが大きいだろうから諦める。

*1:書きかけのリリースを自動バックアップする機能と競合するためらしい

Plan 9でTailscaleネットワークに参加する

Plan 9にはパスワード等シークレットを一元管理するための認証エージェントとしてfactotum(4)が存在しています。factotum はデータを永続化しませんし、そのメモリ領域はカーネルによって保護された状態にあって、デバッガは当然アタッチできないしプロセスの kill(1) さえできません*1。代わりに、暗号化した状態で永続化するためのsecstore(8)があって、この2つが協調して動作します。

筆者は現在、Google Cloud上でCompute Engineを使って3台モデルのPlan 9システムを運用していますが、普段はLinux上で plan9port も使っています。そうしたとき、plan9portfactotum からGCPにある secstored を参照したくなるのだけども、secstored には機密情報が入っているのでインターネットからアクセス可能な場所に置きたくはありません。なので今までは二重管理も仕方ないと思っていたのですが、2025年4月のエイプリルフールネタでTailscaleがPlan 9に対応したとあった*2ので試してみました。

エイプリルフールの記事は以下の2つです。9fansもとても盛り上がっていました。

必要なもの

Tailscaleアカウント

なければ取得しておきましょう。

Goコンパイラ

TailscaleはGoで実装されているのでPlan 9用のGoコンパイラが必要です。他OSでクロスコンパイルする方法がいちばん早いと思いますが、Plan 9の9k(64bit)カーネルでGoを最初からビルドするで紹介したのように最初から順に更新してもいいと思います。

カーネルの更新

9legacyを使っている場合は、Tailscale対応のために不動小数点の扱いと ndb/dns の修正が入っているので、最新のコードに更新しておきましょう。古いPlan 9を使っている場合は、tailscaled を実行したときに以下のようなエラーとなって動作しません。

cpu% tailscaled
logtail started
Program starting: v1.82.0-ERR-BuildInfo, Go 1.24.2: []string{"tailscaled"}
logpolicy: using UserCacheDir, "/usr/lufia/lib/cache/Tailscale"
logpolicy.ConfigFromFile /usr/lufia/lib/cache/Tailscale/tailscaled.log.conf: open /usr/lufia/lib/cache/Tailscale/tailscaled.log.conf: '/usr/lufia/lib/cache/Tailscale' does not exist
logpolicy.Config.Validate for /usr/lufia/lib/cache/Tailscale/tailscaled.log.conf: config is nil
dns: using dns.noopManager

インストール

準備ができたらTailscaleをソースからビルドします。

go install tailscale.com/cmd/tailscaled@latest
go install tailscale.com/cmd/tailscale@latest

tailscaleネットワークに参加

ビルドが終わればあとは tailscaled を実行するだけなのですが、オプションが色々あって難しいので、Arch Linuxtailscale パッケージを参考にオプションを与えました。具体的にはsystemdのユニットファイルが以下のようになっていたので

EnvironmentFile=/etc/default/tailscaled
ExecStart=/usr/sbin/tailscaled --state=/var/lib/tailscale/tailscaled.state --socket=/run/tailscale/tailscaled.sock --port=${PORT} $FLAGS
ExecStopPost=/usr/sbin/tailscaled --cleanup

なのでPlan 9でも次のようなで起動します。ここでは --socket オプションと --port オプションを渡していませんがデフォルトで動作します。

tailscaled --state $home/lib/tailscaled/tailscaled.state

デーモンが起動したら、あとは tailscale up とするとログインURLが出力されるので、それにアクセスすればネットワークに参加できます。

tailscale up

これで比較的安全に secstored とローカルの factotum が連携できるようになって嬉しいですね。

Plan 9端末からtailscaleネットワークに参加した画面です

*1:本当に誰もなにもできなくなる

*2:この少し前にRuss CoxがGitHubで9legacyをForkしていて、何しているんだろうねと社内のSlackで話をしていたのだった

Plan 9の9k(64bit)カーネルでGoを最初からビルドする

はじめに

  • この記事は趣味の記録みたいなものです
  • 他OSでPlan 9用にクロスコンパイルする方が圧倒的に早いので普通はそれがおすすめです

ブートストラップ可能な下限バージョン

GoはもともとCで実装されていましたが、Go 1.5以降はセルフホストされています。公式ドキュメントのInstalling Go from sourceにビルド可能なバージョンが書かれていますが、現時点までのバージョンをまとめると

バージョン どこまでビルド可能か
C 〜 Go 1.4
Go 1.4 Go 1.5 〜 1.19
Go 1.17 Go 1.20 〜 1.21
Go 1.20 Go 1.22 〜 1.23

これ以降は、ターゲットのマイナーバージョンが

  • 偶数の場合: N-2
  • 奇数の場合: N-3

という制約になります。例えばGo 1.25をビルドしたい場合、マイナーバージョンが奇数なので下限はGo 1.22です。

Plan 9(64bit)の場合

基本的には上記の通りなのですが、Plan 9でビルドする場合は上記の通りにはいかないケースがいくつかあります。なのでインストールした直後のPlan 9で2025年夏時点の最新バージョンに到達する手順をまとめます。

Go 1.4.3

まずはCコンパイラで1.4.3をビルドします。素のPlan 9では証明書の検証がうまくいかないので、別の方法でダウンロードしておいてください。

curl -LO https://go.dev/dl/go1.4.3.src.tar.gz

あとはPlan 9go1.4 をビルドします。

gunzip -c go1.4.3.src.tar.gz | tar x
mv go go1.4
cd go1.4/src
./make.rc

Go 1.9.7

次に、公式にはGo 1.4があれば1.17までビルドできるとなっていますが、Plan 9(64bit)の場合はGo 1.17やGo 1.11まで上げると runtime: garbage collector found invalid heap pointer というエラーで失敗します。なので、一旦Go 1.9を経由する必要があります。これはGo 1.4.3を使ってビルドします。

curl -LO https://go.dev/dl/go1.9.7.src.tar.gz

ビルドの手順はGo 1.4と同じなので省略。

Go 1.11.13

Go 1.9.7を使ってGo 1.11をビルドします。これは飛ばしてGo 1.17まで上げても動くかもしれないけれど試していません。

curl -LO https://go.dev/dl/go1.11.13.src.tar.gz

GOROOT_BOOTSTRAP 環境変数で指示する必要があります。

gunzip -c go1.11.13.src.tar.gz | tar x
mv go go1.11
cd go1.11/src
GOROOT_BOOTSTRAP=$home/go1.9 ./make.rc -v

ここで make.rc-v オプションを与えていますが、これはビルドの状況を詳しく出力するためのオプションです。Go 1.10前後で make.rc の出力が

Building Go cmd/dist using /usr/glenda/go1.9. (go1.9.7 plan9/amd64)
Building Go toolchain1 using /usr/glenda/go1.9.
Building Go bootstrap cmd/go (go_bootstrap) using Go toolchain1.

のようにシンプルなスタイルに変わりましたが、Plan 9でのビルドはネットワーク次第で遅くなるので細かい進捗が出力される方が嬉しいのです。とはいえ無くても困ったりはしません。

Go 1.17.13

Go 1.11を使ってビルドします。

curl -LO https://go.dev/dl/go1.17.13.src.tar.gz

ビルド方法は1.11と同じなので省略。

Go 1.21.13

Go 1.17を使ってビルドします。次の1.22をビルドするために必要な最小バージョンは1.20だけど、新しい方がなにかと良いだろうと思って、Go 1.17でビルド可能な中での最も新しいバージョンを選択しました。

curl -LO https://go.dev/dl/go1.21.13.src.tar.gz

ビルド方法は1.11と同じなので省略。

Go 1.22.12

Go 1.21を使ってビルドします。

curl -LO https://go.dev/dl/go1.22.12.src.tar.gz

ビルド方法は1.11と同じなので省略。

Go 1.24.5

この記事を書いている時点で最新バージョンは1.24.5なのでGo 1.22を使ってビルドしたいのですが、このバージョンは64bit Plan 9環境では問題があるらしく、以下のエラーで失敗します。

Building Go toolchain3 using go_bootstrap and Go toolchain2.
M structure uses sizeclass 1792/0x700 bytes; incompatible with mutex flag mask 0x3ff
fatal error: runtime.m memory alignment too small for spinbit mutex
runtime: panic before malloc heap initialized

runtime stack:
runtime.throw({0xc675e7?, 0x0?})
        runtime/panic.go:1101 +0x49 fp=0x7fffffffee78 sp=0x7fffffffee48 pc=0x273529
runtime.lockVerifyMSize()
        runtime/lock_spinbit.go:97 +0xc5 fp=0x7fffffffeea0 sp=0x7fffffffee78 pc=0x2175c5
runtime.schedinit()
        runtime/proc.go:818 +0x34 fp=0x7fffffffef00 sp=0x7fffffffeea0 pc=0x244c74
runtime.rt0_go()
        runtime/asm_amd64.s:349 +0x128 fp=0x7fffffffef08 sp=0x7fffffffef00 pc=0x278a28
go: error obtaining buildID for go tool compile: exit status: 'compile 17771: 2'
go tool dist: FAILED: /usr/glenda/go/pkg/tool/plan9_amd64/go_bootstrap install -v -a cmd/asm cmd/cgo cmd/compile cmd/link cmd/preprofile: exit status: 'go_bootstrap 17767: 1'

この問題はruntime: panic before malloc heap initialized on plan9/amd64で報告されていて、新しいバージョンでは解決しているようなので1.25相当まで上げましょう。

Go 1.25rc2相当

上記の通り1.24がビルドできないので、GitHubから最新のソースコードを拾ってきてこれで代用します。もう少し経てば正式版が出ると思うので、それを使ってもいいかもしれません。

unzip go-master.zip
mv go-master go
cd go/src

ここで VERSION ファイルを作るのですが、ソースをzipでダウンロードした場合は .git がなくて VERSION の作成が上手く動きません。なので手で以下のような内容のファイルを作成します。

go1.25rc2
time 2025-07-29T12:08:00Z

これであとは make.rc するだけです。

GOROOT_BOOTSTRAP=$home/go1.22 ./make.rc -v

9legacy/9front/plan9portのrcシェルで`delim{cmd}構文が利用可能になりました

タイトル通りなんですが、主なPlan 9派生の rc シェルで `delim{cmd} 構文が使えるようになりました。この構文は9atomで最初に実装されたのですが、用途としては一時的に $ifs を置き換えたい場合に利用できます。

# 例です
newline='
'
a=`$newline{cat file}

$ifs を変更することに比べて、変更が子孫プロセスに影響するかどうかが異なります。$ifs環境変数*1なので子孫プロセスに引き継がれてしまいますが、この構文は書いたコマンドの出力にだけ影響します。

例えば、あまり良い例ではありませんが、次のような platform.rc ファイルがあったとします。実行するとOSとCPUアーキテクチャ: で区切って出力します。

#!/usr/lib/plan9/bin/rc

os=`{uname}
arch=`{uname -m}
switch($os){
case Linux
    echo -n linux:$arch
case *
    echo -n other:$arch
}

そして platform.rc を呼び出す build.rc があったとします。このとき、build.rcplatform.rc の出力を分割しようとして $ifs: に設定しています。

#!/usr/lib/plan9/bin/rc

# ifsを一時的に変更するため var=val {cmd} スタイルを利用している
ifs=: {a=`{rc ./platform.rc}}

os=$a(1)
arch=$a(2)

一見うまく動きそうですが、実際は親プロセスで変更した $ifsplatform.rc にも影響してしまい ifs=: として uname の出力を扱います。これでは uname の出力にある改行が残ってしまうため、意図した動作にならず問題ですね。この例では改行が残る程度ですが、もっとひどい結果になる場合はあるでしょう。

そこで `delim{cmd} では、環境変数$ifs は変更せずに、コマンド出力を分割する部分でだけ delim を使うようになっています。基本的に、$ifs を子孫プロセスに引き継ぎたいケースはほとんど無いと思いますし、今までの書き方よりもこちらの方が便利なので、積極的に利用すると良いんじゃないかなと思います。

# これまでの書き方
ifs=: {a=`{rc ./platform.rc}}

# 新しい構文では記述も簡単になる
a=`:{rc ./platform.rc}

*1:厳密にはUnix相当の環境変数ではないけれど分かりやすさのためにそう表現しています

systemdユニットの依存関係と実行順序を理解する: Wants=, Requires=, Requisite=, PartOf=, BindsTo=, Upholds=の違い

systemdには、依存関係を記述するためのディレクティブとして Wants=, Requires=, BindsTo= などがあり、順序を記述するために Before=After= が用意されています。systemd.unit(5)に説明は書かれていますが、具体的にどう動くのか分からなかったので、ユニットの起動処理と停止処理に注目して挙動を調べました。

Unitセクション

まずは各ディレクティブの概要から。マニュアルから起動、停止に関連する部分を抜き出して意訳したものを載せます。

Wants=

弱い依存関係を示す。a.service の実行に b.service が必要なとき Wants=b.service と記述する。ユニットファイルに記述する他に、a.service へのシンボリックリンクb.service.wants/ ディレクトリ以下に配置することでも依存関係を注入できる。

Wants= で指定した依存先ユニットの起動に失敗しても、要求したユニット自体は起動する。また、依存関係はサービスの起動や停止の順序に影響しない。After= または Before= を設定しない限り依存元と依存先は同時に起動する。

Requires=

強い依存関係を示す。Wants= に似て、ユニットファイルに記述する他に a.service へのシンボリックリンクb.service.requires/ ディレクトリ以下に配置することでも依存関係を注入できる。

Requires=After= を同時に指定している場合、依存先ユニットの起動に失敗すると要求したユニットも起動しない。ただし ConditionXxx= による条件チェックに失敗した場合、依存先ユニットが正常に停止した場合などは要求したユニットを実行する。

BindsTo=

Requires= よりも強い依存を意味する。Requires= の挙動に加えて、依存ユニットが突然非アクティブ状態になった場合、要求したユニットも一緒に停止する。After= と同時に使うと、上記に加えて ConditionXxx= の条件チェックに失敗した場合でも要求したユニットを停止する。

PartOf=

Requires= と同様の依存関係となるが、影響はユニットの停止と再起動に限定される。依存先→依存元の単方向な依存となる。

Upholds=

Wants= に似ているが、これを設定したユニットが動作している間、Wants= にリストされたユニットも維持される。Wants= は起動時だけに影響するがだが、これは要求したユニットが実行中の間は永続的に影響する。

Before=

ユニットに Before=b.service と記述された場合、このユニットが起動完了するまで b.service の起動を遅延する。複数のユニットを記述する必要がある場合はスペース区切りで列挙する。

ユニットを停止する際には順序が逆になる。a.service の後に b.service を起動した場合、停止するときは b.service の後に a.service を停止する。

After=

Before= の順序が逆になる。

実際の動作検証

上記を読んだとき、なんとなく分かりそうだけど PartOf= の動作は全然わからないと感じました。なので以下のユニットを用意して手元で実験してみました。この手の例は a.serviceb.service がよく使われる印象ですが、どっちが依存元なのか分かりづらいので app.servicedb.service で例を書きます。まずは他ユニットを要求する app.service のユニットファイルです。

[Unit]
Description=app
Wants=db.service

[Service]
Type=simple
ExecStart=/bin/sleep inf

次に依存先となる db.service のユニットファイルです。

[Unit]
Description=db

[Service]
Type=simple
ExecStart=/bin/sleep inf

この2つを使って、app.serviceWants= を変更したり、After= を設定したりと変化させつつ動作を確認します。

ユニットを起動したときの結果

app.servicedb.service どちらも実行していない状態からそれぞれを起動したときは、表のように動作しました。余談ですが、Requires= + BindsTo= + PartOf= のように、1つのユニットに複数のディレクティブを記述した場合、制約の強いものが反映されるようです。

ディレクティブ appを起動したとき appを起動したとき(After=あり) dbを起動したとき
Wants= appdb を同時に起動する db, app の順に起動する db だけ起動する
Requires= appdb を同時に起動する db, app の順に起動する db だけ起動する
Requisite= app だけ起動する app は起動に失敗する db だけ起動する
BindsTo= appdb を同時に起動する db, app の順に起動する db だけ起動する
PartOf= app だけ起動する app だけ起動する db だけ起動する
Upholds= appdb を同時に起動する db, app の順に起動する db だけ起動する

After= を設定しない場合は、Requisite=PartOf= において依存先が起動していなくても app.service を実行中にします。それ以外は db.service を同時に起動します。このとき After= を設定していないので、Requisite= であってもユニットは実行中状態になります。また、「同時に」と書いてはいますが、プロセスIDを見る限りでは、保証はされていないと思うけれども app.service を先に起動しているようです。

After= を設定した場合は挙動が変化して、実行順序を保証しつつ Requisite= の起動が失敗するようになります。

表の右端でみるように db.service 単体で起動した場合は app.service に影響を与えません。db.service を起動したとき同時に app.service を起動したければ、下でみるように Install セクションの RequiredBy= または WantedBy= を使います。

依存先ユニットの起動に失敗したときの結果

次に依存先ユニットの起動に失敗したときの動作をみます。失敗させるため db.service はすぐに失敗するようなユニットにしておきます。

 [Unit]
 Description=db
 
 [Service]
 Type=simple
-ExecStart=/bin/sleep inf
+ExecStart=/bin/false

上の実験と同様に、app.serviceWants= を変更しつつ、依存先として db.service を起動したときの様子を確認します。

ディレクティブ dbの起動に失敗
Wants= app だけ起動する
Requires= app だけ起動するが数分後に app を停止する(1)
Requisite= app もすぐに停止する
BindsTo= app もすぐに停止する
PartOf= app だけ起動する
Upholds= app を起動したまま db もリトライし続ける(2)

表として表現するのが困難な事柄がいくつかあったので補足です。

(1)で数分後というのは、db.service が失敗してすぐに app.service も停止する訳ではありませんでした。手元の環境ではしばらく app.service だけで実行し続けて、だいたい2分後に app.service も停止しました。systemdのユニットにおけるrequiresが機能してないように見えますの回答によると、この挙動は Type=simple だから「起動と同時に成功した」扱いとなっているようです。2分という時間の出どころは不明ですが、Restart= ディレクティブなどで変化するのかもしれませんが試していません。

(2)では、Upholds=app.service を実行中にしつつ、db.service をリトライし続けます。10分待ってみたけどリトライが止まらないので、それ以上は確認していません。

ユニットを停止したときの結果

最後に、停止するときの動作は次のようになります。After= の有無による変化はありません。

ディレクティブ appの正常/異常終了 dbの正常終了 dbの異常終了
Wants= db は継続する app は継続する app は継続する
Requires= db は継続する app も停止する app は継続する
Requisite= db は継続する app も停止する app は継続する
BindsTo= db は継続する app も停止する app も停止する
PartOf= db は継続する app も停止する app は継続する
Upholds= db は継続する db が再起動される db が再起動される

Unitセクションのまとめ

After= の有無や Type= の値によって挙動が変わるので難しいですが、誤解を気にせず簡単に言えば Wants=Requires= は「ユニットを起動するとき依存先ユニットも一緒に起動するか」「依存先ユニットが停止したときどう振る舞うか」がディレクティブ毎に異なると言えそうです。

Installセクション

Install セクションは systemctl enable または systemctl disable したときに影響します。Unit セクションの内容が分かっていれば、依存の向きが逆になる(自身に依存させるような書き方をする)だけで動作としては同じなので理解しやすいと思います。

WantedBy=

WantedBy= にユニットを記述とすると、そのユニットに Wants= を記述した動作と同等になります。例えば acme.serviceWantedBy=graphical-session.target を記述した場合、

# acme.service
[Unit]
Requires=graphical-session.target
After=graphical-session.target

[Install]
WantedBy=graphical-session.target

上記の acme.servicesystemctl enable で有効化すると graphical-session.target.wants/ ディレクトリに acme.service へのシンボリックリンクが作成されて、結果的に graphical-session.targetacme.service に依存した状態になります。このとき、acme.servicegraphical-session.target に依存して、graphical-session.targetacme.service に依存する状態となって循環するのでは、と危惧するかもしれませんが、上記でみたように Wants=Requires= は依存先ユニットを起動または停止するかどうかでしかないので問題なく動作します。

ところで graphical-session.targetacme.service に依存すると書くと、依存関係として逆じゃないかと思います。個人的にはずっと気持ち悪さを感じていましたが、.target なユニットは「あるべき状態」を定義するものなので「graphical-session.target としてあるべき状態は xxx.service を含む」と解釈すれば違和感は解消されるのではないでしょうか。

RequiredBy=

WantedBy= と似ていますが Requires= 相当となります。

Goで気に入っているテストの書き方と関数をモックするjest.fnのようなライブラリを作った話

以前からずっとある方法だけど、関数の内部で xxxInternal という形で実装を分けて、単体テストでは xxxInternal を使ってテストする書き方を気に入っています。どういうことかといえば、例えばKinesisにPutする以下のようなメソッドがあったとき、

type Writer struct{ ... }

func (w *Writer) BulkPost(ctx context.Context, metrics []*Metric) error {
    k := kinesisFromConfig(w) // Kinesisを初期化する
    records := make([]types.PutRecordsRequestEntry, len(metrics))
    for i, m := range metrics {
        records[i] = metricToRequestEntry(m)
    }
    input := &kinesis.PutRecordsInput{
        Records:    records,
        StreamName: aws.String("dummy"),
    }
    _, err := k.PutRecords(ctx, input)
    return err
}

このままでは k.PutRecords で本当のKinesisに投げてしまって困るので、本物のKinesisを参照する部分とそれ以外で、Writer.BulkPost()bulkPostInternal のように分けてしまいます。このとき、bulkPostInternalインターフェイスKinesisクライアントを受け取るようにしておきます。

type Writer struct{ ... }

func (w *Writer) BulkPost(ctx context.Context, metrics []*Metric) error {
    // 公開する実装は本物のKinesisクライアントを渡す
    k := kinesisFromConfig(w)
    return bulkPostInternal(ctx, metrics, k)
}

// recordsPutter はbulkPostInternalで必要なKinesisのメソッドだけ定義したインターフェイス。
type recordsPutter interface {
    // 本物のKinesisをそのまま渡せるようにKinesis SDKの型にあわせておく
    PutRecords(ctx context.Context, input *kinesis.PutRecordsInput, opts ...func(*kinesis.Options)) (*kinesis.PutRecordsOutput, error)
}

func bulkPostInternal(ctx context.Context, metrics []*Metric, putter recordsPutter) error {
    records := make([]types.PutRecordsRequestEntry, len(metrics))
    for i, m := range metrics {
        records[i] = metricToRequestEntry(m)
    }
    input := &kinesis.PutRecordsInput{
        Records:    records,
        StreamName: aws.String("dummy"),
    }
    _, err := putter.PutRecords(ctx, input)
    return err
}

これで自由にモックできるようになったので、テストでは関数にメソッドを生やしてモックします。

type putterFunc func(metrics []*Metric) (*kinesis.PutRecordsOutput, error)

// PutRecords はrecordsPutterインターフェイスのPutRecordsを実装します。
func (f putterFunc) PutRecords(ctx context.Context, input *kinesis.PutRecordsInput, opts ...func(*kinesis.Options)) (*kinesis.PutRecordsOutput, error) {
    return f(input)
}

func TestBulkPost(t *testing.T) {
    metrics := make([]*Metric, 10)
    bulkPostInternal(context.Background(), metrics, putterFunc(func(metrics []*Metric) (*kinesis.PutRecordsOutput, error) {
        return nil, errors.New("error")
    }))
}

このように分割すると、

  • モックの差し替えも関数を書くだけなので簡単
  • テストしたい対象の近くにモックの実装があるので、モックが返している値がすぐ見える
  • パッケージの外からは必要なインターフェイスしか見えない

特に最後の項目は、モックのためのインターフェイスをエクスポートしていないので、モックのための関数や型が見えていないところがいいですね。

type Writer struct{ ... }

func (w *Writer) BulkPost(ctx context.Context, metrics []*Metric) error

ライブラリ作成のモチベーション

やりたいのは上記の内容なのだけども、テストのたびに func(metrics []*Metric) (*kinesis.PutRecordsOutput, error) を手書きするのも少し面倒になるので、jest.fn みたいなライブラリが欲しいなと思いました。上記で putterFunc を実装した関数でいうと、以下のように書きたい。

fn := mock.Fn().ReturnOnce(nil, errors.New("error"))
bulkPostInternal(context.Background(), metrics, putterFunc(fn))

なのでGoのモックライブラリをいくつか調べたけれど、少なくとも有名なライブラリはインターフェイスをモックするものしか見つけられませんでした。調べるのも飽きてきた頃にたまたま「MOck FUnctionでmofu」って可愛い名前を思いついたので、自分の欲しいものを作りました、というのが以下のライブラリです。

github.com

Goの型パラメータによる制約などがあるので上で書いた欲しいものとは多少違いますが、TestBulkPost の例は

m := mofu.MockFor[putterFunc]().ReturnOnce(nil, errors.New("error"))
fn, r := m.Make()
bulkPostInternal(context.Background(), metrics, fn)
// 呼び出し回数のテスト、gtは最近気に入っている github.com/m-mizutani/gt です
gt.Equal(t, r.Count(), 1)

のように書けます。他にも、モックを書くときに必要そうなものは組み込んでいて、呼び出した回数によって結果を変えたい場合は ReturnOnce を繋げて書きます。

fn, r := m.
    ReturnOnce(&kinesis.PutRecordsOutput{}, nil). // 1回目の呼び出し
    ReturnOnce(nil, errors.New("error")). // 2回目
    Panic("panic"). // 3回目以降はずっとpanic
    Make()

引数によって返す値を変える場合は When で条件を書きます。

m.When(mofu.Any, &kinesis.PutRecordsInput{}).ReturnOnce(...).Make()

上記では MockFor で型を指定しましたが、具体的な関数があってそれをモックしたい場合のために MockOf もあります。

now, _ := mofu.MockOf(time.Now).Return(time.Date(...)).Make()

メソッドが複数の場合どうするか

ここまで恣意的にメソッドが1つだけの例を挙げていましたが、実際はメソッドが複数必要な場合もあります。mofu では、インターフェイスの実装に必要なだけ関数を作って合成する方法を採用しました。

read := mofu.MockOf(io.Reader.Read)
write := mofu.MockOf(io.Writer.Write)
m := mofu.Implement[io.ReadWriter](read, write)

ここで mio.ReadWriter を動的に実装しています。reflect だけでは動的なメソッド実装できないのですが、karamaruさんのポストを読んでいて知った go-dyno というパッケージが unsafego:linkname コンパイラディレクティブなども使って動的なインターフェイス実装を実現しています。

github.com

具体的には go-dyno では Dynamic という関数が func(meth reflect.Method, args []reflect.Value) []reflect.Value というシグネチャでコールバックを受け取り、メソッドが呼ばれたときにこの関数で動的に挙動を切り替えるようになっています。もともとはovechkin-dm/mockioというモックライブラリのために作られたようで、mofu と用途が被っていて気まずいなと思いますが、mockioインターフェイス主体でこちらは関数主体なので、まあいいかな。