Среда выполнения Go
Семантика среды выполнения Go — defer, panic/recover, порядок инициализации, тонкости языка.
9 вопросов
JuniorТеорияЧастоЧто такое замыкание в Go и как оно связано с переменными охватывающей области видимости?
Что такое замыкание в Go и как оно связано с переменными охватывающей области видимости?
Замыкание — это функциональное значение, которое захватывает переменные охватывающей области по ссылке, а не их копии. Оно может читать и изменять эти переменные, и они живут столько, сколько живёт само замыкание, даже после возврата из функции.
Открыть задачу →Типичные ошибки
- ✗Считать, что замыкание захватывает переменные по значению, делая снимок при создании, а не ссылаясь на живую переменную
- ✗Полагать, что захваченная локальная переменная умирает при возврате функции, а не живёт столько, сколько замыкание
- ✗Путать замыкание с обычной анонимной функцией без доступа к охватывающей области
Уточняющие вопросы
- →Как Go сохраняет захваченную переменную живой после возврата её функции?
- →Что произойдёт, если два замыкания захватят одну и ту же охватывающую переменную?
JuniorКодЧастоЧто делает defer и в каком порядке выполняются несколько отложенных вызовов?
Что делает defer и в каком порядке выполняются несколько отложенных вызовов?
defer планирует вызов на момент возврата из охватывающей функции. Несколько отложенных вызовов выполняются в порядке LIFO — зарегистрированный последним выполняется первым — поэтому печатаются в обратном порядке.
Типичные ошибки
- ✗Ожидать, что отложенные вызовы выполнятся в порядке регистрации (FIFO), а не LIFO
- ✗Думать, что
deferвыполняет вызов сразу, а не при возврате из функции - ✗Считать, что отложенный вызов пропускается при panic в функции
Уточняющие вопросы
- →Выполнится ли отложенный вызов, если в функции произойдёт panic?
- →Где хранятся отложенные вызовы для одной goroutine?
MiddleДебаггингЧастоПочему defer f.Close() внутри цикла — это ошибка и как правильно ограничить очистку?
Почему defer f.Close() внутри цикла — это ошибка и как правильно ограничить очистку?
defer выполняется при возврате функции, а не каждую итерацию, поэтому каждый f.Close() не закрывается до выхода из функции — на многих файлах исчерпываются дескрипторы. Та же ловушка с defer mu.Unlock() даёт самоблокировку на следующей итерации. Исправление — вынести тело цикла в функцию, возвращающуюся каждый раз.
Типичные ошибки
- ✗Думать, что
deferвыполняется в конце каждой итерации, а не при возврате функции - ✗Считать, что накопленные
defer f.Close()безвредны, ведь ОС вернёт дескрипторы при выходе - ✗Упускать, что
defer mu.Unlock()в цикле даёт самоблокировку на второй итерации
Уточняющие вопросы
- →Как оборачивание тела цикла в замыкание или функцию ограничивает
deferодной итерацией? - →Почему
defer mu.Unlock()в цикле приводит к deadlock, аdefer f.Close()лишь течёт?
JuniorКодИногдаЧто выведет это замыкание counter() за три вызова и почему счётчик сохраняется?
Что выведет это замыкание counter() за три вызова и почему счётчик сохраняется?
Выведет 1 2 3. Возвращённая функция захватывает переменную count по ссылке, а не по значению, поэтому единственный count живёт после возврата counter, и каждый вызов увеличивает ту же переменную. Второй counter() создаёт независимый count, снова начиная с 1.
Типичные ошибки
- ✗Думать, что замыкание захватывает
countпо значению, сбрасывая его каждый вызов - ✗Считать, что
countосвобождается при возвратеcounter— escape-анализ переносит его в heap - ✗Ожидать, что два счётчика из
counter()делят одинcount
Уточняющие вопросы
- →Почему escape-анализ переносит
countв heap в этом примере? - →Как изменился бы вывод, если бы
counterвозвращала два замыкания, делящих одинcount?
JuniorДебаггингИногдаПочему переприсваивание параметра-указателя не меняет указатель вызывающего?
Почему переприсваивание параметра-указателя не меняет указатель вызывающего?
Печатает Bob дважды. changeName получает копию указателя; присваивание person = &Person{...} лишь перенаправляет эту локальную копию, не трогая вызывающего. В Go нет передачи по ссылке. Исправление — менять через него: person.Name = "Alice".
Типичные ошибки
- ✗Считать, что параметр-указатель позволяет переприсвоить сам указатель вызывающего внутри функции
- ✗Думать, что Go передаёт аргументы по ссылке, а не всегда по значению (включая значение указателя)
- ✗Полагать, что строка с переприсваиванием не компилируется, а не просто бесполезна для вызывающего
Уточняющие вопросы
- →Как дать функции переуказать переменную вызывающего на совершенно новый
Person? - →Чем
*person = Person{Name: "Alice"}отличается отperson = &Person{...}?
MiddleКодИногдаЧто выведет этот фрагмент и почему обёртывание вызова в замыкание меняет результат?
Что выведет этот фрагмент и почему обёртывание вызова в замыкание меняет результат?
Выведет 0. defer вычисляет аргументы в момент оператора defer, фиксируя значение a (0) тогда; последующее a = 20 на него не влияет. Оберните как defer func(){ fmt.Println(a) }() — замыкание читает a при возврате и печатает 20.
Типичные ошибки
- ✗Полагать, что
deferвычисляет аргументы при возврате из функции, а не при регистрации - ✗Считать, что форма с замыканием захватывает
aпо значению на оператореdefer, а не читает при возврате - ✗Путать момент вычисления аргументов с порядком LIFO выполнения отложенных вызовов
Уточняющие вопросы
- →Если отложить вызов метода на указателе, когда вычисляется получатель?
- →Как
deferвнутри циклаforвзаимодействует с переменной цикла?
MiddleКодИногдаЧто выведет этот фрагмент с двумя параметризованными отложенными замыканиями и в каком порядке?
Что выведет этот фрагмент с двумя параметризованными отложенными замыканиями и в каком порядке?
Выведет exiting: 30, second: 20, first: 10. Аргументы отложенного вызова вычисляются в момент оператора defer, поэтому (a) фиксирует значение a — 10, затем 20 — и последующее a = 30 их не меняет. Отложенные вызовы выполняются в порядке LIFO.
Типичные ошибки
- ✗Полагать, что параметр отложенного замыкания вычисляется при выполнении вызова, а не на операторе
defer - ✗Ожидать отложенные вызовы в порядке регистрации, печатая
firstпередsecond - ✗Считать, что финальное
a = 30перекрывает значения, уже зафиксированные в отложенных вызовах
Уточняющие вопросы
- →Как изменился бы вывод, если бы замыкания читали
aнапрямую вместо параметраval? - →Почему
exiting: 30печатается раньше всех отложенных строк?
MiddleТеорияИногдаКогда компилятор Go может заинлайнить функцию, и что это даёт?
Когда компилятор Go может заинлайнить функцию, и что это даёт?
Инлайнинг копирует тело вызываемой функции в вызывающую, убирая накладные расходы на вызов и открывая код для дальнейших оптимизаций (свёртка констант, escape analysis, удаление мёртвого кода). Компилятор инлайнит только небольшие функции, чья стоимость укладывается в его бюджет инлайнинга; очень крупные тела пропускаются. Посмотреть решения можно через go build -gcflags=-m.
Типичные ошибки
- ✗Думать, что компилятор безусловно инлайнит каждую маленькую функцию
- ✗Считать, что инлайнинг про размер бинарника, а не про устранение накладных расходов на вызов
- ✗Полагать, что функция со многими точками вызова никогда не инлайнится
Уточняющие вопросы
- →Почему очень большое тело функции обычно мешает компилятору её заинлайнить?
- →Как инлайнинг маленького аксессора позволяет компилятору удержать значение на стеке?
SeniorКодРедкоЧто вернёт эта функция и как отложенное замыкание добирается до именованного возвращаемого значения?
Что вернёт эта функция и как отложенное замыкание добирается до именованного возвращаемого значения?
Вернёт 2. При именованном возвращаемом значении return 1 присваивает 1 переменной result, затем выполняется отложенное замыкание и увеличивает result до 2 перед фактическим возвратом. Отложенное замыкание может читать и менять именованные возвращаемые значения после return.
Типичные ошибки
- ✗Думать, что
return 1окончателен и отложенное замыкание не может изменить результат - ✗Полагать, что это работает с безымянным возвратом — только именованное значение изменяемо из
defer - ✗Считать, что отложенное замыкание выполняется до присваивания выражения
return, а не после
Уточняющие вопросы
- →Как этот приём используют, чтобы превратить
panicв возвращаемыйerror? - →Изменился бы результат, если бы возвращаемое значение было безымянным?