[インデックス 10131] ファイルの概要
このコミットは、Go言語の標準ライブラリ内の複数のパッケージにおいて、fmt
パッケージの出力関数に渡される引数から冗長な.String()
メソッド呼び出しを削除する変更です。これは、fmt.Printf
などの関数がStringer
インターフェースを実装する型に対して自動的にString()
メソッドを呼び出すというGoの言語仕様に基づいた、コードのクリーンアップと改善を目的としています。
コミット
commit 32f3770ec51a8317214ac5b3725fb827c5b98e86
Author: Russ Cox <rsc@golang.org>
Date: Thu Oct 27 18:03:52 2011 -0700
pkg: remove .String() from some print arguments
I found these by adding a check to govet, but the check
produces far too many false positives to be useful.
Even so, these few seem worth cleaning up.
R=golang-dev, bradfitz, iant
CC=golang-dev
https://golang.org/cl/5311067
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/32f3770ec51a8317214ac5b3725fb827c5b98e86
元コミット内容
pkg: remove .String() from some print arguments
I found these by adding a check to govet, but the check
produces far too many false positives to be useful.
Even so, these few seem worth cleaning up.
R=golang-dev, bradfitz, iant
CC=golang-dev
https://golang.org/cl/5311067
変更の背景
このコミットの背景には、Go言語のfmt
パッケージの動作と、コード品質チェックツールgovet
の利用があります。
Go言語のfmt
パッケージ(fmt.Printf
, fmt.Println
, fmt.Errorf
など)は、引数として渡された値がfmt.Stringer
インターフェース(String() string
メソッドを持つインターフェース)を実装している場合、自動的にそのString()
メソッドを呼び出して文字列表現を取得します。このため、Stringer
を実装している型に対して明示的に.String()
を呼び出すことは冗長であり、場合によっては二重に文字列変換が行われるなど、非効率的になる可能性があります。
コミットメッセージによると、この変更はgovet
というGoのコード静的解析ツールに新しいチェックを追加した際に発見されたものです。govet
は、Goのソースコードを解析して疑わしい構成や潜在的なバグを検出するツールです。この新しいチェックは、fmt
パッケージの引数に.String()
が明示的に呼び出されている箇所を検出することを目的としていましたが、結果として「誤検知が多すぎる」と判断されました。しかし、その中でも特に修正する価値があると判断された少数の箇所が、このコミットでクリーンアップされています。
したがって、この変更の主な目的は、コードの冗長性を排除し、Goのイディオムに沿ったよりクリーンなコードベースを維持することにあります。
前提知識の解説
1. Go言語のfmt
パッケージ
fmt
パッケージは、Go言語におけるフォーマットされたI/O(入出力)を実装するためのパッケージです。C言語のprintf
/scanf
に似た機能を提供し、様々な型の値を文字列に変換して出力したり、文字列から値を読み取ったりすることができます。
fmt.Printf(format string, a ...interface{}) (n int, err error)
: フォーマット文字列に基づいて値を整形して出力します。fmt.Println(a ...interface{}) (n int, err error)
: 引数をスペースで区切り、改行を追加して出力します。fmt.Errorf(format string, a ...interface{}) error
: フォーマット文字列に基づいてエラーメッセージを整形し、新しいエラーを生成します。
これらの関数は、引数としてinterface{}
型を受け取ります。これはGoのポリモーフィズムの仕組みであり、任意の型の値を渡すことができます。
2. fmt.Stringer
インターフェース
Go言語には、特定のメソッドシグネチャを持つ型が自動的に満たす「インターフェース」という概念があります。fmt.Stringer
インターフェースは、その代表的な例の一つです。
type Stringer interface {
String() string
}
このインターフェースは、String() string
というメソッドを持つ任意の型によって実装されます。例えば、カスタムエラー型や構造体が自身の文字列表現を提供したい場合に、このインターフェースを実装します。
fmt
パッケージの出力関数は、引数として渡された値がfmt.Stringer
インターフェースを実装している場合、その値のString()
メソッドを自動的に呼び出して、その戻り値を文字列として扱います。これにより、開発者は明示的にvalue.String()
と書く必要がなく、コードが簡潔になります。
3. govet
ツール
govet
は、Go言語のソースコードを静的に解析し、疑わしいコード構成や潜在的なバグを検出するコマンドラインツールです。GoのSDKに標準で含まれています。
govet
が検出する問題の例としては、以下のようなものがあります。
Printf
系の関数におけるフォーマット文字列と引数の不一致- 到達不能なコード
- ロックの誤用
- 構造体タグの誤り
- シャドーイング(変数の隠蔽)
このコミットの背景にあるように、govet
は新しいチェックを追加することで、Goのイディオムに沿わないコードパターンを特定するのに役立ちます。
技術的詳細
このコミットの技術的な核心は、Go言語のfmt
パッケージがfmt.Stringer
インターフェースを実装する型をどのように扱うかという点にあります。
fmt.Printf
やfmt.Println
などの関数は、内部的に引数の型をチェックし、もしその型がfmt.Stringer
インターフェースを満たしていれば、自動的にそのString()
メソッドを呼び出して文字列を取得します。これは、%v
(デフォルトのフォーマット)や%s
(文字列フォーマット)などの動詞を使用した場合に特に顕著です。
例えば、error
型はGoの組み込みインターフェースであり、Error() string
メソッドを持っています。多くのerror
実装は、このError()
メソッドを通じてエラーの詳細な文字列表現を提供します。Goの慣習として、error
型はfmt.Stringer
インターフェースも暗黙的に満たすように設計されていることが多く、fmt.Printf("%v", err)
のように記述すると、err.Error()
が呼び出されてエラーメッセージが出力されます。
このコミットでは、以下のようなコードパターンが修正されています。
変更前:
t.Errorf("%s gave err %v but should have given %v", name, err.String(), expected.String())
log.Println("x11:", err.String())
fmt.Fprintf(b, "\"%s\": %v", key, val.String())
p.printf("%s (len = %d) {\n", x.Type().String(), x.Len())
return fmt.Sprintf("%s (and %d more errors)", p[0].String(), len(p)-1)
t.Errorf("bad token for %q: got %s, expected %s", lit, tok.String(), e.tok.String())
errorf("decode can't handle type %s", rt.String())
変更後:
t.Errorf("%s gave err %v but should have given %v", name, err, expected)
log.Println("x11:", err)
fmt.Fprintf(b, "\"%s\": %v", key, val)
p.printf("%s (len = %d) {\n", x.Type(), x.Len())
return fmt.Sprintf("%s (and %d more errors)", p[0], len(p)-1)
t.Errorf("bad token for %q: got %s, expected %s", lit, tok, e.tok)
errorf("decode can't handle type %s", rt)
これらの変更は、err.String()
やval.String()
のように明示的にString()
を呼び出す代わりに、err
やval
といった元の値を直接fmt
関数に渡すようにしています。これにより、fmt
パッケージが自動的にStringer
インターフェースのルールに従って適切な文字列表現を取得するため、コードがより簡潔になり、Goのイディオムに沿ったものになります。
この変更は、パフォーマンスに大きな影響を与えるものではありませんが、コードの可読性と保守性を向上させます。また、govet
のような静的解析ツールが、このような冗長なコードパターンを特定するのに役立つことを示しています。
コアとなるコードの変更箇所
このコミットでは、以下のファイルで.String()
メソッドの呼び出しが削除されています。
src/pkg/crypto/bcrypt/bcrypt_test.go
src/pkg/exp/gui/x11/conn.go
src/pkg/expvar/expvar.go
src/pkg/go/ast/print.go
src/pkg/go/scanner/errors.go
src/pkg/go/scanner/scanner_test.go
src/pkg/gob/decode.go
src/pkg/gob/encode.go
src/pkg/smtp/smtp_test.go
src/pkg/strconv/fp_test.go
具体的な変更例をいくつか示します。
src/pkg/crypto/bcrypt/bcrypt_test.go
--- a/src/pkg/crypto/bcrypt/bcrypt_test.go
+++ b/src/pkg/crypto/bcrypt/bcrypt_test.go
@@ -86,7 +86,7 @@ func TestInvalidHashErrors(t *testing.T) {
t.Errorf("%s: Should have returned an error", name)
}
if err != nil && err != expected {
- t.Errorf("%s gave err %v but should have given %v", name, err.String(), expected.String())
+ t.Errorf("%s gave err %v but should have given %v", name, err, expected)
}
}
for _, iht := range invalidTests {
ここでは、err.String()
とexpected.String()
がそれぞれerr
とexpected
に置き換えられています。error
型は通常Stringer
インターフェースを実装しているため、この変更は適切です。
src/pkg/exp/gui/x11/conn.go
--- a/src/pkg/exp/gui/x11/conn.go
+++ b/src/pkg/exp/gui/x11/conn.go
@@ -87,7 +87,7 @@ func (c *conn) writeSocket() {
setU32LE(c.flushBuf0[16:20], uint32(y<<16))
if _, err := c.w.Write(c.flushBuf0[:24]); err != nil {
if err != os.EOF {
- log.Println("x11:", err.String())
+ log.Println("x11:", err)
}
return
}
@@ -106,7 +106,7 @@ func (c *conn) writeSocket() {
tx += nx
if _, err := c.w.Write(c.flushBuf1[:nx]); err != nil {
if err != os.EOF {
- log.Println("x11:", err.String())
+ log.Println("x11:", err)
}
return
}
@@ -114,7 +114,7 @@ func (c *conn) writeSocket() {
}
if err := c.w.Flush(); err != nil {
if err != os.EOF {
- log.Println("x11:", err.String())
+ log.Println("x11:", err)
}
return
}
log.Println
に渡されるerr.String()
がerr
に修正されています。log.Println
もfmt.Println
と同様にStringer
インターフェースを自動的に処理します。
src/pkg/go/ast/print.go
--- a/src/pkg/go/ast/print.go
+++ b/src/pkg/go/ast/print.go
@@ -149,7 +149,7 @@ func (p *printer) print(x reflect.Value) {
p.print(x.Elem())
case reflect.Map:
- p.printf("%s (len = %d) {\n", x.Type().String(), x.Len())
+ p.printf("%s (len = %d) {\n", x.Type(), x.Len())
p.indent++
for _, key := range x.MapKeys() {
p.print(key)
@@ -178,7 +178,7 @@ func (p *printer) print(x reflect.Value) {
p.printf("%#q", s)
return
}
- p.printf("%s (len = %d) {\n", x.Type().String(), x.Len())
+ p.printf("%s (len = %d) {\n", x.Type(), x.Len())
p.indent++
for i, n := 0, x.Len(); i < n; i++ {
p.printf("%d: ", i)
@@ -189,7 +189,7 @@ func (p *printer) print(x reflect.Value) {
p.printf("}")
case reflect.Struct:
- p.printf("%s {\n", x.Type().String())
+ p.printf("%s {\n", x.Type())
p.indent++
t := x.Type()
for i, n := 0, t.NumField(); i < n; i++ {
reflect.Type
のString()
メソッド呼び出しが削除されています。reflect.Type
もStringer
インターフェースを実装しているため、p.printf
に直接渡すことで同様の動作が得られます。
コアとなるコードの解説
これらの変更は、Go言語のfmt
パッケージの設計思想と、fmt.Stringer
インターフェースの役割を深く理解していることを示しています。
Goのfmt
パッケージの関数(Printf
, Println
, Errorf
など)は、引数として渡された値の文字列表現を生成する際に、以下の優先順位で処理を行います。
%T
: 引数の型名を出力します。%v
(デフォルト):- 値が
fmt.Stringer
インターフェースを実装している場合、そのString()
メソッドの戻り値を使用します。 - 値が
error
インターフェースを実装している場合、そのError()
メソッドの戻り値を使用します。 - それ以外の場合、Goの構文に沿ったデフォルトの表現(構造体はフィールド名と値、ポインタはアドレスなど)を使用します。
- 値が
%s
: 文字列として扱います。引数が文字列でない場合、Stringer
インターフェースを実装していればそのString()
メソッドを呼び出し、そうでなければデフォルトの文字列表現を試みます。
このコミットで修正された箇所は、ほとんどがerror
型やreflect.Type
型、あるいはtoken.Token
型など、既にString()
メソッド(またはError()
メソッド)を実装しており、fmt
パッケージが自動的に適切な文字列表現を取得できる型でした。
したがって、err.String()
のように明示的にString()
を呼び出すことは、以下の点で冗長であり、Goのイディオムに反していました。
- 冗長なコード:
fmt
パッケージが自動的に処理するため、開発者が手動で呼び出す必要がありません。 - 潜在的な非効率性:
String()
メソッドが呼び出されて文字列が生成された後、その文字列がさらにfmt
パッケージによって処理されるため、場合によっては不要な中間文字列の生成が発生する可能性があります。 - 一貫性の欠如: Goの標準ライブラリや一般的なGoのコードでは、このような状況で明示的に
String()
を呼び出すことは稀です。コードベース全体で一貫したスタイルを保つことが重要です。
この変更は、これらの冗長な呼び出しを削除することで、コードをより簡潔にし、Goの慣習に沿ったものにしています。これは、コードの可読性と保守性を向上させる小さな改善ですが、大規模なプロジェクトではこのような細かな改善が全体の品質に寄与します。
関連リンク
- Gerrit Change-ID:
https://golang.org/cl/5311067
(GoプロジェクトのコードレビューシステムであるGerritの変更リストへのリンク)