Shogo's Blog

May 16, 2018 - 2 minute read - go golang

Goの構造体のコピーを防止する方法

去年仕込んだネタが見つかってしまったので、macopy 構造体について一応解説。

2021-05-25 追記

今はこの方法では動かないというツイートを見かけました。

どうやら Go 1.11 から判定基準が 「sync.Locker を実装しているか」に変わっていたようです。 (修正コミット: c2eba53, Issue: #26165)

というわけで、 macopy 構造体を以下のように変更する必要があります。

type macopy struct{}

func (*macopy) Lock() {}
func (*macopy) Unlock() {}

追記ここまで

目的

深淵な理由で Go の構造体のコピーを禁止したい場合があると思います。 kuiperbelt のケースでは、sync/atomic パッケージを使ってフィールドを更新しているので、 フィールドへの読み書きは必ず sync/atomic パッケージを使わなければなりません。 sync/atomic パッケージを使わずに構造体をコピーするとレースコンディションが発生してしまうので、コピーを禁止する必要がありました。

// https://github.com/kuiperbelt/kuiperbelt/blob/e3c1432ed798716c8e88183518f9126951c227f3/stats.go#L20-L28
type Stats struct {
	connections        int64
	totalConnections   int64
	totalMessages      int64
	connectErrors      int64
	messageErrors      int64
	closingConnections int64
	noCopy             macopy
}

// atomic.AddInt64 を使っているので、s.connections の読み取り時には必ずこのメソッドを呼んで欲しい。
func (s *Stats) Connections() int64 {
  // return s.connections ではレースコンディションになってしまう。
	return atomic.LoadInt64(&s.connections)
}

func (s *Stats) ConnectEvent() {
	atomic.AddInt64(&s.totalConnections, 1)
	atomic.AddInt64(&s.connections, 1)
}

macopy 構造体の使い方

そこで登場するのが macopy 構造体です(いや、もちろん別の名前でもいいんですが)。

// https://github.com/kuiperbelt/kuiperbelt/blob/e3c1432ed798716c8e88183518f9126951c227f3/stats.go#L12-L18

// macopy may be embedded into structs which must not be copied
// after the first use.
// See https://github.com/golang/go/issues/8005#issuecomment-190753527
// for details.
type macopy struct{}

func (*macopy) Lock() {}

ここで例えば以下のようなコードを書いてしまったとします。

package kuiperbelt

func hoge() {
	var noCopy macopy
	_ = noCopy
}

このコードを go vet でチェックすると、誤ってコピーしていることを指摘してくれます。

$ go vet
# github.com/kuiperbelt/kuiperbelt
./test.go:5: assignment copies lock value to _: kuiperbelt.macopy

コンパイル自体はできてしまうので完全に禁止することはできませんが、 Gopher なみなさんなら go vet は CI とかエディターの拡張等で自動的に実行するようにしてあるでしょうから、 これでコピーを防ぐことができるでしょう。

もちろんこの機能は構造体のフィールドに含まれている場合も指摘してくれます。

原理

これはもともと sync.Mutex構造体のコピーを防ぐための機能です。 この機能がどうやって実装されているか go vet のコードをあさっていくと・・・

// https://github.com/golang/go/blob/3868a371a85f2edbf2132d0bd5a6ed9193310dd7/src/cmd/vet/copylock.go#L240-L244

	if plock := types.NewMethodSet(types.NewPointer(typ)).Lookup(tpkg, "Lock"); plock != nil {
		if lock := types.NewMethodSet(typ).Lookup(tpkg, "Lock"); lock == nil {
			return []types.Type{typ}
		}
	}

sync.Mutex構造体のコピーをチェックしているのではなく、 Lock メソッドが存在している型のコピーをチェックしていることがわかります。

というわけで、sync.Mutex構造体にかぎらず、Lockメソッドを実装さえしていれば OK なので、自作可能というわけです。

まとめ

必要なところにはどんどんまこぴー仕込んでいきましょう。