Goクイズアドベントカレンダー1日目
December 1, 2020
この記事は、Goクイズ Advent Calendar 2020の1日目の記事です。
問題
以下のコードを実行するとどうなるか?
package main
func main() {
ns := [...]int{1, 2, 3}
v1 := struct{a[len(ns)]int;s[]int}{ns, ns[:1]}
v2 := v1
v2.s = append(v2.s, 4)
v2.a[1] = 5
println(v2.a[1] + v2.s[1] + v1.a[1])
}
- コンパイルエラー
11
と表示される13
と表示される14
と表示される
解答と解説
答えは「2. 解答
11
と表示される」でした!
この問題では、コピーについての理解を問われています。
Goでは、変数の代入や引数に値を渡す際にコピーが発生します。
構造体や配列でも例外ではなく、コピー先で新たに構造体や配列が確保され、
構造体の場合はすべてのフィールドが、配列の場合はすべての要素がコピーされます。
つまり、問題のv1.a
とv2.a
の配列は、v2 := v1
の代入によって、コピーされまったく別ものになります。
また、スライスについても同様に代入時にコピーが発生します。
しかし、スライスは内部に配列へのポインタを保持しているため、スライス自体がコピーされても参照しているポインタが一緒になるため、スライスを介した配列への操作は元のスライスと同じ配列へ行われます。
つまり、問題においてv1.s
とv2.s
のスライスでは、同じ配列についてのスライスになります。
そのため、v2.s = append(v2.s, 4)
を行うとv1.s
が参照している配列に対して要素の追加が行われます。一見、配列v1.a
の要素が変更され、[1, 4, 3]
のようになりそうです。しかし、スライスv1.s
はns
のスライスであり、フィールドv1.a
に設定された配列はそのコピーであり、別ものであるためフィールドv1.a
には影響がありません。
つまり、v1.a
は[1, 2, 3]
で、v1.s
は[1]
、v2.a
は[1 5 3]
、v2.s
は[1 4]
となります。
そのため、println(v2.a[1] + v2.s[1] + v1.a[1])
が出力するのは、5 + 4 + 2
で11
となります。
コピーチェック
Go Code Review Comments(和訳)にあるように、構造体を扱う場合、構造体のコピーは注意深く扱う必要があります。イミュータブルに扱うために、ポインタの使用を避ける場合もありますが、多くの場合は構造体はポインタで扱った方がコピーに関連する見つかりにくいバグに頭を悩まされる必要がなくなります。
フィールドにスライスを持ち、コピーされることを避けたい場合には、strings.Builder
で使用されているコピーチェックの方法を用いると良いでしょう。
例えば、以下のような型T
を定義し、addr
という自身のポインタ格納するためのフィールドを用意します。
type T struct {
addr *T
buf []byte
}
func (t *T) Append(b ...byte) {
t.buf = append(t.buf, b...)
}
型T
の値を以下のようにコピーして用いると、予想外の挙動になります。
変数t1
に1
と3
をAppend
し、変数t2
に2
をAppend
しています。
出力結果は、[1 3]
と[1 2]
になりそうですが、スライスのコピーが行われているため、フィールドbuf
は同じ配列を参照しています。そのため、ともに[1 3]
となってしまいます。
var t1 T
t1.Append(1)
t2 := t1
t2.Append(2)
t1.Append(3)
fmt.Println(t1.buf) // [1 3]
fmt.Println(t2.buf) // [1 3]
このように、型T
の値はコピーされて用いられると問題になるため、コピーされたことを検出できるように以下のようにcopyCheck
メソッドを追加します。そして、Append
メソッドの先頭で呼び出しておくことで、コピーされたことを検出できるようにしておきます。コピーされた場合には、フィールドaddr
の値がレシーバのポインタ値と異なるため、検出ができます。
func (t *T) Append(b ...byte) {
t.copyCheck()
t.buf = append(t.buf, b...)
}
func (t *T) copyCheck() {
if t.addr == nil {
t.addr = t
} else if t.addr != t {
panic("copied!!")
}
}
本稿では、構造体や配列のコピーに関する問題の出題と解説を行いました。 コピーによる予想外の挙動は、クイズだけではなく実際の開発でもたまに見かけるバグです。 コピーをされたくない場合は、ぜひコピーチェックなどの対策をやってみてください。