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

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

このコミットは、Go言語のcmd/vetツールに新たな警告機能を追加し、fmt.Println(os.Stderr, ...)のような誤った使用パターンを検出できるようにするものです。また、net/http/httptestパッケージ内の既存のバグも修正しています。

コミット

commit 60e4d5668e80457023a3432752b2889fb73b89bf
Author: Shenghou Ma <minux.ma@gmail.com>
Date:   Tue Feb 14 11:24:41 2012 -0500

    cmd/vet: give warning for construct 'Println(os.Stderr, ...)'
            also fixes this bug in net/http/httptest.
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/5654083

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

https://github.com/golang/go/commit/60e4d5668e80457023a3432752b2889fb73b89bf

元コミット内容

cmd/vet: give warning for construct 'Println(os.Stderr, ...)' also fixes this bug in net/http/httptest.

このコミットは、cmd/vetツールがPrintln(os.Stderr, ...)という形式の呼び出しに対して警告を発するように変更します。これは、fmt.Printlnが可変引数を取るため、os.Stderrが最初の引数として渡されると、os.Stderrオブジェクト自体が出力されてしまうという一般的な誤用を指摘するためのものです。本来、標準エラー出力に書き込む場合はfmt.Fprintln(os.Stderr, ...)を使用すべきです。この変更は、net/http/httptestパッケージ内の同様のバグも修正しています。

変更の背景

Go言語のfmtパッケージには、様々な出力関数が用意されています。fmt.Print, fmt.Println, fmt.Printfなどは、デフォルトで標準出力(os.Stdout)に書き込みます。一方、特定のio.Writerに書き込みたい場合は、fmt.Fprint, fmt.Fprintln, fmt.Fprintfといった関数を使用し、最初の引数としてio.Writerインターフェースを満たすオブジェクト(例: os.Stderr)を渡します。

しかし、fmt.Printlnのような関数は可変引数(...interface{})を取るため、誤ってfmt.Println(os.Stderr, "エラーメッセージ")のように記述してしまうことがあります。この場合、fmt.Printlnos.Stderrオブジェクト自体を文字列としてフォーマットし、その後に"エラーメッセージ"を出力してしまいます。これは開発者の意図とは異なり、デバッグ情報の出力が期待通りに行われない原因となります。

このコミットは、このような一般的な間違いを静的解析ツールであるcmd/vetで検出し、開発者に警告することで、より堅牢で意図通りのコード記述を促進することを目的としています。また、Go標準ライブラリ内のnet/http/httptestパッケージにもこの誤用が存在していたため、その修正も同時に行われています。

前提知識の解説

Go言語のfmtパッケージ

fmtパッケージは、Go言語におけるフォーマットされたI/Oを実装するためのパッケージです。主に以下の種類の関数を提供します。

  • Print系: デフォルトの出力先(os.Stdout)に引数をフォーマットして出力します。
    • fmt.Print(a ...interface{}) (n int, err error): 引数をデフォルトのフォーマットで出力します。
    • fmt.Println(a ...interface{}) (n int, err error): 引数をデフォルトのフォーマットで出力し、最後に改行を追加します。
    • fmt.Printf(format string, a ...interface{}) (n int, err error): フォーマット文字列に従って引数を出力します。
  • Fprint系: 指定されたio.Writerに引数をフォーマットして出力します。
    • fmt.Fprint(w io.Writer, a ...interface{}) (n int, err error)
    • fmt.Fprintln(w io.Writer, a ...interface{}) (n int, err error)
    • fmt.Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error)
  • Sprint系: 引数をフォーマットして文字列として返します。
    • fmt.Sprint(a ...interface{}) string
    • fmt.Sprintln(a ...interface{}) string
    • fmt.Sprintf(format string, a ...interface{}) string

このコミットで問題となっているのは、fmt.Printlnが可変引数を取るため、os.Stderrを最初の引数として渡すと、os.Stderr自体がフォーマットされて出力されてしまう点です。正しくは、os.Stderrに書き込む場合はfmt.Fprintlnを使用し、最初の引数にos.Stderrを明示的に渡す必要があります。

os.Stdoutos.Stderr

