emahiro/b.log

日々の勉強の記録とか育児の記録とか。

Loop 内の値渡しと参照渡しで久しぶりにハマった話

Overview

Go の loop 処理の中で Slice の中の全値を書き換えたいと言う処理、において参照渡しと値渡しを取り違えて、意図した処理をできずにバグを生んでしまったのでその懺悔のための備忘録です。

何が起きたか?

以下の処理でSliceの全ての ID フィールドを書き換えることはできません。

type X struct {
    ID int
}

func main() {
    x := []X{{ID: 0}, {ID: 0}, {ID: 0}, {ID: 0}}
    for _, v := range x {
        v.ID = 1
    }
    fmt.Println(x)
}

ref: https://play.golang.org/p/wTDbQzO0AwQ

以下の処理では値を書き換えることができます。

type X struct {
    ID int
}

func main() {
    x := []X{{ID: 0}, {ID: 0}, {ID: 0}, {ID: 0}}
    for i := range x {
        x[i].ID = 1
    }
    fmt.Println(x)
}

ref: https://play.golang.org/p/TVHROj7vWD1

原因: 実態のコピーを上書きしていたので大元の slice の struct の値が書き変わらなかった

1 つ目の実装は struct の slice なので loop の中の v は実体がコピーされて毎回同じメモリ空間に同一の値を書き込み続けますが、これは実体のコピーなので書き換えることができません。
要素を struct の参照にすれば書き換えることができます。

参照型の slice じゃないパターンでは各 loop におけるポインタが不変でそこに対して値を上書きしてしまっています。ただし、この場合、loop 内の struct は全く別のものとして扱われ、別参照に対してずっと値を上書きしようとしてるに過ぎず、元の slice の中身は書き変わらりません。
参照型の slice だと書き変わるのは loop 内で参照する値が参照型で、それが指し示す先の slice も書きかわります。
(これが元で参照の slice の loop の中で値の書き換えを行うと、全部同じ値になってしまうのがいわゆる for loop pointer 問題です。)

ref: https://play.golang.org/p/cCgEOYGrnmC

実際に playground での挙動は以下です。

カスタム struct の slice の場合は別参照に対して書き換えていることがわかり、大物 slice は何も更新されていないことがわかります。

https://play.golang.org/p/k5HOsjGR--b

一方で Index を指定したときは以下です。

https://play.golang.org/p/COgCsWlSc5F

大元の slice の各要素のポインタに対して値を書き換えています。

まとめ

久しぶりに初心者みたいな実装ミスして、改めて slice の loop 内での挙動を確認してみました。

追記

20210531

https://github.com/golang/tools/blob/master/gopls/doc/analyzers.md#unusedwrite を教えてもらいました。

The analyzer reports instances of writes to struct fields and arrays that are never read. Specifically, when a struct object or an array is copied, its elements are copied implicitly by the compiler, and any element write to this copy does nothing with the original object.

今回みたいな original の slice に上書きできないケースを検知して警告してくれます。早速 VSCode で設定しました。

"gopls": {
        "analyses": {
            "unusedwrite": true, // NEW
        },
}