Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 18412] ファイルの概要

このコミットは、Go言語の標準ライブラリ text/tabwriter パッケージにおけるパニック時のバックトレースの改善を目的としています。具体的には、tabwriter の内部処理中に発生したパニックが、より分かりやすいメッセージと共に再パニックされるように変更され、デバッグ時の情報が強化されています。

コミット

commit d885aeaaa4263717a18c322da7c9dfc6847aa2b2
Author: Josh Bleecher Snyder <josharian@gmail.com>
Date:   Tue Feb 4 10:19:02 2014 -0800

    text/tabwriter: improve panic backtraces
    
    Fixes #7117.
    
    LGTM=gri
    R=golang-codereviews, gobot, gri
    CC=golang-codereviews
    https://golang.org/cl/53310044

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/d885aeaaa4263717a18c322da7c9dfc6847aa2b2

元コミット内容

text/tabwriter: improve panic backtraces Fixes #7117. LGTM=gri R=golang-codereviews, gobot, gri CC=golang-codereviews https://golang.org/cl/53310044

変更の背景

text/tabwriter パッケージは、テキストを整形してタブ区切りの列を揃えるための機能を提供します。このパッケージの内部処理において、例えば基盤となる io.Writer がエラーを返したり、予期せぬパニックが発生した場合、そのパニックが適切に処理されず、デバッグが困難になる可能性がありました。

このコミットの背景には、おそらく tabwriterFlush メソッドや Write メソッドの実行中に、下層の io.Writer からのパニックが捕捉された際に、そのパニックが元のエラーとして適切に伝播されず、デバッグ情報が不足するという問題があったと考えられます。コミットメッセージにある Fixes #7117 は、この特定の挙動に関する課題を指しています。この変更は、パニック発生時に、より明確な情報(どの操作中にパニックが発生したか)をバックトレースに含めることで、問題の特定と解決を容易にすることを目的としています。

前提知識の解説

  • text/tabwriter パッケージ: Go言語の標準ライブラリの一つで、テキストデータをタブ区切りで整形し、列を揃えて出力するための機能を提供します。Writer 型が主要なインターフェースであり、Write メソッドでデータを書き込み、Flush メソッドでバッファをフラッシュして整形された出力を完了します。
  • パニック (Panic): Go言語におけるランタイムエラーの一種です。通常、プログラムの回復不可能なエラーや、プログラマの想定外の状況で発生します。パニックが発生すると、通常の実行フローは中断され、defer 関数が実行された後、プログラムは終了します。
  • リカバリ (Recover): panic によって中断された実行フローを捕捉し、回復させるための組み込み関数です。defer 関数内でしか呼び出すことができません。recovernil 以外の値を返した場合、パニックが発生したことを意味し、その値は panic に渡された引数です。
  • バックトレース (Backtrace): プログラムがクラッシュまたはパニックした際に、その時点までの関数呼び出しの履歴(スタックトレース)を示すものです。デバッグ時に問題発生箇所を特定するために非常に重要な情報です。
  • osError: text/tabwriter パッケージ内部で定義されている非公開の構造体で、io.Writer から返されるエラーをラップするために使用されます。これは、tabwriter 自身が生成したエラーと、下層の io.Writer から伝播されたエラーを区別するために利用されます。

技術的詳細

このコミットの主要な変更点は、handlePanic 関数と、Writer 型の Flush および Write メソッドにおける handlePanic の呼び出し方にあります。

handlePanic 関数の変更

変更前:

func handlePanic(err *error) {
	if e := recover(); e != nil {
		*err = e.(osError).err // re-panics if it's not a local osError
	}
}

変更後:

func handlePanic(err *error, op string) {
	if e := recover(); e != nil {
		if nerr, ok := e.(osError); ok {
			*err = nerr.err
			return
		}
		panic("tabwriter: panic during " + op)
	}
}
  • op string パラメータの追加: handlePanic 関数に op という文字列パラメータが追加されました。このパラメータは、パニックが発生した操作(例: "Flush" や "Write")を示すために使用されます。
  • osError の型アサーションの改善: 変更前は e.(osError).err と直接型アサーションを行っていましたが、これは eosError 型でない場合にパニックを引き起こす可能性がありました。変更後は nerr, ok := e.(osError) というパターンで型アサーションを行い、oktrue の場合のみ osError として処理します。これにより、osError 以外のパニックが捕捉された場合でも、安全に処理を継続できます。
  • 明確な再パニックメッセージ: eosError 型でない場合(つまり、tabwriter 内部で発生した osError ではない、予期せぬパニックの場合)、panic("tabwriter: panic during " + op) という形式で再パニックされるようになりました。これにより、バックトレースに「tabwriter: panic during Flush」や「tabwriter: panic during Write」といった、どの操作中にパニックが発生したかを示す明確なメッセージが含まれるようになります。これはデバッグ時に非常に役立ちます。