Go言語のosパッケージは、オペレーティングシステムとのインタフェースを提供します。その中で、os.Stdoutos.Stderrは、それぞれ標準出力と標準エラー出力を表す*os.File型の変数です。これらはio.Writerインターフェースを満たしており、fmt.Fprintなどの関数に渡すことができます。

  • os.Stdout: プログラムの標準出力ストリーム。通常、コンソールに表示されます。
  • os.Stderr: プログラムの標準エラー出力ストリーム。通常、エラーメッセージや診断情報の出力に使用され、標準出力とは別に扱われます。

cmd/vetツール

cmd/vetは、Go言語のソースコードを静的に解析し、疑わしい構成や一般的なエラーを報告するツールです。コンパイラが検出できないが、実行時に問題を引き起こす可能性のあるコードパターンを特定するのに役立ちます。例えば、Printfのフォーマット文字列と引数の不一致、構造体タグの誤り、ロックの誤用などを検出します。

vetはGoのツールチェインの一部であり、go vetコマンドとして実行できます。このコミットは、vetが検出できる問題のリストに新たな項目を追加するものです。

Go言語のAST (Abstract Syntax Tree)

Go言語のコンパイラやツールは、ソースコードを直接扱うのではなく、その抽象構文木(AST)を生成して解析します。ASTは、プログラムの構造を木構造で表現したものです。go/astパッケージは、GoプログラムのASTを表現するための型と関数を提供します。

cmd/vetのような静的解析ツールは、このASTを走査し、特定のパターン(例えば、関数呼び出しの引数)を検出することで、コードの健全性をチェックします。このコミットでは、fmt.Print系の関数呼び出しのASTを解析し、最初の引数がos.Stderrのような標準エラー出力オブジェクトであるかどうかをチェックしています。

技術的詳細

このコミットの技術的詳細の中心は、cmd/vetツールがどのようにしてPrintln(os.Stderr, ...)のような誤用を検出するか、そしてその検出ロジックがどのように実装されているかです。

cmd/vetprint.goファイルは、fmtパッケージのPrint系関数の呼び出しを解析し、潜在的な問題を検出する役割を担っています。このコミットでは、checkPrint関数に新しいロジックが追加されています。

追加されたロジックは以下の条件をチェックします。

  1. skip == 0: これは、Print系の関数呼び出しにおいて、最初の引数から出力対象となることを意味します。例えば、fmt.Printfmt.Printlnは最初の引数から出力対象ですが、fmt.Fprintは最初の引数がio.Writerであるため、skipの値が異なります。このチェックにより、fmt.Fprintのような関数が誤って警告されないようにしています。
  2. !isF: これは、関数名がFで始まらないことを意味します。つまり、fmt.Fprintfmt.Fprintlnfmt.FprintfのようなFプレフィックスを持つ関数は対象外とします。これらの関数はio.Writerを最初の引数として取るのが正しい使用法であるため、警告の対象外です。
  3. len(args) > 0: 呼び出しに引数が存在することを確認します。
  4. 最初の引数の型チェック: 最初の引数(args[0])が*ast.SelectorExpr(セレクタ式、例: os.Stderr)であるかどうかをチェックします。
    • もしセレクタ式であれば、そのセレクタのX部分(例: os)が*ast.Ident(識別子)であり、その名前が"os"であるかをチェックします。
    • さらに、セレクタのSel部分(例: Stderr)が*ast.Identであり、その名前が"Std"で始まる(例: Stdout, Stderr)かをチェックします。

これらの条件がすべて満たされた場合、f.Warnfを呼び出して警告メッセージを生成します。警告メッセージは、「%sの最初の引数が%s.%sです」という形式で、関数名(例: Println)と、誤って渡されたos.Stderrのような識別子を表示します。

この静的解析は、コンパイル時に実行されるため、開発者はコードを実行する前に潜在的な問題を特定し、修正することができます。

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

src/cmd/vet/print.go

