🍫

Goのfor文はスライスに入ってる要素がポインタか値かで対処の仕方が異なる

2022/04/06に公開1

Goにおいてスライスおよびマップをforで回す際には「rangeで取ってきている値は常に同じメモリアドレスに格納される」ようになっています
結果として、for _, item := range listと書いたときの「&item」や「&item.field」の扱いが問題になります。これは多くの記事でも論じられてきています
https://blog.p1ass.com/posts/pointer-of-for-range-loop-of-go/
https://zenn.dev/muro/articles/c988a58bd48814

ただ、「&item」と「&item.field」では対処法というか結論が異なるので分けて論じられるべきですが、あまり見かけなかったので一応記事にしておきます

&item

これは先程も述べた「rangeで取ってきている値は常に同じメモリアドレスに格納される」というGoの仕様上、どのような場合もループ中、常に同じアドレスが取得されてしまいます
この場合、下記のどちらかの方法をとる必要があります
(2022年4月6日に1を追記致しました!)

  1. ループ内で、ループ変数をダミー変数に代入し直す(コメント欄にてご指摘頂いた方法です!実際はこれもよく見ますが完全に忘れていました……ありがとうございます👶)
  2. StrToPtrのような他関数への「値渡し」を一度挟むことで異なる参照が生じるようにする
    (Goでは関数の引数の引き渡しの際に値渡しが行われる、すなわち値がコピーされる。そのためStrToPtrのような関数を中継すれば、異なる参照を生み出せる)
  3. 参照を取らないようにする
//fromSliceの各要素をなんとかしてポインタ型でtoSliceに入れたい

//1の方法
for _, item := range fromSlice {
	i := item
	toSlice = append(toSlice, &i)
}

//2の方法
for _, item := range fromSlice {
	toSlice = append(toSlice, StrToPtr(item))
}

func StrToPtr(v string) *string {
	return &v
}

&item.field

こちらはスライスやマップが値型かポインタ型かで異なります。なぜなら「item.ID」の意味が若干異なるためです。結論としては、値型の場合はAと同様に対策が必要になり、ポインタ型の場合は対策がいりません
値型であれば、item.IDは「常に同じメモリアドレス(itemのメモリアドレス)に存在する構造体が持つ1フィールド」であり、それは常に同じメモリアドレスにあるからです。itemのメモリアドレスを起点に構造体が占めるメモリの分だけ勘定した値になります
しかしポインタ型であれば、item.IDは「常に同じメモリアドレス(itemのメモリアドレス)に記録されている、毎回異なるメモリアドレスにある構造体が持つ1フィールド」であり、それは常に異なるメモリアドレスにあることになります
なお、この若干の違いは、itemにポインタが記録されている構造体のフィールド("ID")を指す際、Goの表現で「*item.ID」と書かずに「item.ID」と書くことに起因しています  
とりあえず結論としては、値型の場合はAと同様に対策が必要になり、ポインタ型の場合は対策がいらないということになります

確かめたコード

実際に確かめたのがこちらのコードです
go playground

type Example struct {
	id    int
	value string
}

