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

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

このコミットは、Go言語の標準ライブラリであるtestingパッケージにおいて、パニック発生時のスタックトレースのフォーマット方法を改善するものです。具体的には、これまでtestingパッケージ内で独自に実装されていたスタックトレースの生成ロジックを、runtime/debugパッケージのStack()関数を使用するように変更しています。これにより、スタックトレースの出力がより標準的で読みやすくなり、特にtesting.go:nnn:のような余分なプレフィックスが各行に付加されるのを避けることができます。

コミット

commit f735d2d9d3d9665d0e5058615ac6f62e2ba79887
Author: Russ Cox <rsc@golang.org>
Date:   Sun Feb 12 23:39:40 2012 -0500

    testing: use runtime/debug to format panics
    
    Among other things, this avoids putting a testing.go:nnn:
    prefix on every line of the stack trace.
    
    R=golang-dev, r, dsymonds, r
    CC=golang-dev
    https://golang.org/cl/5651081

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

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

元コミット内容

testing: use runtime/debug to format panics

Among other things, this avoids putting a testing.go:nnn:
prefix on every line of the stack trace.

変更の背景

Go言語のテストフレームワークであるtestingパッケージは、テスト実行中に発生したパニック(Goにおけるランタイムエラーの一種)を捕捉し、そのスタックトレースを出力する機能を持っています。このコミット以前は、testingパッケージが独自にスタックトレースを生成・フォーマットしていました。しかし、この独自の実装にはいくつかの問題がありました。

主な問題点の一つは、生成されるスタックトレースの各行にtesting.go:nnn:のようなファイル名と行番号のプレフィックスが付加されていたことです。これは、スタックトレースの可読性を損ねるだけでなく、プログラムによる解析を困難にする可能性がありました。例えば、テストが失敗してパニックが発生した場合、開発者はスタックトレースを見て問題の箇所を特定しますが、余分なプレフィックスがあると視覚的にノイズとなり、本来のエラー情報が埋もれてしまうことがありました。

この問題を解決し、より標準的でクリーンなスタックトレースを提供するために、Goのランタイムデバッグ機能を提供するruntime/debugパッケージのStack()関数を利用するよう変更されました。runtime/debug.Stack()は、現在のゴルーチンのスタックトレースを標準的な形式でバイトスライスとして返すため、testingパッケージが独自にフォーマットする手間を省き、より一貫性のある出力を実現できます。

前提知識の解説

Goにおけるパニックとリカバリ (Panic and Recover)

Go言語には、プログラムの異常終了を扱うためのpanicrecoverというメカニズムがあります。

  • panic: 実行時エラーやプログラマが意図的に発生させる例外的な状況を示すために使用されます。panicが発生すると、現在の関数の実行が中断され、遅延関数(deferで登録された関数)が実行されながら、呼び出し元の関数へとスタックを遡っていきます。最終的にスタックの最上位まで到達すると、プログラムは異常終了します。
  • recover: defer関数内で呼び出されることで、panicによって中断されたゴルーチンの実行を捕捉し、パニックからの回復を試みることができます。recovernil以外の値を返した場合、それはパニックが発生したことを意味し、その値はpanicに渡された引数です。recoverがパニックを捕捉すると、そのゴルーチンの実行は正常な状態に戻り、プログラムの異常終了を防ぐことができます。testingパッケージでは、テスト中に発生したパニックを捕捉し、テストの失敗として記録するためにrecoverが利用されます。

runtime/debugパッケージ

runtime/debugパッケージは、Goプログラムのデバッグ情報にアクセスするための機能を提供します。このパッケージは、主に以下のような目的で使用されます。

  • スタックトレースの取得: Stack()関数は、現在のゴルーチンのスタックトレースをバイトスライスとして返します。これは、プログラムがクラッシュした際や、特定の時点での実行フローを把握したい場合に非常に役立ちます。
  • GC(ガベージコレクション)情報の取得: GCの統計情報や設定を取得・変更する機能を提供します。
  • ビルド情報の取得: プログラムのビルド時に埋め込まれた情報を取得できます。

このコミットでは、特にruntime/debug.Stack()関数が重要な役割を果たしています。

testingパッケージ

