Обнаружение блокировок, переданных по значению в 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 под капотом.

👏👏👏 ниже, чтобы помочь другим узнать эту историю. Пожалуйста, подпишитесь на меня здесь или в Твиттере, если вы хотите получать новости о новых сообщениях или ускорять работу над будущими историями.

Ресурсы