Ответ 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% случаев, но для файлов, открываемых для записи, вы можете захотеть реализовать более сложную проверку ошибок и некоторую стратегию для их устранения (простой отчет, ожидание, затем повтор, спроси-то-может быть-повторить или что-то еще).