Flush および Write メソッドの変更

Writer 型の Flush メソッドと Write メソッドの defer ステートメントで handlePanic を呼び出す際に、op パラメータが追加されました。

  • Flush メソッド: defer handlePanic(&err) から defer handlePanic(&err, "Flush") へ変更。
  • Write メソッド: defer handlePanic(&err) から defer handlePanic(&err, "Write") へ変更。

これにより、Flush または Write の実行中にパニックが発生し、それが osError ではない場合、それぞれ「tabwriter: panic during Flush」または「tabwriter: panic during Write」というメッセージで再パニックされるようになります。

テストケースの追加

tabwriter_test.go に、パニック時の挙動を検証するための新しいテストケースが追加されました。

  • panicWriter 構造体: io.Writer インターフェースを満たすダミーの型で、Write メソッドが常にパニックを発生させるように実装されています。
    type panicWriter struct{}
    
    func (panicWriter) Write([]byte) (int, error) {
        panic("cannot write")
    }
    
  • wantPanicString 関数: recover を使用してパニックを捕捉し、期待されるパニックメッセージと一致するかを検証するヘルパー関数です。
    func wantPanicString(t *testing.T, want string) {
        if e := recover(); e != nil {
            got, ok := e.(string)
            switch {
            case !ok:
                t.Errorf("got %v (%T), want panic string", e, e)
            case got != want:
                t.Errorf("wrong panic message: got %q, want %q", got, want)
            }
        }
    }
    
  • TestPanicDuringFlush: panicWritertabwriter.Writer の出力先として設定し、Flush メソッドを呼び出した際に、期待されるパニックメッセージ「tabwriter: panic during Flush」が発生するかを検証します。
  • TestPanicDuringWrite: 同様に panicWriter を設定し、WriteString を呼び出すことで Write メソッドが内部的に呼び出され、期待されるパニックメッセージ「tabwriter: panic during Write」が発生するかを検証します。特に、io.WriteString(w, "a\\n\\n") のように、2つ目の \nw.Write の呼び出しをトリガーし、パニックを発生させる点が重要です。

これらのテストケースは、handlePanic の変更が意図通りに機能し、パニック時に適切なメッセージが生成されることを保証します。

コアとなるコードの変更箇所

src/pkg/text/tabwriter/tabwriter.go

--- a/src/pkg/text/tabwriter/tabwriter.go
+++ b/src/pkg/text/tabwriter/tabwriter.go
@@ -434,9 +434,13 @@ func (b *Writer) terminateCell(htab bool) int {
 	return len(*line)
 }
 
