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

[インデックス 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.Printffmt.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()を呼び出す代わりに、errvalといった元の値を直接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()がそれぞれerrexpectedに置き換えられています。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.Printlnfmt.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.TypeString()メソッド呼び出しが削除されています。reflect.TypeStringerインターフェースを実装しているため、p.printfに直接渡すことで同様の動作が得られます。

コアとなるコードの解説

これらの変更は、Go言語のfmtパッケージの設計思想と、fmt.Stringerインターフェースの役割を深く理解していることを示しています。

Goのfmtパッケージの関数(Printf, Println, Errorfなど)は、引数として渡された値の文字列表現を生成する際に、以下の優先順位で処理を行います。

  1. %T: 引数の型名を出力します。
  2. %v (デフォルト):
    • 値がfmt.Stringerインターフェースを実装している場合、そのString()メソッドの戻り値を使用します。
    • 値がerrorインターフェースを実装している場合、そのError()メソッドの戻り値を使用します。
    • それ以外の場合、Goの構文に沿ったデフォルトの表現(構造体はフィールド名と値、ポインタはアドレスなど)を使用します。
  3. %s: 文字列として扱います。引数が文字列でない場合、Stringerインターフェースを実装していればそのString()メソッドを呼び出し、そうでなければデフォルトの文字列表現を試みます。

このコミットで修正された箇所は、ほとんどがerror型やreflect.Type型、あるいはtoken.Token型など、既にString()メソッド(またはError()メソッド)を実装しており、fmtパッケージが自動的に適切な文字列表現を取得できる型でした。

したがって、err.String()のように明示的にString()を呼び出すことは、以下の点で冗長であり、Goのイディオムに反していました。

  1. 冗長なコード: fmtパッケージが自動的に処理するため、開発者が手動で呼び出す必要がありません。
  2. 潜在的な非効率性: String()メソッドが呼び出されて文字列が生成された後、その文字列がさらにfmtパッケージによって処理されるため、場合によっては不要な中間文字列の生成が発生する可能性があります。
  3. 一貫性の欠如: Goの標準ライブラリや一般的なGoのコードでは、このような状況で明示的にString()を呼び出すことは稀です。コードベース全体で一貫したスタイルを保つことが重要です。

この変更は、これらの冗長な呼び出しを削除することで、コードをより簡潔にし、Goの慣習に沿ったものにしています。これは、コードの可読性と保守性を向上させる小さな改善ですが、大規模なプロジェクトではこのような細かな改善が全体の品質に寄与します。

関連リンク

  • Gerrit Change-ID: https://golang.org/cl/5311067 (GoプロジェクトのコードレビューシステムであるGerritの変更リストへのリンク)

参考にした情報源リンク