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

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

このコミットは、Go言語のruntime/debugパッケージにおけるStack関数の出力に関するバグ修正です。具体的には、パッケージパスにドット(.)が含まれる場合に、スタックトレースの関数名が誤って表示される問題を解決します。これにより、debug.Stackが非推奨であるにもかかわらず、正しい結果を返すように改善されました。

コミット

commit d7c14655a9a022ea900d25b49bda3c474cd1b97c
Author: Vincent Vanackere <vincent.vanackere@gmail.com>
Date:   Mon Jan 27 14:00:00 2014 -0800

    runtime/debug: fix incorrect Stack output if package path contains a dot

    Although debug.Stack is deprecated, it should still return the correct result.
    Output before this CL (using a trivial library in $GOPATH/test.com/a):
    /home/vince/src/test.com/a/lib.go:9 (0x42311e)
            com/a.ShowStack: os.Stdout.Write(debug.Stack())

    Output with this CL applied:
    /home/vince/src/test.com/a/lib.go:9 (0x42311e)
            ShowStack: os.Stdout.Write(debug.Stack())

    LGTM=iant
    R=golang-codereviews, iant
    CC=golang-codereviews
    https://golang.org/cl/57330043

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

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

元コミット内容

このコミットは、runtime/debugパッケージのStack関数が、パッケージパスにドットが含まれる場合に誤ったスタックトレースを出力する問題を修正します。debug.Stackは非推奨ですが、それでも正しい結果を返す必要があります。

修正前は、$GOPATH/test.com/aのようなパッケージを使用すると、スタックトレースの関数名がcom/a.ShowStackのようにパッケージパスを含んで表示されていました。このコミットを適用すると、出力はShowStackのように純粋な関数名のみになります。

変更の背景

Go言語のruntime/debug.Stack()関数は、現在のゴルーチンのスタックトレースをバイトスライスとして返します。この情報は、デバッグやエラーハンドリングにおいて非常に重要です。しかし、この関数が生成するスタックトレースのフォーマットには、特定の条件下で問題がありました。

問題は、Goのパッケージパスがドット(.)を含む場合に発生しました。例えば、test.com/aのようなパッケージパスを持つコードが実行されると、debug.Stack()の出力において、関数名がcom/a.FunctionNameのように、パッケージパスの一部が関数名の一部として誤って解釈されて表示されていました。これは、スタックトレースの可読性を損ない、デバッグ作業を困難にする可能性がありました。

このコミットの目的は、debug.Stack()が非推奨であるにもかかわらず、その出力が常に正確であることを保証することです。特に、パッケージパスの構造に依存せず、純粋な関数名のみをスタックトレースに表示するように修正することが求められました。

前提知識の解説

Go言語のパッケージとパス

Go言語では、コードはパッケージにまとめられます。パッケージは、ファイルシステム上のディレクトリ構造に対応しており、そのパスは通常、バージョン管理システムのリポジトリパス(例: github.com/user/repo/package)や、ローカルのGOPATH内のパス(例: test.com/a)によって識別されます。

GOPATH

GOPATHは、Goのワークスペースのルートディレクトリを指定する環境変数です。Goのソースコード、コンパイルされたバイナリ、パッケージは通常、このGOPATHの構造に従って配置されます。例えば、$GOPATH/src/test.com/aにソースコードがある場合、そのパッケージはtest.com/aとしてインポートされます。

runtime/debugパッケージ

runtime/debugパッケージは、Goプログラムのデバッグ情報にアクセスするための機能を提供します。

  • debug.Stack(): 現在のゴルーチンのスタックトレースをバイトスライスとして返します。これは、プログラムがパニックを起こした際などに表示されるスタックトレースと似た形式です。
  • debug.PrintStack(): debug.Stack()が返すスタックトレースを標準エラー出力に直接出力します。

これらの関数は、プログラムの実行中にどの関数がどの順序で呼び出されたかを追跡するために使用されます。

スタックトレースのフォーマット

Goのスタックトレースは通常、以下の形式で表示されます。

<ファイルパス>:<行番号> (<プログラムカウンタ>)
        <パッケージパス>.<関数名>: <呼び出し元のコード>

例:

/home/user/project/main.go:10 (0x401000)
        main.myFunction: fmt.Println("Hello")

このコミットで問題となっていたのは、<パッケージパス>.<関数名>の部分の解析ロジックでした。

技術的詳細

このコミットの修正は、src/pkg/runtime/debug/stack.goファイル内のfunction(pc uintptr) []byte関数に焦点を当てています。この関数は、プログラムカウンタ(pc)から関数名を含むバイトスライスを抽出する役割を担っています。

元の実装では、関数名からパッケージパスを取り除くために、最初のドット(.)を区切り文字として使用していました。例えば、main.myFunctionという文字列からmain.を取り除き、myFunctionだけを残すようなロジックです。

