[Перевод] Тайна финализаторов в Go
Финализаторы
Когда сборщик мусора Go готов собрать объект, оставшийся без ссылок, предварительно вызывается функция, называемая финализатором. Добавить такую функцию к своему объекту можно при помощи runtime.SetFinalizer
. Посмотрим на него в работе:
package main
import (
"fmt"
"runtime"
"time"
)
type Test struct {
A int
}
func test() {
// создаём указатель
a := &Test{}
// добавляем простой финализатор
runtime.SetFinalizer(a, func(a *Test) { fmt.Println("I AM DEAD") })
}
func main() {
test()
// запускаем сборку мусора
runtime.GC()
// даём время горутине финализатора отработать
time.Sleep(1 * time.Millisecond)
}
Очевидно, что вывод будет:
I AM DEAD
Итак, мы создали объект a
, который является указателем, и поставили на него простой финализатор. Когда функция test()
завершается, все ссылки на a
пропадают, и сборщик мусора получает разрешение собрать его и, следовательно, вызвать финализатор в собственной горутине. Попробуйте изменить test()
так, чтобы она возвращала *Test
и печатала его в main() — вы обнаружите, что финализатор не вызывался. То же самое случится, если убрать поле A
из типа Test
— структура будет пустой, а пустые структуры не занимают памяти и не требуют очистки сборщиком мусора.
Примеры финализаторов
Исходный код стандартной библиотеки Go отлично подходит для изучения языка. Попробуем обнаружить в ней примеры финализаторов — и найдём только использование их при закрытии дескрипторов файлов, как, например, в пакете net:
runtime.SetFinalizer(fd, (*netFD).Close)
Таким образом, файловый дескриптор никогда не утечёт, даже если забыть вызвать Close
у net.Conn
.
Может быть, финализаторы — не такая уж классная штука, раз их почти не использовали авторы стандартной библиотеки? Посмотрим, какие с ними могут быть проблемы.
Почему финализаторов стоит избегать
Идея использовать финализаторы довольно притягательна, особенно для адептов языков без GC или в тех случаях, когда вы не ожидаете от пользователей качественного кода. В Go у нас есть и GC, и опытные разработчики, так что, по моему мнению, лучше всегда явно вызывать Close
, чем использовать магию финализаторов. К примеру, вот финализатор из os, обрабатывающий дескриптор файла:
func NewFile(fd uintptr, name string) *File {
fdi := int(fd)
if fdi < 0 {
return nil
}
f := &File{&file{fd: fdi, name: name}}
runtime.SetFinalizer(f.file, (*file).close)
return f
}
os.NewFile
вызывается функцией os.OpenFile
, которая в свою очередь вызывается из os.Open
, так что этот код исполняется при каждом открытии файла. Одна из проблем финализаторов в том, что они нам неподконтрольны, но, что ещё хуже, они неожиданны. Взгляните на код:
func getFd(path string) (int, error) {
f, err := os.Open(path)
if err != nil {
return -1, err
}
return f.Fd(), nil
}
Это обычный подход к получению дескриптора файла по заданному пути при разработке на Linux. Но этот код ненадёжен: при возврате из getFd
объект f
теряет последнюю ссылку, и ваш файл обречён вскоре закрыться (при следующем цикле сборки мусора). Но проблема здесь не в том, что файл закроется, а в том, что такое поведение недокументированно и совершенно неожиданно.
Вывод
Я считаю, лучше считать пользователей в меру смышлёными и способными самостоятельно подчищать объекты. По крайней мере, все методы, вызывающие SetFinalizer(даже не напрямую как в примере с os.Open
), должны иметь соответствующее упоминание в документации. Я лично считаю этот метод бесполезным и может даже немного вредным.