はじめに
こんにちは、tosshyと申します。最近、CyberAgentが主催するGo Collegeという1ヶ月のインターンシップでGo言語を学んでいます。
その中でチームメイトとGoのWebフレームワーク(Gin)について内部実装を読んでいたときのことです。
以下のようなコードがありました。
type I interface {
D()
}
type T struct{
// いくつかのメンバ変数
}
func (t *T) D() {
// なんらかの処理
}
var _ I = (*T)(nil) // なんだこれ!?
最後の1行に驚きました。
_で値を捨てたり、構造体Tのポインタにnilを入れたりと一見すると意味不明なことをしています。
本記事ではvar _ I = (*T)(nil)とはなんなのか、なぜやっているのか、具体的なユースケースついてまとめました。
対象とする読者
- Go言語初心者
- 筆者のように度肝を抜かれた人
前提
- Go言語の基礎文法がわかる
改めてじっくり読む
冒頭でさっと読み飛ばしましたが、あのコードは何をしていたのでしょうか?
1つずつ紐解いていきましょう。
type I interface {
D()
}
インターフェースIの定義です。
インターフェースI型はD()というメソッドを持っていることを保証します。
type T struct {
// いくつかのメンバ変数
}
構造体Tの定義です。
構造体Tはいくつかのメンバ変数を持っている想定ですが、具体的な内容は割愛します。
func (t *T) D() {
// なんらかの処理
}
構造体Tのポインタが持っているメソッドD()の定義です。
メソッドD()はなんらかの処理をしますが、具体的な内容については割愛します。
レシーバが
*Tだから、値型TはD()メソッドを持っていない。
var _ I = (*T)(nil)
こちらの処理は2つのことをしています。
-
nilを構造体Tのポインタ型*Tに変換 - インターフェース
I型の変数_に代入
_はブランク識別子と呼ばれる特殊な識別子で、値を使わないことを明示するために使う。forループのインデックスを捨てるときにも出現する。
やっていることは一通りわかったと思います。
でもなぜvar _ I = (*T)(nil)を書くのでしょうか?
値を捨ててしまうのならばわざわざ書かなくていいとは思いませんか?
その理由を知るためにはGoのインターフェースの特徴を知る必要があります。
Goインターフェースは暗黙的な実装
Goのインターフェースにはimplementsのようなキーワードがありません。型がインターフェースに定義してある全てのメソッドを実装することでインターフェースを満たすことになっています。
これを暗黙的な実装と呼ぶ。
以下のサンプルコードを見てみましょう。
package main
import "fmt"
type Human1 struct {}
type Human2 struct {}
type Greeter interface {
Greet()
}
type Singer interface {
Greet()
Sing()
}
func (h1 *Human1) Greet() {
fmt.Println("Hello! I'm human1")
}
func (h2 *Human2) Greet() {
fmt.Println("Hello! I'm human2")
}
func (h2 *Human2) Sing() {
fmt.Println("La La La")
}
func main() {
h1 := &Human1{}
h2 := &Human2{}
var g1, g2 Greeter
g1 = h1
g2 = h2
// var s1 Singer
var s2 Singer
// s1 = h1 // Human1のポインタがSing()を実装していないからコンパイルエラー!
s2 = h2
// Greeterインターフェースを満たすものはGreetが呼べる
g1.Greet()
g2.Greet()
// Singerインターフェースを満たすものはGreetとSingが呼べる
s2.Greet()
s2.Sing()
}
g1 = h1とg2 = h2は*Human1と*Human2がGreet()を実装しているため、Greeterを満たし、コンパイルが通ります。
同様にs2 = h2は*Human2がGreet()とSing()を実装しているため、Singerを満たし、コンパイルが通ります。
しかしs1 = h1は*Human1がSing()を実装していないため、Singerを満たさず、コンパイルエラーになります。
インターフェース型の変数に代入した時点で、その型がインターフェースを満たすかどうかがわかる。裏を返せば、代入しなければコンパイル時に気づけないということです。
ここまではインターフェースへの基礎理解があれば大丈夫だと思います。
それではなぜvar _ I = (*T)(nil)のような奇妙な文を書くのでしょうか?
なぜ書くのか?
前節ではGoのインターフェースが暗黙的な実装であることを、例を見ながら振り返りました。前節のコード例を踏まえて、下記のコードを書くとコンパイルは通るでしょうか?
package main
import "fmt"
type Human struct {}
type Singer interface {
Greet()
Sing()
}
func (h *Human) Greet() {
fmt.Println("Hello! I'm human")
}
func main() {}
答えは通ります。*HumanはSing()を持っていないのでSingerを満たしませんが、このコードではどこにもSingerの変数に代入していないので、コンパイラは*HumanがSingerを満たしていないことに気づけません。
もし開発者は*HumanがSingerを満たすつもりで書いていたとしたらどうでしょうか?
コンパイル時にSing()の未実装に気づくことができませんね。
インターフェースを満たすかチェックをするために以下のようなコードを書くこともできますが、チェックのためだけに変数を2つ作るのは冗長です。大したこともやっていないのに、4行も書くのはコスパが悪いように感じますし、ポインタを作るためにわざわざゼロ値で初期化された構造体をインスタンス化しています。
var s Singer
h := &Human{}
s = h
_ = s
そこでvar _ I = (*T)(nil)の登場です。
冒頭のコードの正体は、インターフェースを満たすことをコンパイル時に保証するためのパターンだったのです。
実際に組み込んでみましょう。
package main
import "fmt"
type Human struct {}
type Singer interface {
Greet()
Sing()
}
func (h *Human) Greet() {
fmt.Println("Hello! I'm human")
}
var _ Singer = (*Human)(nil) // ここでインターフェースを満たすかチェックできる!
func main() {}
このコードをコンパイルすると以下のエラーが出ます。
cannot use (*Human)(nil) (value of type *Human) as Singer value in variable
declaration: *Human does not implement Singer (missing method Sing)
nilはどのポインタ型にも変換できるから、*Human型に変換したnilをSinger型の_に代入する。実際のインスタンスを作らずにインターフェースを満たすかどうかだけをチェックし、値は_で即座に破棄する。この一連の動作を1行で表現できます。
nilはデフォルトでは型を持たないため、(*Human)(nil)と書いて明示的に*Human型に変換する必要がある。こうすることで「*HumanがSingerを満たすか?」というチェックがコンパイル時に走る。
どんなときに書くのか?
確かにコンパイル時にインターフェースの型を保証できるのは強力ですが、一体どんな時に書けば良いのでしょうか?
実際のユースケースを見てみましょう。
例: fmt.Stringer
fmt.StringerはString()メソッドを実装することで満たします。
プリントするときにString()メソッドが返すフォーマットで出力させることができます。
type Stringer interface {
String() string
}
下記のサンプルコードを見てみましょう。
勘違いで頭が大文字のString()メソッドではなく頭が小文字のstring()メソッドを実装したとします。
このコードはエラーで落ちることなく動きますが、意図した出力を得られません。
package main
import (
"fmt"
)
type Person struct {
Name string
Age int
}
func (p *Person) string() string {
return fmt.Sprintf("Name: %s\nAge: %d", p.Name, p.Age)
}
func main() {
p := &Person{Name: "taro", Age: 22}
fmt.Println(p)
// Name: taro
// Age: 22
// と出力されて欲しいのに
// &{taro 22}と出力される!
}
ここでvar _ fmt.Stringer = (*Person)(nil)を差し込んでみましょう。
package main
import (
"fmt"
)
var _ fmt.Stringer = (*Person)(nil) // コンパイルエラー発生
type Person struct {
Name string
Age int
}
func (p *Person) string() string {
return fmt.Sprintf("Name: %s\nAge: %d", p.Name, p.Age)
}
func main() {
p := &Person{Name: "taro", Age: 22}
fmt.Println(p)
}
これで意図していない実装になっていることをコンパイルエラーが教えてくれます。
とても便利ですね。
その他の例
同じように実装してあれば挙動が代わり、実装してなければデフォルトの挙動にフォールバックするものとしてjson.Marshaler、json.Unmarshalerなどがあります。
これらもfmt.Strigerの例と同じように使えます。
また、メソッド名のリファクタなどで気づかないうちにインターフェースを満たさないものにリネームしてしまう事故なども防げます。
type Handler interface {
Serve()
}
type MyHandler struct {}
func (m *MyHandler) Serve() {}
func (m *MyHandler) ServeHTTP() {} // 誰かがメソッド名を変更
var _ Handler = (*MyHandler)(nil) // コンパイルエラーで気づける
おわりに
いかがだったでしょうか?
今回はvar _ I = (*T)(nil)が何者なのか、なぜこの書き方なのか、具体的なユースケースについてまとめました。
Goならではのインターフェースの暗黙的な実装に起因する魔法のようなテクニックを今後自分も使っていきたいと思います。
参考文献
効率の良いコードを書くためのテクニックが書かれているGo言語公式の記事です
Effective Go blank implements
今回は深く取り扱わなかったメソッドセットの話も解説しています。
【Go】型が特定のinterfaceを満たしているかをコンパイル時に確認させる方法