func main() {
	a := Example{id: 1, value: "aaa"}
	b := Example{id: 2, value: "bbb"}
	c := Example{id: 3, value: "ccc"}

	//値型のスライスをつくる
	var slice []Example
	slice = append(slice, a)
	slice = append(slice, b)
	slice = append(slice, c)
	fmt.Println("slice: ", slice)

	for _, s := range slice {
		fmt.Println("「s自体のアドレス」: ", &s)
		fmt.Println("「sに入っている値、つまり各Example要素」: ", s)
		fmt.Println("「『sに入っている値、つまり各Example要素』のvalueの値」: ", s.value)
		fmt.Println("「『sに入っている値、つまり各Example要素を指すアドレス』のvalueのアドレス」: ", &s.value) //sliceが値型なので常に同じアドレスを指す
	}

	fmt.Println("############################")
	fmt.Println("############################")

	//ポインタ型のスライスをつくる
	var pslice []*Example
	pslice = append(pslice, &a)
	pslice = append(pslice, &b)
	pslice = append(pslice, &c)
	fmt.Println("pslice: ", pslice)

	for _, s := range pslice {
		fmt.Println("「s自体のアドレス」: ", &s)
		fmt.Println("「sに入っている値、つまり各Example要素を指すアドレス」: ", s)
		fmt.Println("「『sに入っている値、つまり各Example要素を指すアドレス』に入っている実際のExample」: ", *s)
		fmt.Println("「『sに入っている値、つまり各Example要素を指すアドレス』に入っている実際のExampleのvalueの値」: ", s.value)     //*s.valueとはならない
		fmt.Println("「『sに入っている値、つまり各Example要素を指すアドレス』に入っている実際のExampleのvalueのアドレス」: ", &s.value) //sliceがポインタ型なので常に異なるアドレスを指す
	}
}

//実行結果
// slice:  [{1 aaa} {2 bbb} {3 ccc}]
// 「s自体のアドレス」:  &{1 aaa}
// 「sに入っている値、つまり各Example要素」:  {1 aaa}
// 「『sに入っている値、つまり各Example要素』のvalueの値」:  aaa
// 「『sに入っている値、つまり各Example要素を指すアドレス』のvalueのアドレス」:  0xc00000c0f8
// 「s自体のアドレス」:  &{2 bbb}
// 「sに入っている値、つまり各Example要素」:  {2 bbb}
// 「『sに入っている値、つまり各Example要素』のvalueの値」:  bbb
// 「『sに入っている値、つまり各Example要素を指すアドレス』のvalueのアドレス」:  0xc00000c0f8
// 「s自体のアドレス」:  &{3 ccc}
// 「sに入っている値、つまり各Example要素」:  {3 ccc}
// 「『sに入っている値、つまり各Example要素』のvalueの値」:  ccc
// 「『sに入っている値、つまり各Example要素を指すアドレス』のvalueのアドレス」:  0xc00000c0f8
// ############################
// ############################
// pslice:  [0xc00000c030 0xc00000c048 0xc00000c060]
// 「s自体のアドレス」:  0xc00000e038
// 「sに入っている値、つまり各Example要素を指すアドレス」:  &{1 aaa}
// 「『sに入っている値、つまり各Example要素を指すアドレス』に入っている実際のExample」:  {1 aaa}
// 「『sに入っている値、つまり各Example要素を指すアドレス』に入っている実際のExampleのvalueの値」:  aaa
// 「『sに入っている値、つまり各Example要素を指すアドレス』に入っている実際のExampleのvalueのアドレス」:  0xc00000c038
// 「s自体のアドレス」:  0xc00000e038
// 「sに入っている値、つまり各Example要素を指すアドレス」:  &{2 bbb}
// 「『sに入っている値、つまり各Example要素を指すアドレス』に入っている実際のExample」:  {2 bbb}
// 「『sに入っている値、つまり各Example要素を指すアドレス』に入っている実際のExampleのvalueの値」:  bbb
// 「『sに入っている値、つまり各Example要素を指すアドレス』に入っている実際のExampleのvalueのアドレス」:  0xc00000c050
// 「s自体のアドレス」:  0xc00000e038
// 「sに入っている値、つまり各Example要素を指すアドレス」:  &{3 ccc}
// 「『sに入っている値、つまり各Example要素を指すアドレス』に入っている実際のExample」:  {3 ccc}
// 「『sに入っている値、つまり各Example要素を指すアドレス』に入っている実際のExampleのvalueの値」:  ccc
// 「『sに入っている値、つまり各Example要素を指すアドレス』に入っている実際のExampleのvalueのアドレス」:  0xc00000c068
GitHubで編集を提案

Discussion

canaluncanalun

ご指摘ありがとうございます!実際その方法もよく見ますね、忘れていました……汗
追記致しました👶