testingパッケージは、Go言語の標準的なテストフレームワークです。ユニットテスト、ベンチマークテスト、サンプルテストなどを記述するための機能を提供します。

  • *testing.T: ユニットテストの実行中にテストの状態を管理し、テストの失敗を報告したり、ログを出力したりするためのメソッドを提供します。
  • t.Logf() / t.Log(): テスト中に情報をログ出力するためのメソッドです。
  • t.FailNow() / t.Fatal() / t.Fatalf(): テストを失敗としてマークし、現在のテスト関数の実行を中断するためのメソッドです。

testingパッケージは、テストの実行中に発生したパニックを捕捉し、それをテストの失敗として扱うことで、テストの堅牢性を保っています。

技術的詳細

このコミットの技術的な核心は、testingパッケージがパニック発生時にスタックトレースを生成する方法を、手動でのフォーマットからruntime/debug.Stack()関数への委譲に切り替えた点にあります。

変更前は、testing.go内のcommon.stack()というプライベートメソッドが、runtime.Caller()関数を繰り返し呼び出すことで、スタックフレームの情報を手動で取得し、c.Logf()を使って各フレームのファイル名、行番号、関数名などを整形して出力していました。この手動でのフォーマットは、testing.go:nnn:のようなプレフィックスを意図せず追加してしまう原因となっていました。

変更後は、common.stack()メソッドが削除され、パニックを捕捉するdeferブロック内で直接debug.Stack()が呼び出されるようになりました。debug.Stack()は、Goランタイムが提供する標準的なスタックトレースフォーマッタであり、より簡潔で一貫性のある出力を保証します。これにより、testingパッケージはスタックトレースのフォーマットに関する詳細なロジックを持つ必要がなくなり、その責任をruntime/debugパッケージに委譲することで、コードの簡素化と品質の向上を実現しています。

また、src/pkg/runtime/debug/stack_test.goの変更も重要です。テストパッケージの名前がdebugからdebug_testに変更され、runtime/debugパッケージが.(ドット)エイリアスでインポートされています。これはGoのテストにおける一般的な慣習で、テスト対象のパッケージと同じ名前のテストパッケージを作成し、テスト対象のパッケージの関数や変数に直接アクセスできるようにするために行われます。これにより、debug.Stack()Stack()として直接呼び出すことが可能になります。

if falseで囲まれたrecover()ブロックは、このコミットの時点ではパニックからの回復ロジックが一時的に無効化されていることを示唆しています。これは、このコミットがスタックトレースのフォーマット変更に焦点を当てており、パニック処理全体のロジックは別のコミットで調整される可能性があったことを意味します。しかし、重要なのはdebug.Stack()の導入であり、この変更によってパニック時のスタックトレース出力が改善されたことです。

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

src/pkg/runtime/debug/stack_test.go

--- a/src/pkg/runtime/debug/stack_test.go
+++ b/src/pkg/runtime/debug/stack_test.go
@@ -2,9 +2,10 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package debug
+package debug_test
 
  import (
+\t. "runtime/debug"
  	"strings"
  	"testing"
  )
  • パッケージ名がdebugからdebug_testに変更されました。
  • runtime/debugパッケージが.エイリアスでインポートされました。

src/pkg/testing/testing.go

