Go言語を使っているとfor-rangeループを多用します。その際、ポインタが絡むと予期しない結果になることがあるので、その事象の発生理由と対策をまとめてみたいと思います。
事象
Aさん(56歳)、Bさん(33歳)、Cさん(41歳)、Dさん(22歳)が配列で定義されていたとして、年齢が50歳以下の人だけを別の配列に入れるというコードを以下のように書いてみたとします。
package main import "fmt" type Person struct { name string age int } func main() { people := []Person{ Person{"Aさん", 56}, Person{"Bさん", 33}, Person{"Cさん", 41}, Person{"Dさん", 22}, } personPtrs := []*Person{} for _, person := range people { fmt.Printf("人の名前: <%s>\n", person.name) if person.age <= 50 { personPtrs = append(personPtrs, &person) } } for _, personPtr := range personPtrs { fmt.Printf("ポインタの参照先の名前 <%s>\n", personPtr.name) } }
このコードを実行して期待する結果は、ポインタの参照先の名前に年齢が50歳以下のBさん、Cさん、Dさんの3名が表示されることですが、実際にコードを実行してみるとDさんの名前だけが3回表示されます。
人の名前: <Aさん> 人の名前: <Bさん> 人の名前: <Cさん> 人の名前: <Dさん> ポインタの参照先の名前 <Dさん> ポインタの参照先の名前 <Dさん> ポインタの参照先の名前 <Dさん>
何が問題か
このコードの挙動を把握するために、変数のポインタを一緒に表示するようにコードを直してみます。
package main import "fmt" type Person struct { name string age int } func main() { people := []Person{ Person{"Aさん", 56}, Person{"Bさん", 33}, Person{"Cさん", 41}, Person{"Dさん", 22}, } personPtrs := []*Person{} for _, person := range people { fmt.Printf("人の名前: <%s>、ポインタ: <%p>\n", person.name, &person) if person.age < 50 { personPtrs = append(personPtrs, &person) } } for _, personPtr := range personPtrs { fmt.Printf("ポインタの参照先の名前 <%s>、ポインタ: <%p>\n", personPtr.name, personPtr) } }
実行してみると以下のように出力されました。
人の名前: <Aさん>、ポインタ: <0xc0000a8018> 人の名前: <Bさん>、ポインタ: <0xc0000a8018> 人の名前: <Cさん>、ポインタ: <0xc0000a8018> 人の名前: <Dさん>、ポインタ: <0xc0000a8018> ポインタの参照先の名前 <Dさん>、ポインタ: <0xc0000a8018> ポインタの参照先の名前 <Dさん>、ポインタ: <0xc0000a8018> ポインタの参照先の名前 <Dさん>、ポインタ: <0xc0000a8018>
この出力内容から for _, person := range people {
のループの度に変数person
を新しく生成しメモリ領域を確保しているのではなく、以下のように1つの変数に代入し、内容を上書いているだけだということが分かります。
package main import "fmt" type Person struct { name string age int } func main() { people := []Person{ Person{"Aさん", 56}, Person{"Bさん", 33}, Person{"Cさん", 41}, Person{"Dさん", 22}, } // 変数(入れ物)は1個だけ var person Person // 1回目のループ person = people[0] fmt.Printf("人の名前: <%s>、ポインタ: <%p>\n", person.name, &person) // 2回目のループ person = people[1] fmt.Printf("人の名前: <%s>、ポインタ: <%p>\n", person.name, &person) // 3回目のループ person = people[2] fmt.Printf("人の名前: <%s>、ポインタ: <%p>\n", person.name, &person) // 4回目のループ person = people[3] fmt.Printf("人の名前: <%s>、ポインタ: <%p>\n", person.name, &person) }
人の名前: <Aさん>、ポインタ: <0xc0000a8018> 人の名前: <Bさん>、ポインタ: <0xc0000a8018> 人の名前: <Cさん>、ポインタ: <0xc0000a8018> 人の名前: <Dさん>、ポインタ: <0xc0000a8018>
最終的に変数 person
(ポインタ 0xc0000a8018) には"Dさん"の情報しか入っていないので、最初のコードのpersonPtrs
の各値である person
のポインタ(0xc0000a8018) を参照しても "Dさん"の情報しか出力されなかったというわけです。
対処方法
期待する結果(Bさん、Cさん、Dさんの3名の名前が表示される)にするにはコードをどのように修正すれば良いでしょうか。
対処方法は2つあるかと思います。ループ変数( person
)のポインタを使わないという点はどちらも共通しています。
①元の配列の要素のポインタを使う
package main import "fmt" type Person struct { name string age int } func main() { people := []Person{ Person{"Aさん", 56}, Person{"Bさん", 33}, Person{"Cさん", 41}, Person{"Dさん", 22}, } personPtrs := []*Person{} for i := 0; i < len(people); i++ { var person *Person person = &people[i] // 元の配列の要素のポインタを代入 fmt.Printf("人の名前: <%s>、ポインタ: <%p>\n", person.name, person) if person.age <= 50 { personPtrs = append(personPtrs, person) } } for _, personPtr := range personPtrs { fmt.Printf("ポインタの参照先の名前 <%s>、ポインタ: <%p>\n", personPtr.name, personPtr) } }
以下の実行結果を見ると personPtrs
の各値が元の配列( people
)の各要素のポインタと同じ(0xc000062198, 0xc0000621b0, 0xc0000621c8)になっていることが分かります。
人の名前: <Aさん>、ポインタ: <0xc000062180> 人の名前: <Bさん>、ポインタ: <0xc000062198> 人の名前: <Cさん>、ポインタ: <0xc0000621b0> 人の名前: <Dさん>、ポインタ: <0xc0000621c8> ポインタの参照先の名前 <Bさん>、ポインタ: <0xc000062198> ポインタの参照先の名前 <Cさん>、ポインタ: <0xc0000621b0> ポインタの参照先の名前 <Dさん>、ポインタ: <0xc0000621c8>
メリット・デメリット
この方法のメリット・デメリットは以下の通りです。
- メリット
- 新しい変数のメモリ領域を確保しないので処理が速い
- デメリット
- 新しい変数のメモリ領域を確保しないのでpersonPtrs の参照先の値を書き換えると元の配列の値まで変わってしまう
②ループ毎に別の変数を定義し、そのポインタを使う
別の変数を定義することによりループ毎にメモリ領域を確保し、そのポインタを personPtrs
に入れます。
package main import "fmt" type Person struct { name string age int } func main() { people := []Person{ Person{"Aさん", 56}, Person{"Bさん", 33}, Person{"Cさん", 41}, Person{"Dさん", 22}, } personPtrs := []*Person{} for i, person := range people { personTmp := person // ループ毎に別の変数を定義 fmt.Printf("人の名前: <%s>、ポインタ: <%p>、元の配列の要素のポインタ: <%p>\n", personTmp.name, &personTmp, &people[i]) if personTmp.age <= 50 { personPtrs = append(personPtrs, &personTmp) } } for _, personPtr := range personPtrs { fmt.Printf("ポインタの参照先の名前 <%s>、ポインタ: <%p>\n", personPtr.name, personPtr) } }
以下の実行結果を見ると、ループ毎に変数のポインタが変っていることが分かります。
人の名前: <Aさん>、別変数のポインタ: <0xc000010030>、元の配列の要素のポインタ: <0xc000062180> 人の名前: <Bさん>、別変数のポインタ: <0xc000010048>、元の配列の要素のポインタ: <0xc000062198> 人の名前: <Cさん>、別変数のポインタ: <0xc000010060>、元の配列の要素のポインタ: <0xc0000621b0> 人の名前: <Dさん>、別変数のポインタ: <0xc000010078>、元の配列の要素のポインタ: <0xc0000621c8> ポインタの参照先の名前 <Bさん>、ポインタ: <0xc000010048> ポインタの参照先の名前 <Cさん>、ポインタ: <0xc000010060> ポインタの参照先の名前 <Dさん>、ポインタ: <0xc000010078>
メリット・デメリット
この方法のメリット・デメリットは以下の通りです。
- メリット
- 新しい変数のメモリ領域を確保するので処理が遅い
- デメリット
- 新しい変数のメモリ領域を確保するので
personPtrs
の参照先の値を書き換えても元の配列の値は変わらない
- 新しい変数のメモリ領域を確保するので
結論
ループの回数がさほど多くない場合には、実行速度はさほど気にしなくても良いので ②ループ毎に別の変数を定義し、そのポインタを使う
という方法が良さそうです。
実行速度に対してセンシティブな用途やループ回数が多い場合は、 ①元の配列の要素のポインタを使う
という方法が良さそうです。