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])
}
  1. コンパイルエラー
  2. 11と表示される
  3. 13と表示される
  4. 14と表示される

解答と解説

解答

答えは「2. 11と表示される」でした!

この問題では、コピーについての理解を問われています。 Goでは、変数の代入や引数に値を渡す際にコピーが発生します。 構造体や配列でも例外ではなく、コピー先で新たに構造体や配列が確保され、 構造体の場合はすべてのフィールドが、配列の場合はすべての要素がコピーされます。 つまり、問題のv1.av2.aの配列は、v2 := v1の代入によって、コピーされまったく別ものになります。

また、スライスについても同様に代入時にコピーが発生します。 しかし、スライスは内部に配列へのポインタを保持しているため、スライス自体がコピーされても参照しているポインタが一緒になるため、スライスを介した配列への操作は元のスライスと同じ配列へ行われます。 つまり、問題においてv1.sv2.sのスライスでは、同じ配列についてのスライスになります。

そのため、v2.s = append(v2.s, 4)を行うとv1.sが参照している配列に対して要素の追加が行われます。一見、配列v1.aの要素が変更され、[1, 4, 3]のようになりそうです。しかし、スライスv1.snsのスライスであり、フィールド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 + 211となります。

コピーチェック

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の値を以下のようにコピーして用いると、予想外の挙動になります。 変数t113Appendし、変数t22Appendしています。 出力結果は、[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!!")
	}
}

本稿では、構造体や配列のコピーに関する問題の出題と解説を行いました。 コピーによる予想外の挙動は、クイズだけではなく実際の開発でもたまに見かけるバグです。 コピーをされたくない場合は、ぜひコピーチェックなどの対策をやってみてください。