LoginSignup
16
11

【Go】型が特定のinterfaceを満たしているかをコンパイル時に確認させる方法

Last updated at Posted at 2024-03-28

はじめに

こんにちは、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を実装しているかはややわかりにくいところがあります。
たとえば型foohogeIFインターフェースを満たしているのか?を確認するためには、型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の場合で書くと、それぞれ次の一行を挟むだけです。

ケース1
var _ fmt.Stringer = (*Person)(nil) // Personがfmt.Stringerインターフェースを満たしていることを保証する
ケース2
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に書かれています。)

ポインタ型と値型のメソッドセット.jpeg


今回の記事のタイトルは、「型が特定の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インターフェースが実装されているかがわかるためです。ポインタ型にデリファレンスする必要があるか気にしなくて良いため、思わぬ事故を防ぐ効果が見込めます。

終わりに

使用する機会は少なそうですが、引き出しとしてもっておきたい知識だなと思いました。
最後は少しくどくなってしまいましたが、自分のギモンを掘り下げていくことでドキュメントを読むだけでは気付けなかったようなことまで知ることが出来て面白かったです。

最後まで読んでいただきありがとうございました。間違いなどあればコメントにてご指摘ください。

  1. 本当はここの処理はもう少し複雑で、json.Marshalerインターフェースが満たされていない場合はencoding.TextMarshalerインターフェースを実装しているかを調べて...といったようなことを行うのですが、ここでは簡単のために省略します。より詳しいことを知りたい方はこちらから

  2. メソッドセット(interfaceを満たしているかを判定するもの)の説明としてはこうなるのですが、実際には値型からもポインタレシーバのメソッドを呼び出せる場合があります。(暗黙的変換)
    たとえば、ポインタレシーバのメソッドである(p *Person) methodA()は、Person構造体の値型変数pからはp.methodA()のように呼び出しが可能です。このときp.methodA()は自動的に(&p).methodA()のように解釈されています。

16
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
11