--- a/src/cmd/vet/print.go
+++ b/src/cmd/vet/print.go
@@ -207,7 +207,18 @@ func (f *File) checkPrintfVerb(call *ast.CallExpr, verb rune, flags []byte) {
 // call.Args[skip] is the first argument to be printed.
 func (f *File) checkPrint(call *ast.CallExpr, name string, skip int) {
 	isLn := strings.HasSuffix(name, "ln")
+	isF := strings.HasPrefix(name, "F")
 	args := call.Args
+	// check for Println(os.Stderr, ...)
+	if skip == 0 && !isF && len(args) > 0 {
+		if sel, ok := args[0].(*ast.SelectorExpr); ok {
+			if x, ok := sel.X.(*ast.Ident); ok {
+				if x.Name == "os" && strings.HasPrefix(sel.Sel.Name, "Std") {
+					f.Warnf(call.Pos(), "first argument to %s is %s.%s", name, x.Name, sel.Sel.Name)
+				}
+			}
+		}
+	}
 	if len(args) <= skip {
 		if *verbose && !isLn {
 			f.Badf(call.Pos(), "no args in %s call", name)

src/pkg/net/http/httptest/server.go

--- a/src/pkg/net/http/httptest/server.go
+++ b/src/pkg/net/http/httptest/server.go
@@ -95,7 +95,7 @@ func (s *Server) Start() {
 	s.URL = "http://" + s.Listener.Addr().String()
 	go s.Config.Serve(s.Listener)
 	if *serve != "" {
-		fmt.Println(os.Stderr, "httptest: serving on", s.URL)
+		fmt.Fprintln(os.Stderr, "httptest: serving on", s.URL)
 		select {}
 	}
 }

コアとなるコードの解説

src/cmd/vet/print.goの変更

checkPrint関数は、fmt.Printfmt.Printlnなどの呼び出しを解析する主要な関数です。 追加されたコードブロックは、以下のロジックで誤ったPrintln(os.Stderr, ...)パターンを検出します。

  1. isF := strings.HasPrefix(name, "F"): 関数名がFで始まるかどうかをチェックする新しいフラグisFが導入されました。これはfmt.Fprintなどの関数を区別するために使用されます。
  2. if skip == 0 && !isF && len(args) > 0:
    • skip == 0: これは、fmt.Printfmt.Printlnのように、最初の引数から出力対象となる関数であることを意味します。fmt.Fprintのような関数ではskipが1になります。
    • !isF: 関数名がFで始まらないことを確認します。これにより、fmt.Fprintなどの正しい使用法が警告されないようにします。
    • len(args) > 0: 呼び出しに引数が存在することを確認します。
  3. if sel, ok := args[0].(*ast.SelectorExpr); ok: 最初の引数がセレクタ式(例: os.Stderr)であるかをチェックします。*ast.SelectorExprは、X.Selのような形式の式を表します。
  4. if x, ok := sel.X.(*ast.Ident); ok: セレクタ式のX部分(例: os)が識別子であるかをチェックします。
  5. if x.Name == "os" && strings.HasPrefix(sel.Sel.Name, "Std"):
    • 識別子の名前が"os"であるかをチェックします。
    • セレクタの選択部分(例: Stderr)の名前が"Std"で始まる(例: Stdout, Stderr)かをチェックします。
  6. f.Warnf(call.Pos(), "first argument to %s is %s.%s", name, x.Name, sel.Sel.Name): 上記の条件がすべて満たされた場合、vetは警告を発します。警告メッセージは、どの関数(例: Println)の最初の引数がos.Stderrのようなosパッケージの標準出力/エラー出力オブジェクトであるかを明確に示します。

この変更により、cmd/vetは開発者が意図しない出力を行う可能性のあるコードパターンを早期に特定できるようになります。

src/pkg/net/http/httptest/server.goの変更

このファイルでは、httptestパッケージ内の既存のバグが修正されています。

  • - fmt.Println(os.Stderr, "httptest: serving on", s.URL)
  • + fmt.Fprintln(os.Stderr, "httptest: serving on", s.URL)

変更前はfmt.Printlnが使用されており、os.Stderrが最初の引数として渡されていました。これにより、os.Stderrオブジェクト自体が文字列として出力され、その後にメッセージが続くという意図しない動作になっていました。

変更後はfmt.Fprintlnに修正されています。fmt.Fprintlnは最初の引数としてio.Writerを受け取るため、os.Stderrに直接メッセージを書き込むという正しい動作になります。この修正は、cmd/vetの新しい警告機能が検出する問題の典型的な例であり、標準ライブラリ自体がこの新しいチェックの恩恵を受けていることを示しています。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコード(特にcmd/vetfmtパッケージの実装)
  • Go言語に関する技術ブログやフォーラムでの議論(fmt.Printlnの誤用に関するもの)
  • Goのコードレビュープロセスに関する情報(R=golang-dev, CC=golang-dev, CLリンクなど)