Go で構造体のフィールドにInterface変数とポインター変数を設定した場合の参照の関係性を知りたかった
始めに
前書き
自分語りになりますが、2021年の春から Go を使うようになりました。
手探りで Go 自体やその書き方を覚えていってる最中です。
他の言語でのクラス、メンバー変数にあたる部分に Go では構造体、フィールドとして存在します。
Goでは引数やフィールドに変数を設定するとき、「構造体などのデータの大きなものはポインターで渡すようにする」というベストプラクティスというか、お作法があるようです。
理由としては、実体で渡すとデータをコピーして渡すことになり、データの大きな場合はメモリスタックから渡すときのオーバーヘッドが大きいからとかなんとかだそうです。
なので、構造体のフィールドとして、構造体を持たせる場合はポインター変数として持たせるようにすると良いのかなという認識です。
Interface も代入した変数のコピーのアドレスを持っているようなので、同じように使って良いのかなという認識でいます。
「Interface が値のコピーのアドレスを持っている」について、参考にしたのはこの辺りです
- http://www.fujlog.net/2014/02/golang-interface-value-or-reference.html
- https://www.ardanlabs.com/blog/2016/05/copying-interface-values-in-go.html
疑問について
気になる挙動部分
ここでふと疑問に思ったことは「構造体のフィールドにInterface/ポインターを渡した後、元の変数を上書きしたらどうなるのか」「他の構造体のフィールドの場合はどうなるのか」ということです。
自分で書いていて、何を言っているのかわかりません。
何を言いたかったかというと以下の場合の挙動を知りたいです。
var value StructInterface
value = StructImpl{}
// 構造体に変数を渡す
struct1 := Sample{Value: value}
// 変数を override
value = StructImpl2{}
fmt.Printf("%v", struct1.doSomething())
// この時のフィールド変数の値は?
var value2 StructInterface
value2 = StructImpl{}
struct2 := Sample{Value: value2}
struct3 := Sample{Value: value2}
// 構造体のInterfaceフィールドを上書き
struct2.Value = StructImpl2{}
fmt.Printf("s2: %v, s3: %v", struct2.Value, struct3.Value)
// この時のフィールド変数の値は?
想定しているケース
何を気にしているかというと、
例えば複数のService層で、共通のRespositoryが依存してしまった場合、
別のService内でRepositoryの状態が変わった場合に、他のサービスのRepositoryに伝搬するのかといった点の挙動を把握したいと思いました。
(個人的には全てイミュータブルにしたい)
ポインタ変数はまだしも、Interface については Go 特有(?)なので、実際にどうなるのか知りたかったのです。
なのでサンプルコードを書いて確認してみました。
実行環境について
あんまり関係ないとは思いますが、いちおう動作環境としては以下で試しています。
- go 1.16
-
testing
で実行
(go が実行できればなんでも良いと思うのですが、自分はサンプルコードをいつもテストフレームワーク上で動かすので) - IntelliJ IDEA with Go plugin
- Mac (Intel チップ)
実際にやってみたこと
事前準備(構造体の定義とか)
まず、フィールド変数に指定したい Interface や構造体を定義します。
type Clock interface {
Now() int
}
type ClockImpl struct {
}
func (c ClockImpl) Now() int {
return int(time.Now().Unix())
}
type ClockMock struct {
value int
}
func (cm ClockMock) Now() int {
return cm.value
}
Clock
からは Now()
メソッドを実行した際に unixtime な int を取得できる。という体で考えています。
ClockImpl
からは実際の unixtime な int を取得できますし、
ClockMock
からはフィールドに指定した int 値を取得できるという実装を行います。
次に、その Clock
を扱う構造体の定義します。
Interface で持つ場合、ポインター変数で持つ場合で2種類用意しています。
type Sample struct {
Clock Clock
}
type Sample2 struct {
Clock *ClockMock
}
実際に動かす
Interface をフィールドに持つ構造体で、片方の構造体のフィールド変数を別の実装で上書きした場合
func TestApp_Interface(t *testing.T) {
clock := ClockImpl{}
struct1 := Sample{Clock: clock}
struct2 := Sample{Clock: clock}
fmt.Printf("struct1 now=[%d], struct2 now=[%d].\n", struct1.Clock.Now(), struct2.Clock.Now())
fmt.Printf("base addres=[%p], struct1 address=[%p], struct2 address=[%p].\n", &clock, &struct1.Clock, &struct2.Clock)
// struct1 の interface メンバー変数を override
struct1.Clock = ClockMock{10}
fmt.Printf("struct1 now=[%d], struct2 now=[%d].\n", struct1.Clock.Now(), struct2.Clock.Now())
fmt.Printf("base addres=[%p], struct1 address=[%p], struct2 address=[%p].\n", &clock, &struct1.Clock, &struct2.Clock)
}
/* 結果
=== RUN TestApp_Interface
struct1 now=[1626280965], struct2 now=[1626280965].
base addres=[0x14fa838], struct1 address=[0xc00011b450], struct2 address=[0xc00011b460].
struct1 now=[10], struct2 now=[1626280965].
base addres=[0x14fa838], struct1 address=[0xc00011b450], struct2 address=[0xc00011b460].
--- PASS: TestApp_Interface (0.00s)
*/
上書きした構造体のみ挙動が変わってそうです。
Interface をフィールドに持つ構造体で、元となる変数自体を別の実装で上書きした場合
func TestApp_Interface_OverrideBase(t *testing.T) {
// interface の宣言
var clock Clock
clock = ClockImpl{}
struct1 := Sample{Clock: clock}
struct2 := Sample{Clock: clock}
fmt.Printf("struct1 now=[%d], struct2 now=[%d].\n", struct1.Clock.Now(), struct2.Clock.Now())
fmt.Printf("base addres=[%p], struct1 address=[%p], struct2 address=[%p].\n", &clock, &struct1.Clock, &struct2.Clock)
// 代入元の interface 変数を override
clock = ClockMock{10}
fmt.Printf("struct1 now=[%d], struct2 now=[%d].\n", struct1.Clock.Now(), struct2.Clock.Now())
fmt.Printf("base addres=[%p], struct1 address=[%p], struct2 address=[%p].\n", &clock, &struct1.Clock, &struct2.Clock)
}
/* 結果
=== RUN TestApp_Interface_OverrideBase
struct1 now=[1626280965], struct2 now=[1626280965].
base addres=[0xc00011b4b0], struct1 address=[0xc00011b4c0], struct2 address=[0xc00011b4d0].
struct1 now=[1626280965], struct2 now=[1626280965].
base addres=[0xc00011b4b0], struct1 address=[0xc00011b4c0], struct2 address=[0xc00011b4d0].
--- PASS: TestApp_Interface_OverrideBase (0.00s)
*/
どちらの構造体も挙動は変わっていないようでした。
ポインター変数をフィールドに持つ構造体で、片方の構造体のフィールド変数を別の実装で上書きした場合
func TestApp_Pointer(t *testing.T) {
clock := ClockMock{10}
struct1 := Sample2{Clock: &clock}
struct2 := Sample2{Clock: &clock}
fmt.Printf("struct1 now=[%d], struct2 now=[%d].\n", struct1.Clock.Now(), struct2.Clock.Now())
fmt.Printf("base addres=[%p], struct1 address=[%p], struct2 address=[%p].\n", &clock, struct1.Clock, struct2.Clock)
// struct1 のポインタメンバー変数を override
struct1.Clock = &ClockMock{100}
fmt.Printf("struct1 now=[%d], struct2 now=[%d].\n", struct1.Clock.Now(), struct2.Clock.Now())
fmt.Printf("base addres=[%p], struct1 address=[%p], struct2 address=[%p].\n", &clock, struct1.Clock, struct2.Clock)
}
/* 結果
=== RUN TestApp_Pointer
struct1 now=[10], struct2 now=[10].
base addres=[0xc0001249b8], struct1 address=[0xc0001249b8], struct2 address=[0xc0001249b8].
struct1 now=[100], struct2 now=[10].
base addres=[0xc0001249b8], struct1 address=[0xc0001249c0], struct2 address=[0xc0001249b8].
--- PASS: TestApp_Pointer (0.00s)
*/
上書きした構造体のみ挙動が変わってそうです。
ポインター変数をフィールドに持つ構造体で、元の変数を別の変数で上書きした場合
func TestApp_PointerOverride(t *testing.T) {
clock := ClockMock{10}
struct1 := Sample2{Clock: &clock}
struct2 := Sample2{Clock: &clock}
fmt.Printf("struct1 now=[%d], struct2 now=[%d].\n", struct1.Clock.Now(), struct2.Clock.Now())
fmt.Printf("base addres=[%p], struct1 address=[%p], struct2 address=[%p].\n", &clock, struct1.Clock, struct2.Clock)
// 代入元の interface 変数を override
clock = ClockMock{100}
fmt.Printf("struct1 now=[%d], struct2 now=[%d].\n", struct1.Clock.Now(), struct2.Clock.Now())
fmt.Printf("base addres=[%p], struct1 address=[%p], struct2 address=[%p].\n", &clock, struct1.Clock, struct2.Clock)
}
/* 結果
=== RUN TestApp_PointerOverride
struct1 now=[10], struct2 now=[10].
base addres=[0xc0001249e0], struct1 address=[0xc0001249e0], struct2 address=[0xc0001249e0].
struct1 now=[100], struct2 now=[100].
base addres=[0xc0001249e0], struct1 address=[0xc0001249e0], struct2 address=[0xc0001249e0].
--- PASS: TestApp_PointerOverride (0.00s)
*/
どうやら両方の構造体の挙動が変わっていました。
まとめ
結果
結論としては以下
- Interface をフィールドに持つ場合
- 元の変数を上書きしても構造体のフィールドには影響しない
- 構造体のフィールド変数の変更は、他の構造体のフィールド変数には影響しない
これは元の変数のアドレス、フィールド変数のアドレスが全て異なるから。
- ポインターをメンバー変数を持つ場合
- 元の変数を上書きすると、その変数を参照しているフィールドも上書きされる
- 構造体のフィールドの変更は、他の構造体のフィールドには影響しない
これは元の変数のアドレスの値を、構造体のメンバー変数が参照しているから。
構造体のフィールド自体の上書きが他の構造体に影響しないのは、上書きされたフィールドの参照先のアドレスが上書きされるから。
自身の疑問への解
なので、気にしていたことの自分の中の解答として、
「Interface のフィールドは、元の変数・他の構造体について考慮しなくても良さそう」ですし、
「ポインタ変数の場合は、フィールド変数の元の変数の変更のみに注意すれば良さそう」という解釈に落ち着きました。
これで今夜はよく眠れそうな気がします。
Discussion