概要
Goの sliceは参照渡しだがややこしい
- 前提として Goのsliceは一応参照渡しである
- 参照渡しなので、呼び出し先の関数内の更新は、呼び出し元に反映される (
sli[0] = "updated"
等) - しかし goの組み込み関数
append
を使った更新は呼び出し元に反映されない- 理由は
append
による更新が、単なる変数の再代入で実現されているからである(sli = append(sli, "appended")
等) - このため呼び出し元とヒープで共有するメモリには一切変更が入らない
- 理由は
テストコード
下記のテストコードでは下記の 3種類の関数を呼び出し、呼び出し元への影響を確認している
- 変数の再代入による更新
- 組み込み関数 appendを用いた更新
- 参照に閉じた更新
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestSlice(t *testing.T) {
// 要素3を持つ sliceを3つ用意
sli1, sli2, sli3 :=
[]int{1, 2, 3},
[]int{1, 2, 3},
[]int{1, 2, 3}
// 1:変数の更新, 2:append更新, 3:index指定の更新 を適用
f1, f2, f3 :=
func(sl []int) { sl = []int{4, 5, 6} },
func(sl []int) { sl = append(sl, 4) },
func(sl []int) { sl[2] = 5 }
// 変数の更新
// -> 呼び出し元に影響なし
f1(sli1)
assert.Equal(t, []int{1, 2, 3}, sli1)
// appendによる更新(変数の更新)
// -> 呼び出し元に影響なし
f2(sli2)
assert.Equal(t, []int{1, 2, 3}, sli2)
// index指定の更新(参照先の更新)
// -> 呼び出し元に影響あり(sort.Ints(), reverse()等も同様の仕組み)
f3(sli3)
assert.Equal(t, []int{1, 2, 5}, sli3)
}
学び
- 要素数の変更の無い更新であれば関数に副作用を適用できる
- 要素数の変更の無い更新とは、具体的にはソートやリバース等の並べ替えのことである
- 具体例として
sort
パッケージのsort.Ints([]int)
等が当てはまる - 例外系の方が多いので、一般的な更新系の関数は引数にコレクションを渡し副作用を期待する形式より、結果を戻り値に戻す形式を採用した方が無難そうだ