Финализаторы
Когда сборщик мусора 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
), должны иметь соответствующее упоминание в документации. Я лично считаю этот метод бесполезным и может даже немного вредным.EDIT 1: ivan4th привел пример где использование финализаторов уместно (очистка памяти в C коде): ссылка
EDIT 2: JIghtuse справедливо указал, что поведение метода
Fd
теперь документировано: ссылка. Что лишний раз подтверждает, что свои финализаторы тоже неплохо бы документировать.
Комментарии (12)
tirinox
15.10.2015 10:19+4Мне кажется, ваш пример некорректен. Если уж возвращать, то объект файла, а не дескриптор, тогда он и не помрет раньше времени.
JIghtuse
15.10.2015 14:08+2Но проблема здесь не в том, что файл закроется, а в том, что такое поведение недокументированно и совершенно неожиданно.
func (f *File) Fd() uintptr
Fd returns the integer Unix file descriptor referencing the open file. The file descriptor is valid only until f.Close is called or f is garbage collected.
Во многих языках возвращать нечто из кишков другого нечто — не самая хорошая идея. Тем более в случае, когда первое может пережить второе. Не понимаю, что здесь неожиданного.LK4D4
15.10.2015 16:33Когда я впервые писал этот код с дескрипторами этого не было еще в документации. Конечно же моя ошибка, что я не перепроверил. Спасибо, добавлю в статью.
0xd34df00d
15.10.2015 16:25-1То же самое случится, если убрать поле A из типа Test — структура будет пустой, а пустые структуры не занимают памяти и не требуют очистки сборщиком мусора.
Стройный, логичный язык. Принцип наименьшего удивления в работе, ага.
ivan4th
Если варить кашу из топора, она будет, скорее всего, вовсе не вкуснее каши без топора, и, быть может, будет иметь какой-то привкус — особенно в случае, если топор был грязный. В этой связи считаю топор инструментом бесполезным и даже немного вредным.
Искренне прошу прощения, не удержался. По делу: представьте себе, что вы делаете биндинги для сторонней библиотеки, написанной, например, на C. И в этой библиотеке какие-то объекты должны создаваться функциями create_XXX() (внутри вызывают malloc()) и освобождаться функциями free_XXX(). И вот, честно избегая SetFinalizer, мы пишем в документации, что, хотя Go — язык с GC, конкретно вот эти вот объекты, вследствие особенностей их внутренней реализации, надо обязательно освобождать через какой-нибудь Free(), при том, что эта функция/метод по своей сути не будет делать ничего, кроме освобождения памяти. Звучит логично? По-моему, нет. Как раз для оборачивания подобной логики управления памятью SetFinalizer подходит идеально.
LK4D4
Спасибо, я думаю вы правы. Хотя я наверное все-равно вызывал бы явно.
Можете дать пример на гитхабе, я добавлю в статью?
ivan4th
Вот на вскидку из чьих-то биндингов для библиотеки exiv2: github.com/abustany/goexiv/blob/44f56aad5477a9e8dabe44270c99aeabffec7361/exiv.go#L53 «Дистиллированный» пример прямо сейчас нет времени писать, сорри, от компа уже убегаю.
0xd34df00d
Ещё круче, если это какое-нибудь общесистемное соединение с устройством по какому-нибудь протоколу типа MTP. Если я правильно понимаю, если создать объект прямо перед выходом программы, то без цирка с
конямионо всё утечёт и не закроется.
LK4D4
Все верно — утечёт. И даже цирк может не помочь, потому что некоторые объекты еще будут иметь ссылки. Поэтому надо логику очистки писать руками в таком случае.