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

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

このコミットは、Go言語の標準ライブラリtestingパッケージにおけるgodocの出力改善を目的としたものです。具体的には、godocがエクスポートされていない埋め込みフィールドのメソッドを適切に表示しないという当時の問題に対する一時的な回避策として、ラッパーメソッドを追加しています。

コミット

commit 416afcb411d7b2fe59d38257bcfe0df3a903919e
Author: Rob Pike <r@golang.org>
Date:   Thu Dec 22 17:17:19 2011 -0800

    testing: add wrapper methods so the godoc output lists all methods
    To be deleted when godoc catches up.
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/5504079

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

https://github.com/golang/go/commit/416afcb411d7b2fe59d38257bcfe0df3a903919e

元コミット内容

testing: add wrapper methods so the godoc output lists all methods To be deleted when godoc catches up.

このコミットは、godocの出力がすべてのメソッドをリストするように、ラッパーメソッドを追加します。これはgodocが改善された際に削除される予定の一時的な変更です。

変更の背景

Go言語のtestingパッケージには、テスト(*T)やベンチマーク(*B)の実行中にエラー報告やログ出力を行うための様々なメソッド(例: Error, Errorf, Log, Logf, Fail, FailNowなど)が提供されています。これらのメソッドは、commonという非公開(アンエクスポート)な構造体を*T*Bに埋め込むことで実装されていました。

当時のgodocツールには、非公開の埋め込みフィールドを通じて提供されるメソッドを、その埋め込み先の型(この場合は*T*B)の公開メソッドとして適切にドキュメントに表示しないという問題がありました。これにより、開発者がgodocを使ってtestingパッケージのドキュメントを参照した際に、*T*Bが提供する重要なメソッド群が欠落して見えてしまうというユーザビリティ上の課題がありました。

このコミットは、godocのこの挙動が修正されるまでの間、一時的な回避策として、*T*Bのそれぞれに、commonフィールドのメソッドを直接呼び出す公開ラッパーメソッドを追加することで、godocがこれらのメソッドを正しく認識し、ドキュメントに表示するようにすることを目的としています。コミットメッセージにある「To be deleted when godoc catches up.」という記述は、この変更が一時的なものであることを明確に示しています。

前提知識の解説

Go言語の埋め込み(Embedding)

Go言語では、構造体の中に別の構造体を「埋め込む」ことができます。これは、他の言語における継承に似た機能を提供しますが、Goでは「コンポジション(合成)」として扱われます。埋め込まれた構造体のフィールドやメソッドは、埋め込み先の構造体のフィールドやメソッドであるかのように直接アクセスできます。

例:

type Common struct {
    name string
}

func (c *Common) Log(msg string) {
    fmt.Println(c.name + ": " + msg)
}

type MyType struct {
    Common // Common構造体を埋め込み
    id int
}

func main() {
    m := MyType{Common: Common{name: "MyInstance"}, id: 1}
    m.Log("Hello") // MyTypeのインスタンスからCommonのLogメソッドを直接呼び出せる
}

この場合、MyTypeCommonLogメソッドを「継承」しているように見えます。しかし、godocは、埋め込まれたフィールドが非公開(小文字で始まる)である場合、そのフィールドのメソッドを埋め込み先の型の公開メソッドとして認識しないという問題がありました。testingパッケージのcommonフィールドがまさにこのケースに該当していました。

godocツール

godocは、Go言語のソースコードからドキュメントを生成するためのツールです。Goのコードは、コメントの書き方によって自動的にドキュメントとして抽出され、Webブラウザで閲覧可能な形式で提供されます。godocは、Goの標準的なドキュメンテーションシステムの中核をなすものであり、開発者がライブラリやパッケージのAPIを理解するために不可欠なツールです。

runtime.Caller

runtime.Callerは、Goのruntimeパッケージが提供する関数で、現在のゴルーチンのコールスタックに関する情報を取得するために使用されます。 func Caller(skip int) (pc uintptr, file string, line int, ok bool) skip引数は、スタックフレームをスキップする数を指定します。skip=0Caller自身の呼び出し元、skip=1Callerを呼び出した関数、といった具合です。 testingパッケージのdecorate関数では、エラーメッセージにファイル名と行番号を含めるためにruntime.Callerを使用しています。このコミットでは、ラッパーメソッドが追加されたことでコールスタックの深さが変わり、正しい呼び出し元の情報を取得するためにskipの値が調整されています。

