Skip to content

Goのエラーハンドリングでsetjmp/longjmpが使えるか

Goのエラーハンドリングは度々不格好だと言われ続けているが、Goによるエラーハンドリング構文の最終結論で公式に「当分の間はエラー用の構文を導入しない」とアナウンスされてしまった。たまたまPlan 9のソースコードを読んでいたときに waserror というマクロで setjmp + longjmp を実装していたコードを見て、これがエラー処理に使えないかと考えた。例えば

// 最初は常にnilを返す。Checkに失敗した場合はここに戻ってきてerrを返す。
if err := try.Handle(); err != nil {
// ここで一度だけエラーを扱う
return fmt.Errorf("failed: %w", err)
}
// 失敗したらHandleまで戻り、エラーを返す。
v := try.Check(strconv.Atoi(s))

形を変えつつ、上記の案を実装してみた。

HandleCheck という名前はGo2のときに公開されたError Handling — Problem Overviewに揃えている。

ブログ記事に書いた他にも色々と対策をしていて、Handle をインライン関数化して(同じスコープで Check する限りは)スタック領域を書き換えられないようにすることで、waserror に擬態するだけでよくしている。これがなければ Handle に擬態する必要が出てきて error を返さなければならなくなる。

あと、メソッドは型パラメータを持てないので関数として Check を実装していたり、戻り値をまとめて受け取るために func(*Checkpoint) を返す関数として Check を設計しなければならなかった。

アセンブリを書くことになるので、各種アーキテクチャでCIを実行したくなった。これはgithub.com/uraimo/run-on-arch-actionでたぶん実現できる。

エラーをラップするためのしくみ

Section titled “エラーをラップするためのしくみ”

それからやってみて気づいたのだが、APIデザインとしてはエラーをラップしたい場合がある。アイデアとしては2つあって、ひとつは単に何もしないが使い方で工夫する方法。

scope1, err := try.Handle()
if err != nil {
return httperror.BadRequest(err)
}
scope2, err := try.Handle()
if err != nil {
return httperror.InternalServerError(err)
}
try.Check(strconv.Atoi(s))(scope1) // 状況によってスコープを使い分ける

で、もうひとつはAPIとして仕組みを構築する方法。

scope, err := try.Handle()
if err != nil {
return err
}
try.Check(strconv.Atoi(s)).Wrap(httperror.BadRequest).Eval(scope)

前者の方が実装は楽なのだが、その方法は結局冗長になるので解決したかった問題が解決できそうにない。なのでラップするための機能は必要だろうと思ったが、Eval は不格好なのでこういうのでどうか。

scope, err := try.Handle()
if err != nil {
return err
}
try.Check(strconv.Atoi(s))(scope, try.WithHandler(httperror.BadRequest))

Goでスタックが自動で伸縮するしくみのように、Goではスタックが移動する場合がある。このとき Handle で退避した各種アドレスは関数の外に出るためエスケープされてヒープに置かれることになる。なのでスタックの再配置が行われた場合、レジスタへ書き戻したとき古いアドレスを参照してしまって panic が発生する。

なのでスタックの値で基本的に動かないものを使って、どのくらい移動したのかを計算する必要があった。

ランタイムを騙すことになるので、デバッガやABI仕様と向き合う必要があった。スタック関連のABIはGoにおけるスタックの使われ方に書いた。デバッガについてはDelveの使い方にまとめている。

DWARFも必要だろうと思ってGoでデバッグ情報を読むにまとめていたが、最終的には使わなくてもよかった。