Обнаружение блокировок, переданных по значению в Go
Введение в `go tool vet -copylocks`
Go поставляется с утилитой командной строки vet. Он запускает набор эвристик в исходном коде, чтобы найти подозрительные конструкции, такие как недоступный код или вызовы fmt.Printf, где аргументы не соответствуют желаемому формату:
package main import "fmt" func f() { fmt.Printf("%d\n") return fmt.Println("Done") } > go tool vet vet.go vet.go:8: unreachable code vet.go:6: missing argument for Printf("%d"): format reads arg 1, have only 0 args
Эта история конкретно об одном варианте - блокировке. Давайте посмотрим, что он делает и чем может быть полезен в реальных программах.
Допустим, программа использует мьютекс для синхронизации:
package main import "sync" type T struct { lock sync.Mutex } func (t *T) Lock() { t.lock.Lock() } func (t T) Unlock() { t.lock.Unlock() } func main() { t := T{lock: sync.Mutex{}} t.Lock() t.Unlock() t.Lock() }
Если v адресный и набор методов & v содержит m, v.m () является сокращением для (& v) .m ()
Подумайте на мгновение, каким может быть результат выполнения того, что реализовано выше…
Программа попадает в тупик:
fatal error: all goroutines are asleep — deadlock! goroutine 1 [semacquire]: sync.runtime_Semacquire(0x4201162ac) /usr/local/go/src/runtime/sema.go:47 +0x30 sync.(*Mutex).Lock(0x4201162a8) /usr/local/go/src/sync/mutex.go:85 +0xd0 main.(*T).Lock(0x4201162a8) ...
Это нехорошо, и основная причина заключается в передаче получателя по значению в метод Unlock, поэтому t.lock.Unlock()
фактически вызывается для копии блокировки. Это очень легко упустить из виду, особенно в больших программах. Компилятор не обнаруживает его, так как это могло быть намерением программиста. Здесь на помощь приходит ветеринар…
> go tool vet vet.go vet.go:13: Unlock passes lock by value: main.T
Параметр copylocks ( включен по умолчанию) проверяет, является ли переданное по значению чем-то вроде типа, имеющего метод Lock с приемником указателя. В этом случае выдается предупреждение.
Пример использования этого механизма есть в самом пакете sync. Есть специальный тип noCopy. Чтобы защитить тип от копирования по значению (фактически сделать его обнаруживаемым с помощью инструмента vet), в структуру нужно добавить одно поле, например WaitGroup:
package main import "sync" type T struct { wg sync.WaitGroup } func fun(T) {} func main() { t := T{sync.WaitGroup{}} fun(t) } > go tool vet lab.go lab.go:9: fun passes lock by value: main.T contains sync.WaitGroup contains sync.noCopy lab.go:13: function call copies lock value: main.T contains sync.WaitGroup contains sync.noCopy
Под капотом
Исходники помещаются в / src / cmd / vet. Каждая опция для vet регистрируется с помощью функции register, которая принимает (среди прочего) переменный параметр типов узлов AST, которые интересуют опции, и обратный вызов. Эта функция обратного вызова будет запущена для каждого узла указанных типов. Узлы copylocks, которые нужно исследовать, - это операторы возврата. В конечном итоге все идет в lockPath, который проверяет, имеет ли переданное значение тип, имеющий метод приемника указателя с именем Lock. На протяжении всего процесса активно используется пакет go / ast. Мягкое введение в этот пакет можно найти в Тестируемых примерах Go под капотом.
👏👏👏 ниже, чтобы помочь другим узнать эту историю. Пожалуйста, подпишитесь на меня здесь или в Твиттере, если вы хотите получать новости о новых сообщениях или ускорять работу над будущими историями.