去年仕込んだネタが見つかってしまったので、macopy 構造体について一応解説。
https://t.co/mHq6oWY3rj
— serinuntius (@_serinuntius) 2018年5月14日
macopyさん構造体だったのか・・・
2021-05-25 追記
今はこの方法では動かないというツイートを見かけました。
これで出てくる "Goの文法を使った構造体のコピーを防ぐ方法" が動かなかった話ですが https://t.co/FpEnspIfmN このへんに書いてありました.重要なことはその型がstructであること,Lock だけでなく Unlockも実装されていることでした.https://t.co/zQc6T058Ip このように変更すると検知されました
— おりさの (@orisano) May 25, 2021
どうやら 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 なので、自作可能というわけです。
まとめ
必要なところにはどんどんまこぴー仕込んでいきましょう。
今後は「ここ、まこぴーしこんどいたほうが良いですね」という会話がGo使いの間でなされるのか
— 猫型🐱蓄音機 (@shinpei0213) 2018年5月15日