tl;dr
- Go の関数レシーバは値型とポインタ型の間で暗黙の型変換が行える場合と行えない場合がある。
- 暗黙的型変換を使うメリットはあまりないので、明示的に型を指定して関数定義をしていったほうが安全。
関数レシーバの暗黙の型変換
- Go の関数のレシーバは呼び出し元がアドレス指定可能であれば、値型とポインタ型の間で暗黙の型変換が行われ、簡略な記述で呼び出すことができる。
type T struct {}
// T 型のメソッド
func (t T) V() {
}
// *T 型のメソッド
func (t *T) P() {
}
t := T{}
t.V()
t.P() // t が &t に変換され (&t).P() が実行される
tt := &T{}
tt.P()
tt.V() // tt が *tt に変換され (*tt).V() が実行される
型変換が行われない場合
- 型変換が行われないのは、主に呼び出し元が値型でマップやインターフェイスの要素であった場合。
- 実行可能なコードを以下に記載する。
package main
import (
"fmt"
"reflect"
)
type S struct{}
func (s S) V() {
fmt.Println("V")
}
func (s *S) P() {
fmt.Println("P")
}
type CanV interface {
V()
}
type CanP interface {
P()
}
func main() {
// 変数の場合 S => *S に型変換される
s := S{}
s.V()
s.P()
// 変数の場合 *S => S にも型変換される
ss := &S{}
ss.V()
ss.P()
// スライスの要素の場合 S => *S に型変換される
l := []S{S{}}
l[0].V()
l[0].P()
// スライスの要素の場合 *S => S にも型変換される
ll := []*S{&S{}}
ll[0].V()
ll[0].P()
// マップの要素の場合 S => *S に型変換されない
m := map[int]S{
0: S{},
}
m[0].V()
// m[0].P() // エラーになるのでコメントアウト
// マップの要素でも *S => S には型変換される
mm := map[int]*S{
0: &S{},
}
mm[0].V()
mm[0].P()
// インターフェイスの場合 *S => S には型変換される
f := func(v CanV) {
v.V()
}
f(S{}) // S.V() は定義されているので ok
f(&S{}) // (*S).V() は定義されていないが *S => S と型変換されて ok
// インターフェイスでも S => *S には型変換されない
ff := func(p CanP) {
p.P()
}
// ff(S{}) // S.P() は定義されておらず型変換も行われないのでエラー
ff(&S{}) // (*S).P() は定義されているので ok
// reflect 経由でのインターフェイスでも *S => S は変換されるが S => *S の変換はエラーになる
reflect.ValueOf(S{}).Interface().(CanV).V()
// reflect.ValueOf(S{}).Interface().(CanP).P()
reflect.ValueOf(&S{}).Interface().(CanV).V()
reflect.ValueOf(&S{}).Interface().(CanP).P()
}
なぜ暗黙的型変換が行えないのか
- むずかしいので https://stackoverflow.com/questions/48790663/why-value-stored-in-an-interface-is-not-addressable-in-golang を参照のこと
レシーバの暗黙的型変換をいつ使うか
- 暗黙的型変換によって関数呼び出しが多少便利になる代わりに、呼び出しできないケースについて気をつけながらコードを書いていくのは割に合わない。
- 基本的に暗黙的型変換に頼らずに明示的に型を指定していったほうが安全。
- この件とは別にレシーバの型をどうするかの指針はこちらにあったりするが、例えば構造体なら特別な場合がなければポインタ型を使っておくとかで問題ない。
- おそらくだが構造体の関数呼び出しの際にフィールド参照と同じように値型かポインタ型かを意識せずに行えるようにしたい、という要望からレシーバの暗黙的型変換が実装されているようなきがする。
おわりに
- まあまあ Go わかっているつもりで Stack Overflow で回答をしてみたところ、関数レシーバの暗黙の型変換について全然わかっておらずに微妙な回答をしてしまったのを反省して今回は調べてみた。
- 結局自分ではあまり使わなそうではあるが、初心者だと混乱してしまいそうな機能ではあるので把握しておくことができてよかった。