技術的詳細

このコミットの主要な変更は、src/pkg/testing/wrapper.goという新しいファイルを追加し、testing.go内のdecorate関数のruntime.Callerの引数を変更し、Makefileを更新することです。

src/pkg/testing/wrapper.goの追加

このファイルは、*B(ベンチマーク)と*T(テスト)のそれぞれに対して、common構造体に埋め込まれたメソッド(Fail, Failed, FailNow, Log, Logf, Error, Errorf, Fatal, Fatalf)の公開ラッパーメソッドを定義しています。

例: *BFailメソッドのラッパー

// Fail marks the function as having failed but continues execution.
func (b *B) Fail() {
	b.common.Fail()
}

これらのラッパーメソッドは、単にb.common.Fail()のように、内部のcommonフィールドの対応するメソッドを呼び出すだけです。これにより、godoc*B.Fail()*T.Error()といった公開メソッドとしてこれらを認識し、ドキュメントに表示できるようになります。

src/pkg/testing/testing.goの変更

decorate関数は、エラーメッセージにファイル名と行番号を追加するために使用されます。この関数内でruntime.Callerが呼び出され、コールスタックを遡って呼び出し元の情報を取得します。

変更前:

		_, file, line, ok := runtime.Caller(3) // decorate + log + public function.

変更後:

		_, file, line, ok := runtime.Caller(4) // decorate + log + public function.

runtime.Callerskip引数が3から4に変更されています。これは、wrapper.goで追加されたラッパーメソッドがコールスタックに1層追加されたため、正しい呼び出し元の情報を取得するためには、さらに1つスタックフレームをスキップする必要があるためです。

src/pkg/testing/Makefileの変更

新しいファイルwrapper.goが追加されたため、testingパッケージをビルドする際にこのファイルがコンパイル対象に含まれるようにMakefileが更新されています。

変更前:

GOFILES=\
        benchmark.go\
        example.go\
        testing.go\

変更後:

GOFILES=\
	benchmark.go\
	example.go\
	testing.go\
	wrapper.go\

wrapper.goGOFILESリストに追加され、ビルドシステムがこの新しいソースファイルを認識するようになります。

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

src/pkg/testing/Makefile

--- a/src/pkg/testing/Makefile
+++ b/src/pkg/testing/Makefile
@@ -6,8 +6,9 @@ include ../../Make.inc
 
 TARG=testing
 GOFILES=\
-        benchmark.go\\\
-        example.go\\\
+\tbenchmark.go\\\
+\texample.go\\\
 \ttesting.go\\\
+\twrapper.go\\\
 
 include ../../Make.pkg

src/pkg/testing/testing.go