-func handlePanic(err *error) {
+func handlePanic(err *error, op string) {
 	if e := recover(); e != nil {
-		*err = e.(osError).err // re-panics if it's not a local osError
+		if nerr, ok := e.(osError); ok {
+			*err = nerr.err
+			return
+		}
+		panic("tabwriter: panic during " + op)
 	}
 }
 
@@ -447,7 +451,7 @@ func handlePanic(err *error) {
 //
 func (b *Writer) Flush() (err error) {
 	defer b.reset() // even in the presence of errors
-	defer handlePanic(&err)
+	defer handlePanic(&err, "Flush")
 
 	// add current cell if not empty
 	if b.cell.size > 0 {
@@ -471,7 +475,7 @@ var hbar = []byte("---\n")
 // while writing to the underlying output stream.
 //
 func (b *Writer) Write(buf []byte) (n int, err error) {
-	defer handlePanic(&err)
+	defer handlePanic(&err, "Write")
 
 	// split text into cells
 	n = 0

src/pkg/text/tabwriter/tabwriter_test.go

--- a/src/pkg/text/tabwriter/tabwriter_test.go
+++ b/src/pkg/text/tabwriter/tabwriter_test.go
@@ -613,3 +613,40 @@ func Test(t *testing.T) {
 		check(t, e.testname, e.minwidth, e.tabwidth, e.padding, e.padchar, e.flags, e.src, e.expected)
 	}\n}\n+\n+type panicWriter struct{}\n+\n+func (panicWriter) Write([]byte) (int, error) {\n+\tpanic("cannot write")\n+}\n+\n+func wantPanicString(t *testing.T, want string) {\n+\tif e := recover(); e != nil {\n+\t\tgot, ok := e.(string)\n+\t\tswitch {\n+\t\tcase !ok:\n+\t\t\tt.Errorf("got %v (%T), want panic string", e, e)\n+\t\tcase got != want:\n+\t\t\tt.Errorf("wrong panic message: got %q, want %q", got, want)\n+\t\t}\n+\t}\n+}\n+\n+func TestPanicDuringFlush(t *testing.T) {\n+\tdefer wantPanicString(t, "tabwriter: panic during Flush")\n+\tvar p panicWriter\n+\tw := new(Writer)\n+\tw.Init(p, 0, 0, 5, ' ', 0)\n+\tio.WriteString(w, "a")\n+\tw.Flush()\n+\tt.Errorf("failed to panic during Flush")\n+}\n+\n+func TestPanicDuringWrite(t *testing.T) {\n+\tdefer wantPanicString(t, "tabwriter: panic during Write")\n+\tvar p panicWriter\n+\tw := new(Writer)\n+\tw.Init(p, 0, 0, 5, ' ', 0)\n+\tio.WriteString(w, "a\\n\\n") // the second \\n triggers a call to w.Write and thus a panic\n+\tt.Errorf("failed to panic during Write")\n+}\n```

## コアとなるコードの解説

このコミットの核心は、`handlePanic` 関数のロジックの強化と、それを利用する `Flush` および `Write` メソッドの変更です。

1.  **`handlePanic` 関数のシグネチャ変更とロジックの改善**:
    *   `func handlePanic(err *error)` から `func handlePanic(err *error, op string)` へと変更され、パニックが発生した操作を示す `op` 文字列が追加されました。
    *   `recover()` で捕捉されたパニック `e` が `osError` 型であるかどうかを `if nerr, ok := e.(osError); ok` で安全にチェックするようになりました。
        *   もし `e` が `osError` 型であれば、その内部のエラー `nerr.err` を `err` ポインタに代入し、関数を正常終了させます。これは、`tabwriter` 自身が意図的に発生させたエラー(例えば、下層の `io.Writer` からのエラーをラップしたもの)を適切に処理するためのものです。
        *   もし `e` が `osError` 型でなければ(つまり、`tabwriter` の想定外のパニックである場合)、`panic("tabwriter: panic during " + op)` という新しいパニックを発生させます。この新しいパニックメッセージには、どの操作(`Flush` または `Write`)中にパニックが発生したかが明示的に含まれるため、デバッグ時の情報が格段に向上します。

2.  **`Flush` および `Write` メソッドでの `handlePanic` の呼び出し変更**:
    *   `defer handlePanic(&err)` の呼び出しが、それぞれ `defer handlePanic(&err, "Flush")` と `defer handlePanic(&err, "Write")` に変更されました。これにより、`handlePanic` 関数がパニック発生元の操作を識別できるようになり、より詳細なパニックメッセージの生成が可能になります。

3.  **新しいテストケースの追加**:
    *   `panicWriter` は、`Write` メソッドが常にパニックを発生させることで、`tabwriter` が下層の `io.Writer` からのパニックをどのように処理するかをシミュレートします。
    *   `wantPanicString` は、`recover` を使ってパニックを捕捉し、そのメッセージが期待通りであるかを検証する汎用的なヘルパー関数です。
    *   `TestPanicDuringFlush` と `TestPanicDuringWrite` は、それぞれ `Flush` と `Write` の実行中に `panicWriter` によってパニックが発生した場合に、`tabwriter` が生成するパニックメッセージが「`tabwriter: panic during Flush`」および「`tabwriter: panic during Write`」となることを確認します。これにより、変更が正しく機能していることが保証されます。

これらの変更により、`text/tabwriter` パッケージは、内部で発生した予期せぬパニックに対して、より情報量の多いバックトレースを提供するようになり、開発者が問題を迅速に特定し、デバッグするのに役立ちます。

## 関連リンク

*   Go言語の `text/tabwriter` パッケージのドキュメント: [https://pkg.go.dev/text/tabwriter](https://pkg.go.dev/text/tabwriter)
*   Go言語におけるパニックとリカバリに関する公式ブログ記事 (英語): [https://go.dev/blog/defer-panic-and-recover](https://go.dev/blog/defer-panic-and-recover)

## 参考にした情報源リンク

*   Go言語の公式リポジトリ: [https://github.com/golang/go](https://github.com/golang/go)
*   Go言語のコードレビューシステム (Gerrit): [https://go.dev/cl/53310044](https://go.dev/cl/53310044) (コミットメッセージに記載されている変更リストのURL)
*   Go言語のIssueトラッカー: [https://go.dev/issue/7117](https://go.dev/issue/7117) (ただし、この特定のコミットに関連するIssue 7117の公開情報は、一般的なWeb検索では直接見つかりませんでした。これは内部的なIssue管理番号であるか、または後に別のIssueに統合された可能性があります。)