ブランク識別子
for
range
ループとマップのコンテキストで、ブランク識別子について数回言及しました。ブランク識別子は、任意の型の任意の値に割り当てまたは宣言でき、値は無害に破棄されます。これは、Unix /dev/null
ファイルに書き込むのと少し似ています:変数が必要だが実際の値は無関係な場所でプレースホルダーとして使用される書き込み専用値を表します。これまでに見た用途以外にも用途があります。
複数代入でのブランク識別子
for
range
ループでのブランク識別子の使用は、一般的な状況の特別なケースです:複数代入。
左側で複数の値を必要とする代入があるが、プログラムで値の1つが使用されない場合、代入の左側のブランク識別子により、ダミー変数を作成する必要がなくなり、値が破棄されることが明確になります。たとえば、値とエラーを返す関数を呼び出すが、エラーのみが重要な場合、ブランク識別子を使用して無関係な値を破棄します。
if _, err := os.Stat(path); os.IsNotExist(err) {
fmt.Printf("%s does not exist\n", path)
}
時々、エラーを無視するためにエラー値を破棄するコードを見ることがあります。これはひどい習慣です。エラーの戻り値は常にチェックしてください。それらは理由があって提供されています。
// 悪い!パスが存在しない場合、このコードはクラッシュします。
fi, _ := os.Stat(path)
if fi.IsDir() {
fmt.Printf("%s is a directory\n", path)
}
未使用のインポートと変数
パッケージをインポートしたり、使用せずに変数を宣言したりするのはエラーです。未使用のインポートはプログラムを肥大化させ、コンパイルを遅くし、初期化されているが使用されていない変数は、少なくとも無駄な計算であり、おそらくより大きなバグの兆候です。ただし、プログラムが積極的に開発されている場合、未使用のインポートと変数がしばしば発生し、コンパイルを進めるためだけにそれらを削除し、後で再び必要になるのは煩わしいことがあります。ブランク識別子は回避策を提供します。
この半完成のプログラムには、2つの未使用のインポート(fmt
とio
)と未使用の変数(fd
)があるため、コンパイルされませんが、これまでのコードが正しいかどうかを確認できれば良いでしょう。
package main
import (
"fmt"
"io"
"log"
"os"
)
func main() {
fd, err := os.Open("test.go")
if err != nil {
log.Fatal(err)
}
// TODO: fdを使用する。
}
未使用のインポートに関する苦情を黙らせるために、ブランク識別子を使用してインポートされたパッケージからシンボルを参照します。同様に、未使用の変数fd
をブランク識別子に代入すると、未使用変数エラーが黙ります。このバージョンのプログラムはコンパイルされます。
package main
import (
"fmt"
"io"
"log"
"os"
)
var _ = fmt.Printf // デバッグ用;完了したら削除。
var _ io.Reader // デバッグ用;完了したら削除。
func main() {
fd, err := os.Open("test.go")
if err != nil {
log.Fatal(err)
}
// TODO: fdを使用する。
_ = fd
}
慣例として、インポートエラーを黙らせるグローバル宣言は、インポートの直後に配置し、見つけやすくし、後で整理するためのリマインダーとしてコメントを付ける必要があります。
副作用のためのインポート
前の例のfmt
やio
のような未使用のインポートは、最終的に使用されるか削除される必要があります:ブランク代入は進行中の作業としてコードを識別します。しかし、明示的な使用なしに、その副作用のためだけにパッケージをインポートすることが有用な場合があります。たとえば、そのinit
関数の間に、net/http/pprof
パッケージはデバッグ情報を提供するHTTPハンドラを登録します。エクスポートされたAPIを持っていますが、ほとんどのクライアントはハンドラの登録のみを必要とし、Webページからデータにアクセスします。その副作用のためだけにパッケージをインポートするには、パッケージをブランク識別子に名前変更します:
import _ "net/http/pprof"
このインポート形式は、パッケージがその副作用のためにインポートされていることを明確にします。なぜなら、パッケージの他の可能な使用はないからです:このファイルでは、名前を持ちません。(もし名前を持っていて、その名前を使用しなかった場合、コンパイラはプログラムを拒否します。)
インターフェースチェック
上記のインターフェースの議論で見たように、型はインターフェースを実装することを明示的に宣言する必要はありません。代わりに、型はインターフェースのメソッドを実装するだけでインターフェースを実装します。実際には、ほとんどのインターフェース変換は静的であるため、コンパイル時にチェックされます。たとえば、*os.File
がio.Reader
インターフェースを実装していない限り、io.Reader
を期待する関数に*os.File
を渡すことはコンパイルされません。
ただし、一部のインターフェースチェックは実行時に発生します。1つのインスタンスはencoding/json
パッケージにあり、Marshaler
インターフェースを定義しています。JSONエンコーダーがそのインターフェースを実装する値を受信すると、エンコーダーは標準変換を行う代わりに、値のマーシャリングメソッドを呼び出してJSONに変換します。エンコーダーは、次のような型アサーションでこのプロパティを実行時にチェックします:
m, ok := val.(json.Marshaler)
実際にインターフェース自体を使用せずに、型がインターフェースを実装するかどうかを尋ねることだけが必要な場合、おそらくエラーチェックの一部として、ブランク識別子を使用して型アサートされた値を無視します:
if _, ok := val.(json.Marshaler); ok {
fmt.Printf("value %v of type %T implements json.Marshaler\n", val, val)
}
この状況が発生する1つの場所は、型を実装するパッケージ内で、それが実際にインターフェースを満たすことを保証する必要がある場合です。型—たとえば、json.RawMessage
—がカスタムJSON表現を必要とする場合、json.Marshaler
を実装する必要がありますが、コンパイラがこれを自動的に検証する静的変換はありません。型が誤ってインターフェースを満たさない場合、JSONエンコーダーは依然として動作しますが、カスタム実装を使用しません。実装が正しいことを保証するために、パッケージでブランク識別子を使用したグローバル宣言を使用できます:
var _ json.Marshaler = (*RawMessage)(nil)
この宣言では、*RawMessage
をMarshaler
への変換を含む代入により、*RawMessage
がMarshaler
を実装することが必要であり、そのプロパティはコンパイル時にチェックされます。json.Marshaler
インターフェースが変更された場合、このパッケージはもはやコンパイルされず、更新が必要であることを通知されます。
この構成でのブランク識別子の出現は、宣言が変数を作成するためではなく、型チェックのためだけに存在することを示しています。ただし、インターフェースを満たすすべての型に対してこれを行わないでください。慣例として、そのような宣言は、コードにまだ静的変換が存在しない場合にのみ使用され、これはまれなイベントです。