😽

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

7 min read

始めに

前書き

自分語りになりますが、2021年の春から Go を使うようになりました。
手探りで Go 自体やその書き方を覚えていってる最中です。

他の言語でのクラス、メンバー変数にあたる部分に Go では構造体、フィールドとして存在します。
Goでは引数やフィールドに変数を設定するとき、「構造体などのデータの大きなものはポインターで渡すようにする」というベストプラクティスというか、お作法があるようです。
理由としては、実体で渡すとデータをコピーして渡すことになり、データの大きな場合はメモリスタックから渡すときのオーバーヘッドが大きいからとかなんとかだそうです。

なので、構造体のフィールドとして、構造体を持たせる場合はポインター変数として持たせるようにすると良いのかなという認識です。
Interface も代入した変数のコピーのアドレスを持っているようなので、同じように使って良いのかなという認識でいます。

「Interface が値のコピーのアドレスを持っている」について、参考にしたのはこの辺りです

疑問について

気になる挙動部分

ここでふと疑問に思ったことは「構造体のフィールドに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 のフィールドは、元の変数・他の構造体について考慮しなくても良さそう」ですし、
「ポインタ変数の場合は、フィールド変数の元の変数の変更のみに注意すれば良さそう」という解釈に落ち着きました。

これで今夜はよく眠れそうな気がします。