しかし、Goのパッケージパス自体にドットが含まれる場合(例: test.com/a)、このロジックが問題を引き起こしました。test.com/a.ShowStackのような関数名が渡された場合、最初のドットがtestcom/a.ShowStackを区切るものと誤解され、結果としてcom/a.ShowStackが関数名として残ってしまいました。これは、期待されるShowStackとは異なる出力です。

この修正は、この問題を解決するために、関数名からパッケージパスをより正確に分離するロジックを追加しました。具体的には、関数名に含まれる最後のスラッシュ(/)を検索し、そのスラッシュ以降の部分を純粋な関数名として扱うように変更されました。これにより、test.com/a.ShowStackのような場合でも、まず最後のスラッシュ(/)でtest.com/aShowStackを区別し、その後、残ったShowStackに対して既存のドットによる処理(もしあれば)を適用することで、正しい関数名ShowStackを抽出できるようになります。

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

変更はsrc/pkg/runtime/debug/stack.goファイルにあります。

--- a/src/pkg/runtime/debug/stack.go
+++ b/src/pkg/runtime/debug/stack.go
@@ -18,6 +18,7 @@ var (\
 	dunno     = []byte("???")
 	centerDot = []byte("·")
 	dot       = []byte(".")
+	slash     = []byte("/")
 )

 // PrintStack prints to standard error the stack trace returned by Stack.
@@ -84,6 +85,11 @@ func function(pc uintptr) []byte {
 	//	runtime/debug.*T·ptrmethod
 	// and want
 	//	*T.ptrmethod
+	// Since the package path might contains dots (e.g. code.google.com/...),\n+\t// we first remove the path prefix if there is one.\n+\tif lastslash := bytes.LastIndex(name, slash); lastslash >= 0 {\n+\t\tname = name[lastslash+1:]\n+\t}\n     if period := bytes.Index(name, dot); period >= 0 {\n     	name = name[period+1:]
     	}

具体的には、以下の2点が変更されています。

  1. slashというバイトスライスが追加されました。これは、スラッシュ文字(/)を表します。
    +	slash     = []byte("/")
    
  2. function関数内で、既存のドットによる処理の前に、最後のスラッシュを検索してパスプレフィックスを削除するロジックが追加されました。
    +	// Since the package path might contains dots (e.g. code.google.com/...),\n+\t// we first remove the path prefix if there is one.\n+\tif lastslash := bytes.LastIndex(name, slash); lastslash >= 0 {\n+\t\tname = name[lastslash+1:]\n+\t}
    

コアとなるコードの解説

追加されたコードは、function関数内で関数名を整形する部分にあります。

if lastslash := bytes.LastIndex(name, slash); lastslash >= 0 {
	name = name[lastslash+1:]
}

このコードブロックの目的は、Goの関数名が完全修飾名(例: test.com/a.ShowStack)として渡された場合に、そのパッケージパス部分を正確に削除することです。

  1. bytes.LastIndex(name, slash):

    • nameは、現在の関数名を表すバイトスライスです。これには、パッケージパスが含まれている可能性があります(例: test.com/a.ShowStack)。
    • slashは、新しく定義されたバイトスライスで、スラッシュ文字(/)を含みます。
    • この関数は、nameバイトスライス内でslash(スラッシュ)が最後に現れるインデックスを検索します。
    • 例えば、test.com/a.ShowStackの場合、最後のスラッシュはtest.coma.ShowStackの間にあるスラッシュです。
  2. if lastslash >= 0:

    • bytes.LastIndexは、指定されたバイトスライスが見つからない場合、-1を返します。
    • この条件は、nameの中にスラッシュが見つかった場合にのみ、その後の処理を実行することを示しています。
  3. name = name[lastslash+1:]:

    • スラッシュが見つかった場合、nameバイトスライスを再スライスします。
    • lastslash + 1は、最後のスラッシュの直後の位置を指します。
    • name[lastslash+1:]は、最後のスラッシュの直後からバイトスライスの最後までを新しいnameとして設定します。
    • これにより、test.com/a.ShowStacka.ShowStackに変換されます。

この変更により、関数名からパッケージパスが正しく分離されるようになります。例えば、test.com/a.ShowStackはまずa.ShowStackになり、その後に既存のドット処理(period := bytes.Index(name, dot))が適用され、最終的にShowStackという純粋な関数名が抽出されます。

この修正は、パッケージパスにドットが含まれるという特殊なケースに対応するために、より堅牢なパス解析ロジックを導入したものです。

関連リンク

参考にした情報源リンク

  • https://golang.org/cl/57330043 (元のGo Gerrit Code Reviewリンク)
  • Go言語の公式ドキュメント (パッケージ、GOPATHに関する一般的な情報)
  • Go言語のソースコード (src/pkg/runtime/debug/stack.go)