Go 言語の range は2番目の返り値に同じ参照を返す場合がある

言葉で説明するのだるいので次のコードを見てください。

package main

import (
    "fmt"
)

func main() {
    array := []int{1, 2, 3}
 
    // これだと &a が同じアドレスになってしまうことがある
    // (range は array[i] のコピーを返す)
    for i, a := range array {
        fmt.Printf("%d: %p\n", i, &a)
    }
    // こうするか
    fmt.Println("----------------------------")
    for i := range array {
        a := array[i]
        fmt.Printf("%d: %p\n", i, &a)
    }
    // こうしないといけない
    fmt.Println("----------------------------")
    for i := range array {
        fmt.Printf("%d: %p\n", i, &array[i])
    }
}

for i, a := range ... { とした時に &a が毎回同じアドレスになってしまうことがある、ということです(結局言葉で説明…)。 あくまで range の2番目の返り値は配列の各要素へのポインタではなく値を取得したい場合に使うらしいです。 なので以下のコードはダメ。

package main

import (
  "fmt"
)

type MyObj struct {
  id int
}

func main() {
  array := []MyObj{MyObj{id: 1}, MyObj{id: 2}, MyObj{id: 3}}

  // id >= 2 の要素だけ抜き出す
  filtered := filterGreaterThan1(array)
  // 出力すると自分の環境だと 3 が2回表示された
  for _, a := range filtered {
    fmt.Println(a.id)
  }
}

func filterGreaterThan1(array []MyObj) []*MyObj {
  filtered := make([]*MyObj, 0, len(array))
  for _, a := range array {
    if a.id > 1 {
      // a は同じアドレスの可能性があるため &a を持ちまわると同じ参照を指してしまうことがある
      filtered = append(filtered, &a)
    }
  }
  return filtered
}

Special Thanks