[インデックス 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.Println
はos.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.Stdout
とos.Stderr
Go言語のos
パッケージは、オペレーティングシステムとのインタフェースを提供します。その中で、os.Stdout
とos.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/vet
のprint.go
ファイルは、fmt
パッケージのPrint
系関数の呼び出しを解析し、潜在的な問題を検出する役割を担っています。このコミットでは、checkPrint
関数に新しいロジックが追加されています。
追加されたロジックは以下の条件をチェックします。
skip == 0
: これは、Print
系の関数呼び出しにおいて、最初の引数から出力対象となることを意味します。例えば、fmt.Print
やfmt.Println
は最初の引数から出力対象ですが、fmt.Fprint
は最初の引数がio.Writer
であるため、skip
の値が異なります。このチェックにより、fmt.Fprint
のような関数が誤って警告されないようにしています。!isF
: これは、関数名がF
で始まらないことを意味します。つまり、fmt.Fprint
、fmt.Fprintln
、fmt.Fprintf
のようなF
プレフィックスを持つ関数は対象外とします。これらの関数はio.Writer
を最初の引数として取るのが正しい使用法であるため、警告の対象外です。len(args) > 0
: 呼び出しに引数が存在することを確認します。- 最初の引数の型チェック: 最初の引数(
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.Print
、fmt.Println
などの呼び出しを解析する主要な関数です。
追加されたコードブロックは、以下のロジックで誤ったPrintln(os.Stderr, ...)
パターンを検出します。
isF := strings.HasPrefix(name, "F")
: 関数名がF
で始まるかどうかをチェックする新しいフラグisF
が導入されました。これはfmt.Fprint
などの関数を区別するために使用されます。if skip == 0 && !isF && len(args) > 0
:skip == 0
: これは、fmt.Print
やfmt.Println
のように、最初の引数から出力対象となる関数であることを意味します。fmt.Fprint
のような関数ではskip
が1になります。!isF
: 関数名がF
で始まらないことを確認します。これにより、fmt.Fprint
などの正しい使用法が警告されないようにします。len(args) > 0
: 呼び出しに引数が存在することを確認します。
if sel, ok := args[0].(*ast.SelectorExpr); ok
: 最初の引数がセレクタ式(例:os.Stderr
)であるかをチェックします。*ast.SelectorExpr
は、X.Sel
のような形式の式を表します。if x, ok := sel.X.(*ast.Ident); ok
: セレクタ式のX
部分(例:os
)が識別子であるかをチェックします。if x.Name == "os" && strings.HasPrefix(sel.Sel.Name, "Std")
:- 識別子の名前が
"os"
であるかをチェックします。 - セレクタの選択部分(例:
Stderr
)の名前が"Std"
で始まる(例:Stdout
,Stderr
)かをチェックします。
- 識別子の名前が
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言語の
fmt
パッケージドキュメント: https://pkg.go.dev/fmt - Go言語の
os
パッケージドキュメント: https://pkg.go.dev/os cmd/vet
の公式ドキュメント(go vet
コマンドについて): https://pkg.go.dev/cmd/vet- Go言語のASTパッケージドキュメント: https://pkg.go.dev/go/ast
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード(特に
cmd/vet
とfmt
パッケージの実装) - Go言語に関する技術ブログやフォーラムでの議論(
fmt.Println
の誤用に関するもの) - Goのコードレビュープロセスに関する情報(R=golang-dev, CC=golang-dev, CLリンクなど)