はじめに
こんにちは、kenです。お仕事ではGoを使っています。
先日、Effective Go(Goの公式が出している慣用的なコーディングスタイルと実践を記したドキュメント)を読んでいたら興味深いことが書かれていたので今日はそれについて書こうと思います。
内容は「型が特定のinterfaceを満たしているかをコンパイル時に確認させる方法について」です。
Goのinterfaceについておさらい
本題に入る前にGoのinterfaceについてざっくり説明しておきます。interfaceについてもう知っているよという方はスキップしてください。
Go言語ではinterfaceをメソッドシグネチャの集合として定義します。
type hogeIF interface {
Method1()
Method2()
...
MethodN()
}
そして型がinterfaceを実装する際には、interfaceの定義に使ったメソッドを型に実装するだけでよく、「その型がどのinterfaceを実装しているか」については明示的に宣言をする必要がありません。つまりGoのinterfaceの実装は暗黙的なのです。
ただ暗黙的であるがゆえに、ある型が自分の望んだinterfaceを実装しているかはややわかりにくいところがあります。
たとえば型foo
がhogeIF
インターフェースを満たしているのか?を確認するためには、型foo
に実装しているメソッドにmethod1
,method2
, ... , methodN
が存在するかをチェックする必要があります。
Goのinterfaceの実装は暗黙的...。だけどそれで困ることは少ない
しかし、このデメリットは多くの場合問題にはなりません。
というのも、ある型が自分の望んだinterfaceを実装しているかは静的解析時・もしくはコンパイル時に検出できるからです。
例えば、io.Reader
インターフェースを引数に期待する関数へある構造体Aを渡す場合、構造体Aがio.Reader
インターフェースを実装していなければコンパイルは通りませんし、静的解析時にもエラーが出るでしょう。つまり型が特定のinterfaceを満たしているかどうかは使用時に気づくことができるので、普段この仕様に不便さを感じることはまずありません。
でもごくたまに困るときもある
しかし、以下のような特殊なケースでは型が特定のインターフェースを実装しているかどうかを確認したくなるときもあります。
【Case1】 とある構造体にjson.Marshaler
インターフェースを実装しておいてほしい場合
json.Marshall
関数は構造体をJSON形式に変換する処理を行います。
このとき、変換対象の構造体にjson.Marshaler
インターフェースが満たされている場合(つまりMarshalJSON
メソッドが実装されている場合)は、実装されているMarshalJSON
を用いてJSONへの変換を行い、そうでない場合はデフォルトの変換形式を用いて変換を行います。1
もしデフォルトの変換形式を使いたくない事情があり、独自の形式でJSONへの変換を行いたい場合はjson.Marshaler
インターフェースが満たされているか、つまり独自に定義したMarshalJSON
が存在するかを確認する必要があります。
以下のコードでは、Person構造体がjson.Marshaler
インターフェースを満たしている場合と満たしていない場合とで、JSONへの変換形式が変わってきます。
package main
import (
"encoding/json"
"fmt"
)
type Person struct {
Name string
Age int
}
/*
func (p Person) MarshalJSON() ([]byte, error) {
return json.Marshal(&struct {
Name string `json:"name"`
Age int `json:"age"`
Greeting string `json:"greeting"`
}{
Name: p.Name,
Age: p.Age,
Greeting: fmt.Sprintf("Hello My name is %s", p.Name),
})
}
*/
func main() {
p := Person{
Name: "ken",
Age: 23,
}
jsonData, err := json.Marshal(p)
if err != nil {
fmt.Println("JSON Marshaling failed:", err)
return
}
fmt.Println(string(jsonData))
// json.Marshalerインターフェースを満たしている(=MarshalJSONメソッドを実装している)場合
// {"name":"ken","age":23,"greeting":"Hello My name is ken"}
// json.Marshalerインターフェースを満たしていない場合
// {"name":"ken","age":23}
}
json.Marshaler
インターフェースを満たしていても満たしていなくてもコンパイルはできてしまうので、独自の形式でJSONへの変換を行いたい場合は型がjson.Marshaler
インターフェースを実装しているかわざわざ確認しにいく必要があります。
【Case2】 とある構造体にfmt.Stringer
インターフェースを実装しておいてほしい場合
構造体をログなどにカスタマイズした形で出力したい場合、%s
の書式指定子を使って構造体の情報を表すことが稀にあると思います。
その際、構造体にString
メソッドを実装していない場合(つまりfmt.Stringer
インターフェースを構造体が満たしていない場合)は読みにくい形でログ出力される恐れがあります。
また、もともとログ出力のためにString
メソッドを実装していても、その事情を知らない人がリファクタリングの途中で使っていないメソッドだと思って消してしまうこともあるでしょう。それを避けるためには、やはりコンパイル時に構造体がfmt.Stringer
インターフェースを満たしているかを確認する必要が出てきます。
以下のコードでは、Person構造体がfmt.Stringer
インターフェースを満たしていない場合に読みにくい文字列が出力されてしまっています。
package main
import "fmt"
type Person struct {
Name string
Age int
}
/*
func (p Person) String() string {
return fmt.Sprintf("Name: %s, Age: %d", p.Name, p.Age)
}
*/
func main() {
p := Person{
Name: "ken",
Age: 23,
}
fmt.Printf("%s\n", p)
// Personがfmt.Stringerインターフェースを満たしていない(=Stringメソッドを実装していない)場合: {ken %!s(int=23)}
// Personがfmt.Stringerインターフェースを満たしている(=Stringメソッドを実装していない)場合: Name: ken, Age: 23
}
こちらもPerson
型がfmt.Stringer
インターフェースを満たしているかどうかにかかわらずコンパイルはできてしまうので、独自の形式でログ出力したい場合は注意する必要があります。
どのようにして解決するか
ではどのようにして型が特定のInterfaceを満たしているかをコンパイル時に確認するかというと、次の一行をどこかに挟むだけです。
var _ <interface> = (*<Type>)(nil)
先ほどのCase1,2の場合で書くと、それぞれ次の一行を挟むだけです。
var _ fmt.Stringer = (*Person)(nil) // Personがfmt.Stringerインターフェースを満たしていることを保証する
var _ json.Marshaler = (*Person)(nil) // Personがjson.Marshalerインターフェースを満たしていることを保証する
なぜこの一行を挟むだけで良いのかを、ケース2の場合を用いて説明します。
この宣言は、json.Marshaler
インターフェース型のアンダースコア変数にPerson
のポインタ型のnil
値を代入しようとしています。
この宣言のコンパイルが成功する場合、Person
型がjson.Marshaler
インターフェースを実装しているとコンパイル時に検証されたことを意味します。ここでのポイントは、Person
型(正確にはそのポインタ型)がjson.Marshaler
インターフェースの要求を満たすメソッドを実装しているかどうかを確認することであり、実際に実行時に何かが代入されるわけではなく、型の互換性を確認するためだけの構文ということです。
これにより、開発者はPerson
が意図した通りにjson.Marshaler
インターフェースを実装していることを確実にすることができます。また、将来json.Marshaler
インターフェースが変更された場合にも、この宣言によって影響を受ける箇所が明確になるため、迅速な修正が可能になるというメリットもあります。
これで型が特定のinterfaceを満たしているかを実行時ではなく、コンパイル時に確認できるようになりました!
めでたし、めでたし〜
あれ、どうしてPersonのポインタ型を使ってチェックしているの??
今回はこれで終わりではありません。
Effective Goではこれで終わりなんですが、私にはどうしても気になる点がありました。それはわざわざPersonのポインタ型を使っている点です。
var _ json.Marshaler = (*Person)(nil)
先ほど出てきた↑のコードですが、どうしてPersonをわざわざポインタ型にして検証しているのだろう...と思いませんでしたか。(私は思いました)
Person
型がjson.Marshaler
インターフェースを満たしているのかを確認するだけなら↓のコードでも良いはずです。
var _ json.Marshaler = Person{}
しかしこの2つのコードには明確な違いがあり、Effective Goに記載はないですがうまく使い分ける必要があると私は思っています。今からその違いと使い分けが必要になる理由について順に説明していきます。
(ここから先は完全に私個人の考察と見解です、正確性には気をつけて書いていますが間違っている場合はご指摘ください)
突然ですが、Case2でPerson
の値レシーバに対して定義していたMarshalJSON
メソッドを、ポインタレシーバに対して定義するとどうなるでしょうか。
package main
import (
"encoding/json"
"fmt"
)
type Person struct {
Name string
Age int
}
// ポインタレシーバに変更
func (p *Person) MarshalJSON() ([]byte, error) {
return json.Marshal(&struct {
Name string `json:"name"`
Age int `json:"age"`
Greeting string `json:"greeting"`
}{
Name: p.Name,
Age: p.Age,
Greeting: fmt.Sprintf("Hello My name is %s", p.Name),
})
}
func main() {
p := Person{
Name: "ken",
Age: 23,
}
jsonData, err := json.Marshal(p)
if err != nil {
fmt.Println("JSON Marshaling failed:", err)
return
}
fmt.Println(string(jsonData))
}
これを実行すると
{"Name":"ken","Age":23}
が出力されます。
独自で定義したMarshalJSON
が使われていませんね。ただこれはjson.Marshall
の引数に渡したものがPersonの値型でありポインタ型ではないので当たり前といったら当たり前です。Person
のポインタ型にはMarshalJSON
メソッドが実装されているものの、Person
の値型にはMarshalJSON
メソッドは実装されていないのでjson.Marshaler
インターフェースは満たされていません。よってデフォルトの変換形式が使われます。
なので独自のJSON形式を使いたい場合はjson.Marshal
にわたす引数をポインタ型にしてjson.Marshal(&p)
とする必要があります。
さて、ではこのコードの中に
var _ json.Marshaler = (*Person)(nil)
を挟むとどうなるでしょうか。これまでの説明だと、このコードのコンパイルが通るのは「Person構造体がjson.Marshaler
インターフェースを実装している場合」に限るということでした。そして今、Personの値型はjson.Marshaler
インターフェースを満たしていないためコンパイルは通らないはずです。
しかし↑の一行を挟んでもコンパイルは通ってしまいます。
あれ?さっきPerson
型がjson.Marshaler
インターフェースを満たしているか確認するためには
var _ json.Marshaler = (*Person)(nil)
を挟めば良いと書かれてあったけど、メソッドのレシーバがポインタ型になっているときはPerson
型がjson.Marshaler
インターフェースを満たしていない場合もあるってこと?
はい。そのとおりです...。。
厳密に書くとvar _ json.Marshaler = (*Person)(nil)
のコンパイルが通るのは「Person
のポインタ型がjson.Marshaler
インターフェースを満たしている場合」であって、「Person
の値型がjson.Marshaler
インターフェースを満たしている場合」ではありません。
つまり整理すると「Person
型からjson.Marshaler
インターフェースに定義されているメソッドを(ポインタ型への変換も駆使して)呼び出せる」場合にコンパイルが通るということです。
このような「ポインタ型が満たすinterfaceを値型は満たさない」という事象が起こるのはGo言語の仕様が関係しています。
Go言語ではメソッドが値レシーバとして定義されている場合、そのメソッドは値型とポインタ型の両方のメソッドセットに所属します。一方でメソッドがポインタレシーバとして定義されている場合、そのメソッドはポインタ型のメソッドセットにのみ所属します。つまりポインタ型が持つメソッドセットのほうが集合として大きいのです。2
(このことはGo wikiのMethod Setsに書かれています。)
今回の記事のタイトルは、「型が特定のinterfaceを満たしているかをコンパイル時に確認する方法」であり、Effective Goでの章名もこのようなものでした。しかし上で見たように、この「型」という表現の中に「値型」と「ポインタ型」が混在する点には注意が必要です。必ずしも「値型」単体がinterfaceを満たしているとは限りません。
繰り返しになってしまいますが、var _ json.Marshaler = (*Person)(nil)
のコンパイルが通るのはポインタ型のメソッドセット(値レシーバのメソッドとポインタレシーバのメソッドを合わしたもの)がinterfaceの定義を満たしている場合です。
ポインタ型は値レシーバのメソッドセットとポインタレシーバのメソッドセットを合わせ持つ一方で、値型は値レシーバのメソッドセットしか持たないという仕様は、一見するとややこしく不親切に感じられます。しかし値型もデリファレンスを行うことでポインタレシーバのメソッドを呼び出せるため、困る瞬間があるかというとそんなにないと思われます。また注記2に書いた暗黙的変換もあるためコードを書いてる際に疑問に思うタイミングも少ないことでしょう。
ただしCase1のときのように、型が特定のinterfaceを満たしているかが厳密に判定される際にはこの仕様は少々厄介です。
実はjson.Marshal
に渡された引数がjson.Marshaler
インターフェースを満たしているかは_, ok := val.(json.Marshaler);
のような型アサーションを使って判定されます。すなわち、引数の型のメソッドセットにMarshalJSON() ([]byte, error)
のシグネチャのメソッドが含まれている必要があります。
よってただ「(必要に応じたデリファレンスを行うことで)MarshalJSON()
を呼び出せる」というだけでは不十分なのです。デリファレンスをせずに直接MarshalJSON()
を呼び出せることが必要です。ややこしいですがMarshalJSON()
が呼び出せるかと、MarshalJSON()
がメソッドセットに含まれているかは別物であることに注意しましょう。
なので私個人的には、値型をレシーバとしてメソッドを基本的に書いていく場合は
var _ json.Marshaler = Person{}
のように書くのもアリだと思います。なぜならこのように書くとポインタレシーバで記述されたメソッドがインターフェースの構成要素としてカウントされないため、値レシーバのメソッドだけでjson.Marshaler
インターフェースが実装されているかがわかるためです。ポインタ型にデリファレンスする必要があるか気にしなくて良いため、思わぬ事故を防ぐ効果が見込めます。
終わりに
使用する機会は少なそうですが、引き出しとしてもっておきたい知識だなと思いました。
最後は少しくどくなってしまいましたが、自分のギモンを掘り下げていくことでドキュメントを読むだけでは気付けなかったようなことまで知ることが出来て面白かったです。
最後まで読んでいただきありがとうございました。間違いなどあればコメントにてご指摘ください。
-
本当はここの処理はもう少し複雑で、
json.Marshaler
インターフェースが満たされていない場合はencoding.TextMarshaler
インターフェースを実装しているかを調べて...といったようなことを行うのですが、ここでは簡単のために省略します。より詳しいことを知りたい方はこちらから ↩ -
メソッドセット(interfaceを満たしているかを判定するもの)の説明としてはこうなるのですが、実際には値型からもポインタレシーバのメソッドを呼び出せる場合があります。(暗黙的変換)
たとえば、ポインタレシーバのメソッドである(p *Person) methodA()
は、Person
構造体の値型変数p
からはp.methodA()
のように呼び出しが可能です。このときp.methodA()
は自動的に(&p).methodA()
のように解釈されています。 ↩