Shogo's Blog

May 16, 2018 - 1 minute read - Comments - go golang

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

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

目的

深淵な理由で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なので、自作可能というわけです。

まとめ

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

OctopressからHugoに乗り換えた

comments powered by Disqus