🦁

Go で構造体のフィールドにInterface変数とポインター変数を設定した場合の参照の関係性を知りたかった2

7 min read

はじめに

以前、構造体のフィールド変数の上書きの挙動 について書いたのですが、補足したくなったので追加で記載します。
何の話を追加で書くかというと、フィールドの構造体の中のフィールドがSetterなどで変更された場合の挙動についてになります。

どこがポインターで、どこが実体かというのがわかっていれば、なんとなく「どう動くのか」は、経験上わかる気がするのですが、
実はこちらの方がケースとして発生することが多いので、文書として残しておこうと思います。
構造体が状態を持つ場合、それ自体の範囲や状態変更による影響を改めて確認したいと思います。
(あと、自分はGoの Interface なんかはどこまで実体でどこまでポインターなのか直感的にわかるほどの理解もなかったので)

動作環境は前回(上述を参照)と同じになります。

内容

以下のパターンについて確認します。

  • 構造体のフィールドに設定された Interface のフィールド変数が Setter で情報を書き換える場合
  • 構造体のフィールドに設定された Interface の元の変数が Setter で情報を書き換える場合
  • 構造体のフィールドに設定されたポインターのフィールド変数が Setter で情報を書き換える場合
  • 構造体のフィールドに設定されたポインターの元の変数が Setter で情報を書き換える場合

使う構造体の定義

前回で使ったような時刻を返す Interface MutableClock と、その実装 MutableClockMock があり、
今回は Setter で構造体のフィールドを変更できるとします。
その MutableClock/MutableClockMock をフィールド変数に持っている構造体 MutableSample があるとします。

type MutableClock interface {
	Now() int
	Setter(value int)
}

type MutableClockMock struct {
	value int
}

func (cm *MutableClockMock) Now() int {
	return cm.value
}

func (cm *MutableClockMock) Setter(value int) {
	cm.value = value
}

type MutableSample struct {
	Clock *MutableClockMock
}

type MutableSample0 struct {
	Clock MutableClock
}

構造体のフィールドに設定された Interface のフィールド変数が Setter で情報を書き換える場合

func TestApp_Interface_mutable(t *testing.T) {
	clock := MutableClockMock{10}
	struct1 := MutableSample0{Clock: &clock}
	struct2 := MutableSample0{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 のフィールド変数を Setter で変更
	struct1.Clock.Setter(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_Interface_mutable
struct1 now=[10], struct2 now=[10].
base addres=[0xc000022a00], struct1 address=[0xc00004b460], struct2 address=[0xc00004b470].
struct1 now=[100], struct2 now=[100].
base addres=[0xc000022a00], struct1 address=[0xc00004b460], struct2 address=[0xc00004b470].
--- PASS: TestApp_Interface_mutable (0.00s)
*/

構造体1のフィールド変数の値をSetterで変更すると、構造体2のフィールド変数にも影響がでています。
変数のアドレスは全て異なっていますが、参照先が同じになるので影響が波及しているのだと思います。

構造体のフィールドに設定された Interface の元の変数が Setter で情報を書き換える場合

func TestApp_Interface_OverrideBase_mutable(t *testing.T) {
	clock := &MutableClockMock{10}
	struct1 := MutableSample0{Clock: clock}
	struct2 := MutableSample0{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)
	// 代入元のフィールド変数を Setter で変更
	clock.Setter(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_Interface_OverrideBase_mutable
struct1 now=[10], struct2 now=[10].
base addres=[0xc00004b4c0], struct1 address=[0xc00004b4d0], struct2 address=[0xc00004b4e0].
struct1 now=[100], struct2 now=[100].
base addres=[0xc00004b4c0], struct1 address=[0xc00004b4d0], struct2 address=[0xc00004b4e0].
--- PASS: TestApp_Interface_OverrideBase_mutable (0.00s)
*/

構造体1・2のフィールド変数の元になった変数をSetterで変更すると、構造体1・2のフィールド変数にも影響が出ています。
これも変数のアドレスは全て異なっていますが、参照先が同じになるので影響が波及しているのだと思います。

構造体のフィールドに設定されたポインターのフィールド変数が Setter で情報を書き換える場合

func TestApp_Pointer_mutable(t *testing.T) {
	clock := MutableClockMock{10}
	struct1 := MutableSample{Clock: &clock}
	struct2 := MutableSample{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 のメンバー変数を Setter で変更
	struct1.Clock.Setter(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_mutable
struct1 now=[10], struct2 now=[10].
base addres=[0xc000022a40], struct1 address=[0xc000022a40], struct2 address=[0xc000022a40].
struct1 now=[100], struct2 now=[100].
base addres=[0xc000022a40], struct1 address=[0xc000022a40], struct2 address=[0xc000022a40].
--- PASS: TestApp_Pointer_mutable (0.00s)
*/

構造体1のフィールド変数の値をSetterで変更すると、構造体2のフィールド変数にも影響がでています。
変数のアドレスが全て同じなので、影響が全ての変数に影響しています。

構造体のフィールドに設定されたポインターの元の変数が Setter で情報を書き換える場合

func TestApp_PointerOverride_mutable(t *testing.T) {
	clock := MutableClockMock{10}
	struct1 := MutableSample{Clock: &clock}
	struct2 := MutableSample{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)
	// 代入元の変数を Setter で変更
	clock.Setter(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_mutable
struct1 now=[10], struct2 now=[10].
base addres=[0xc000022a60], struct1 address=[0xc000022a60], struct2 address=[0xc000022a60].
struct1 now=[100], struct2 now=[100].
base addres=[0xc000022a60], struct1 address=[0xc000022a60], struct2 address=[0xc000022a60].
--- PASS: TestApp_PointerOverride_mutable (0.00s)
*/

構造体1・2のフィールド変数の元になった変数をSetterで変更すると、構造体1・2のフィールド変数にも影響がでています。
変数のアドレスが全て同じなので、影響が全ての変数に影響しています。

まとめ

結果

構造体のフィールド変数の宣言が Interface、ポインターにかかわらず、
元になった変数(構造体)が同じであれば、Setterでの状態変更は参照する全ての構造体に影響していました。
これは参照するフィールド変数自体や、Interfaceが参照する実体の値のアドレスが同じため、
それ自体が変更されると参照先の値が全て変更になっています。

所感

フィールド変数がポインター変数などの参照を扱う場合、
それが Setter などで変更されると、同じ参照を使っていればそちらも変更が影響することが再確認できました(知ってる人には当たり前のことだったかもしれませんが)。

変数の変更によって意図しない動作に遭遇するのを避けたいという気持ちであれば、
フィールド変数にポインター変数を渡す場合、以下の意識が効果がありそうと思います。

  • 構造体に状態を持たないようにする
  • なるべく同じ変数をフィールドとして渡さないこと
  • フィールド変更の書き換えが必要になってしまう場合、変数の参照範囲や書き換えの範囲を明確に把握すること

Discussion

ログインするとコメントできます