--- a/src/pkg/testing/testing.go
+++ b/src/pkg/testing/testing.go
@@ -71,6 +71,7 @@ import (
  	"fmt"
  	"os"
  	"runtime"
+\t"runtime/debug"
  	"runtime/pprof"
  	"strconv"
  	"strings"
@@ -225,19 +226,6 @@ func (c *common) Fatalf(format string, args ...interface{}) {
  	c.FailNow()
  }
  
-// TODO(dsymonds): Consider hooking into runtime·traceback instead.
-func (c *common) stack() {
-	for i := 2; ; i++ { // Caller we care about is the user, 2 frames up
-		pc, file, line, ok := runtime.Caller(i)
-		f := runtime.FuncForPC(pc)
-		if !ok || f == nil {
-			break
-		}
-		c.Logf("%s:%d (0x%x)", file, line, pc)
-		c.Logf("\t%s", f.Name())
-	}
-}
-
 // Parallel signals that this test is to be run in parallel with (and only with) 
 // other parallel tests in this CPU group.
 func (t *T) Parallel() {
@@ -260,11 +248,12 @@ func tRunner(t *T, test *InternalTest) {
  	// a call to runtime.Goexit, record the duration and send
  	// a signal saying that the test is done.
  	defer func() {
-\t\t// Consider any uncaught panic a failure.
-\t\tif err := recover(); err != nil {\n-\t\t\tt.failed = true\n-\t\t\tt.Log(err)\n-\t\t\tt.stack()\n+\t\tif false {\n+\t\t\t// Log and recover from panic instead of aborting binary.\n+\t\t\tif err := recover(); err != nil {\n+\t\t\t\tt.failed = true\n+\t\t\t\tt.Logf("%s\\n%s", err, debug.Stack())\n+\t\t\t}\n  \t\t}\
  
  	\tt.duration = time.Now().Sub(t.start)
  • runtime/debugパッケージがインポートされました。
  • common.stack()メソッドが完全に削除されました。
  • tRunner関数のdeferブロック内で、パニックを捕捉するrecover()の処理が変更されました。以前はt.stack()を呼び出していましたが、debug.Stack()を呼び出すように変更されました。また、このrecoverブロック全体がif falseで囲まれていますが、これは一時的な変更または別のコミットでの調整を意図している可能性があります。しかし、重要なのはdebug.Stack()の導入です。

コアとなるコードの解説

このコミットの主要な変更は、testingパッケージがパニックを処理し、スタックトレースを出力する方法の根本的な変更です。

  1. runtime/debugのインポート: src/pkg/testing/testing.goの冒頭でimport "runtime/debug"が追加されました。これにより、testingパッケージ内でruntime/debugパッケージの機能、特にdebug.Stack()関数を利用できるようになります。

  2. common.stack()メソッドの削除: 変更前は、common構造体(*testing.T*testing.Bの基底となる構造体)にstack()というメソッドが存在しました。このメソッドは、runtime.Caller()をループで呼び出し、現在のゴルーチンのコールスタックをフレームごとに手動で取得し、c.Logf()を使って整形して出力していました。この手動での整形が、testing.go:nnn:のような余分なプレフィックスをスタックトレースの各行に付加する原因となっていました。このコミットでは、この非効率的で問題のあるstack()メソッドが完全に削除されました。

  3. tRunnerにおけるパニック処理の変更: tRunner関数は、個々のテストを実行するゴルーチンを管理します。この関数内にはdeferブロックがあり、テスト実行中に発生したパニックを捕捉する役割を担っています。 変更前は、パニックが発生した場合(recover()nil以外を返した場合)、t.stack()を呼び出してスタックトレースを出力していました。 変更後、この部分が以下のように変更されました。

    		if false {
    			// Log and recover from panic instead of aborting binary.
    			if err := recover(); err != nil {
    				t.failed = true
    				t.Logf("%s\n%s", err, debug.Stack())
    			}
    		}
    

    ここで注目すべきは、t.Logf("%s\n%s", err, debug.Stack())という行です。これは、パニックメッセージ(err)と、debug.Stack()によって生成されたスタックトレースを結合してログに出力しています。debug.Stack()は、現在のゴルーチンのスタックトレースをバイトスライスとして返し、これをt.Logfに渡すことで、Goランタイムが提供する標準的なフォーマットでスタックトレースが出力されるようになります。これにより、以前のtesting.go:nnn:のような余分なプレフィックスが取り除かれ、よりクリーンで読みやすいスタックトレースが提供されるようになりました。

    なお、このrecoverブロック全体がif falseで囲まれているのは、このコミットの時点ではパニックからの回復ロジックが一時的に無効化されていることを示唆しています。これは、スタックトレースのフォーマット変更に焦点を当てたコミットであり、パニック処理全体のロジックは別のコミットで調整されるか、あるいはテストの実行方法によってはこのブロックが常に実行されないように意図されている可能性があります。しかし、debug.Stack()の導入という目的は達成されています。

この変更により、Goのテスト実行時に出力されるスタックトレースは、Goの他の部分で生成されるスタックトレースと一貫性を持つようになり、デバッグ体験が向上しました。

関連リンク

参考にした情報源リンク