Подтвердить что ты не робот

Идите деструкторы?

Я знаю, что в Go нет деструкторов, так как технически нет классов. Таким образом, я использую initClass для выполнения тех же функций, что и конструктор. Однако есть ли способ создать что-то, чтобы имитировать деструктор в случае прекращения, для использования, скажем, для закрытия файлов? Прямо сейчас я называю defer deinitClass, но это довольно хаки, и я думаю, что это плохой дизайн. Каким будет правильный путь?

4b9b3361

Ответ 1

В экосистеме Go существует вездесущая идиома для работы с объектами, которые обертывают драгоценные (и/или внешние) ресурсы: специальный метод, предназначенный для освобождения этого ресурса, который называется явно &— обычно с помощью механизма defer.

Этот специальный метод обычно называется Close(), и пользователь объекта должен вызывать его явно, когда он завершает работу с ресурсом, который представляет объект. Стандартный пакет io даже имеет специальный интерфейс io.Closer, объявляющий этот единственный метод. Объекты, реализующие ввод/вывод в различных ресурсах, таких как сокеты TCP, конечные точки UDP и файлы, все удовлетворяют io.Closer, и ожидается, что они будут явно Close d после использования.

Вызов такого метода очистки обычно выполняется с помощью механизма defer, который гарантирует, что метод будет работать независимо от того, будет ли какой-либо код, который выполняется после получения ресурса, panic() или нет.

Вы также можете заметить, что отсутствие неявных "деструкторов" вполне уравновешивает отсутствие неявных "конструкторов" в Go. На самом деле это не имеет ничего общего с отсутствием "классов" в Go: разработчики языка просто избегают магии настолько, насколько это практически возможно.


Обратите внимание, что подход Go к этой проблеме может показаться не слишком технологичным, но на самом деле это единственное работоспособное решение для среды исполнения с сборкой мусора. В языке с объектами, но без GC, скажем, C++, уничтожение объекта является четко определенной операцией, потому что объект уничтожается либо когда он выходит из области видимости, либо когда вызывается delete в его блоке памяти. Во время выполнения с помощью GC объект будет уничтожен в некоторой неопределенной точке в будущем при сканировании GC и может вообще не быть уничтожен. Так что, если объект оборачивается каким-то ценным ресурсом, этот ресурс может вообще не быть восстановлен, как хорошо объяснил @twotwotwo в их соответствующем ответе.

Еще один интересный аспект, который следует учитывать, - это то, что Go GC полностью параллелен (при обычном выполнении программы). Это означает, что поток GC, который собирается собирать мертвый объект, может (и обычно будет) не быть потоком (ами), который выполнил этот объектный код, когда он был жив. В свою очередь это означает, что если у типов Go могут быть деструкторы, то программист должен убедиться, что любой код, который выполняет деструктор, правильно синхронизирован с остальной частью программы, если состояние объекта влияет на некоторые внешние по отношению к нему структуры данных. Это фактически может заставить программиста добавить такую синхронизацию, даже если объект не нуждается в ней для нормальной работы (и большинство объектов попадают в такую категорию). И подумайте о том, что происходит с этими внешними структурами данных, которые были уничтожены до вызова деструктора объекта (сборщик мусора собирает мертвые объекты недетерминированным способом). Другими словами, гораздо проще контролировать - и рассуждать - уничтожение объекта, когда он явно закодирован в поток программы: как для указания, когда объект должен быть уничтожен, так и для обеспечения правильного порядка его уничтожения в отношении уничтожения внешних структур данных.

Если вы знакомы с .NET, он занимается очисткой ресурсов способом, очень похожим на метод Go: ваши объекты, которые обертывают какой-то драгоценный ресурс, должны реализовать интерфейс IDisposable, а экспортированный метод Dispose() этим интерфейсом, должен быть вызван явно, когда вы закончите с таким объектом. С# предоставляет некоторый синтаксический сахар для этого варианта использования через оператор using, который заставляет компилятор организовать вызов Dispose() для объекта, когда он выходит из области действия, объявленной указанным оператором. В Go вы обычно defer обращаетесь к методам очистки.


Еще одно замечание осторожности. Go хочет, чтобы вы относились к ошибкам очень серьезно (в отличие от большинства основных языков программирования с их ", просто выдавайте исключение и не сообщайте о том, что происходит из-за этого в другом месте и в каком состоянии программа будет в" отношении) и поэтому вы можете рассмотреть возможность проверки ошибок по крайней мере некоторых вызовов методов очистки.

Хорошим примером являются экземпляры типа os.File, представляющие файлы в файловой системе. Самое интересное в том, что вызов Close() для открытого файла может завершиться неудачей по законным причинам, и если вы выполняете запись в этот файл, это может указывать на то, что не все данные, которые вы записали в этот файл, фактически попали в него в файловой системе., Для объяснения, пожалуйста, прочитайте раздел "Примечания" в руководстве close(2).

Другими словами, просто делать что-то вроде

fd, err := os.Open("foo.txt")
defer fd.Close()

подходит для файлов только для чтения в 99,9% случаев, но для файлов, открываемых для записи, вы можете захотеть реализовать более сложную проверку ошибок и некоторую стратегию для их устранения (простой отчет, ожидание, затем повтор, спроси-то-может быть-повторить или что-то еще).

Ответ 2

runtime.SetFinalizer(ptr, finalizerFunc) устанавливает финализатор - не деструктор, а другой механизм, который может в конечном итоге освободить ресурсы. Прочтите документацию для получения подробной информации, включая недостатки. Они могут не запускаться до тех пор, пока объект не будет недоступен, и они могут вообще не запускаться, если программа выйдет в первую очередь. Они также откладывают освобождение памяти для другого цикла GC.

Если вы приобретаете ограниченный ресурс, который еще не имеет финализатора, и в конечном итоге программа не сможет продолжить, если она будет протекать, вам следует рассмотреть возможность установки финализатора. Он может уменьшить утечки. Недостижимые файлы и сетевые подключения уже очищены финализаторами в stdlib, поэтому это могут быть только другие виды ресурсов, в которых пользовательские могут быть полезны. Самый очевидный класс - это системные ресурсы, которые вы получаете через syscall или cgo, но я могу представить других.

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

В повседневном программировании Go вы не видите много вызовов SetFinalizer, отчасти потому, что самые важные из них находятся в стандартной библиотеке и в основном из-за ограниченного диапазона применимости в целом.

Короче говоря, финализаторы могут помочь, освободив забытые ресурсы в долгосрочных программах, но так как мало что о их поведении гарантировано, они не подходят для вашего основного механизма управления ресурсами.

Ответ 3

В Go есть финализаторы. Я написал немного сообщение в блоге об этом. Они даже использовали для закрытия файлов в стандартной библиотеке, так как вы можете видеть здесь. Однако я думаю, что использование defer более предпочтительным, потому что оно более читаемо и менее волшебное.