--- a/src/pkg/testing/testing.go
+++ b/src/pkg/testing/testing.go
@@ -90,7 +90,7 @@ func Short() bool {
 // If addFileLine is true, it also prefixes the string with the file and line of the call site.
 func decorate(s string, addFileLine bool) string {\
 	if addFileLine {\
-\t\t_, file, line, ok := runtime.Caller(3) // decorate + log + public function.\
+\t\t_, file, line, ok := runtime.Caller(4) // decorate + log + public function.\
 \t\tif ok {\
 \t\t\t// Truncate file name at last file name separator.\
 \t\t\tif index := strings.LastIndex(file, \"/\"); index >= 0 {\

src/pkg/testing/wrapper.go (新規ファイル)

// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// This file contains wrappers so t.Errorf etc. have documentation.
// TODO: delete when godoc shows exported methods for unexported embedded fields.
// TODO: need to change the argument to runtime.Caller in testing.go from 4 to 3 at that point.

package testing

// Fail marks the function as having failed but continues execution.
func (b *B) Fail() {
	b.common.Fail()
}

// Failed returns whether the function has failed.
func (b *B) Failed() bool {
	return b.common.Failed()
}

// FailNow marks the function as having failed and stops its execution.
// Execution will continue at the next Test.
func (b *B) FailNow() {
	b.common.FailNow()
}

// Log formats its arguments using default formatting, analogous to Println(),
// and records the text in the error log.
func (b *B) Log(args ...interface{}) {
	b.common.Log(args...)
}

// Logf formats its arguments according to the format, analogous to Printf(),
// and records the text in the error log.
func (b *B) Logf(format string, args ...interface{}) {
	b.common.Logf(format, args...)
}

// Error is equivalent to Log() followed by Fail().
func (b *B) Error(args ...interface{}) {
	b.common.Error(args...)
}

// Errorf is equivalent to Logf() followed by Fail().
func (b *B) Errorf(format string, args ...interface{}) {
	b.common.Errorf(format, args...)
}

// Fatal is equivalent to Log() followed by FailNow().
func (b *B) Fatal(args ...interface{}) {
	b.common.Fatal(args...)
}

// Fatalf is equivalent to Logf() followed by FailNow().
func (b *B) Fatalf(format string, args ...interface{}) {
	b.common.Fatalf(format, args...)
}

// Fail marks the function as having failed but continues execution.
func (t *T) Fail() {
	t.common.Fail()
}

// Failed returns whether the function has failed.
func (t *T) Failed() bool {
	return t.common.Failed()
}

// FailNow marks the function as having failed and stops its execution.
// Execution will continue at the next Test.
func (t *T) FailNow() {
	t.common.FailNow()
}

// Log formats its arguments using default formatting, analogous to Println(),
// and records the text in the error log.
func (t *T) Log(args ...interface{}) {
	t.common.Log(args...)
}

// Logf formats its arguments according to the format, analogous to Printf(),
// and records the text in the error log.
func (t *T) Logf(format string, args ...interface{}) {
	t.common.Logf(format, args...)
}

// Error is equivalent to Log() followed by Fail().
func (t *T) Error(args ...interface{}) {
	t.common.Error(args...)
}

// Errorf is equivalent to Logf() followed by Fail().
func (t *T) Errorf(format string, args ...interface{}) {
	t.common.Errorf(format, args...)
}

// Fatal is equivalent to Log() followed by FailNow().
func (t *T) Fatal(args ...interface{}) {
	t.common.Fatal(args...)
}

// Fatalf is equivalent to Logf() followed by FailNow().
func (t *T) Fatalf(format string, args ...interface{}) {
	t.common.Fatalf(format, args...)
}

コアとなるコードの解説

このコミットの核心は、godocの制限を回避するために、testingパッケージの*T*B型に明示的なラッパーメソッドを追加した点にあります。

testingパッケージでは、*T*Bの構造体は、commonという非公開のフィールドを埋め込んでいます。このcommonフィールドは、テストやベンチマークの実行中に共通のロジック(エラー報告、ログ記録など)を処理するためのメソッド群(例: Fail, Log, Errorなど)を保持しています。Goの埋め込みの仕組みにより、*T*Bのインスタンスからt.Log()のように直接これらのメソッドを呼び出すことができます。

しかし、当時のgodocは、非公開の埋め込みフィールドを通じて提供されるメソッドを、埋め込み先の型の公開APIとして適切にドキュメント化しませんでした。このため、開発者がgodoctestingパッケージのドキュメントを見た際に、*T*Bが提供する重要なメソッド群がリストされず、APIの全貌を把握することが困難でした。

wrapper.goで追加されたラッパーメソッドは、この問題を解決するための一時的な手段です。これらのラッパーは、*T*Bの公開メソッドとして定義され、内部的には単にcommonフィールドの対応するメソッドを呼び出すだけです。これにより、godocはこれらのラッパーメソッドを公開APIとして認識し、ドキュメントに含めることができるようになります。

testing.godecorate関数におけるruntime.Callerの引数変更は、このラッパーの追加に伴う副作用への対応です。decorate関数は、エラーメッセージのソースコード位置を特定するためにコールスタックを遡ります。ラッパーメソッドが導入されたことで、実際の呼び出し元とdecorate関数の間にラッパーメソッドのスタックフレームが1つ追加されるため、runtime.Callerskip引数を3から4に増やすことで、正しい呼び出し元のファイルと行番号を取得できるように調整されました。

この変更は、godocの機能が改善され、非公開の埋め込みフィールドのメソッドを適切にドキュメント化できるようになれば、削除される予定の一時的な解決策として位置づけられています。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • godocツールの動作に関する一般的な知識
  • runtime.Callerの挙動に関する一般的な知識
  • Go言語の埋め込みに関する一般的な知識
  • コミットメッセージとコードの変更点
  • Go言語のIssueトラッカーやメーリングリストでの関連議論 (具体的なリンクはコミットメッセージのhttps://golang.org/cl/5504079から辿れる可能性あり)
  • Go言語のソースコードリポジトリI have generated the detailed technical explanation in Markdown format, following all the specified instructions and chapter structure. The output is now ready.