defer
defer — это часто используемое ключевое слово в повседневной разработке на Go. Оно выполняет функции, связанные с defer, в порядке LIFO (Last In First Out). Мы часто используем этот механизм для операций освобождения ресурсов, таких как закрытие файлов.
fd, err := os.Open("/dev/stdin")
if err != nil{
return err
}
defer fd.Close()
...Учитывая, как часто появляется это ключевое слово, нам необходимо понять лежащую в его основе структуру.
Структура
Ключевое слово defer соответствует структуре runtime._defer. Её структура не сложна:
type _defer struct {
started bool
heap bool
openDefer bool
sp uintptr // sp во время defer
pc uintptr // pc во время defer
fn func() // может быть nil для open-coded defers
_panic *_panic // panic, который выполняет defer
link *_defer // следующий defer на G; может указывать на кучу или стек!
fd unsafe.Pointer // funcdata для функции, связанной с фреймом
varp uintptr // значение varp для стекового фрейма
framepc uintptr
}Поле fn — это функция, соответствующая ключевому слову defer, link представляет следующий связанный defer, а sp и pc записывают информацию о функции вызывающего, используемую для определения, какому функции принадлежит defer. defer существует как связный список во время выполнения, с головой списка на goroutine G, поэтому defer фактически напрямую связан с goroutine.
type g struct {
...
_panic *_panic // внутренний panic - смещение известно liblink
_defer *_defer // внутренний defer
...
}Когда goroutine выполняет функцию, она последовательно добавляет defer из функции в голову связного списка.
defer fn1()
defer fn2()
defer fn3()Приведённый выше код соответствует этой диаграмме:

Помимо goroutines, P также имеет определённое отношение к defer. В структуре P есть поле deferpool, как показано ниже.
type p struct {
...
deferpool []*_defer // пул доступных структур defer (см. panic.go)
deferpoolbuf [32]*_defer
...
}deferpool содержит предварительно выделенные структуры defer, используемые для выделения новых структур defer для связанных goroutine G с P, что может уменьшить накладные расходы.
Выделение
С точки зрения синтаксиса, при использовании ключевого слова defer компилятор транслирует это в вызов функции runtime.deferproc. Например, если код Go написан так:
defer fn1(x, y)Фактически скомпилированный код выглядит так:
deferproc(func(){
fn1(x, y)
})Таким образом, в реальности функция, передаваемая в defer, не имеет параметров и возвращаемых значений. Код функции deferproc следующий:
func deferproc(fn func()) {
gp := getg()
d := newdefer()
d.link = gp._defer
gp._defer = d
d.fn = fn
d.pc = getcallerpc()
d.sp = getcallersp()
return0()
}Эта функция отвечает за создание структуры defer и добавление её в голову связного списка goroutine G. Функция runtime.newdefer пытается получить предварительно выделенную структуру defer из deferpool в P.
if len(pp.deferpool) == 0 && sched.deferpool != nil {
lock(&sched.deferlock)
for len(pp.deferpool) < cap(pp.deferpool)/2 && sched.deferpool != nil {
d := sched.deferpool
sched.deferpool = d.link
d.link = nil
pp.deferpool = append(pp.deferpool, d)
}
unlock(&sched.deferlock)
}Сначала заполняет половину локального deferpool из глобального sched.deferpool, затем пытается получить из deferpool P.
if n := len(pp.deferpool); n > 0 {
d = pp.deferpool[n-1]
pp.deferpool[n-1] = nil
pp.deferpool = pp.deferpool[:n-1]
}
if d == nil {
// Выделяем новый defer.
d = new(_defer)
}
d.heap = trueТолько когда ничего не может быть найдено, будет использоваться ручное выделение. Наконец, вы можете видеть этот код:
d.heap = trueЭто указывает, что defer выделяется в куче. Соответственно, когда оно false, оно выделяется в стеке. Память, выделенная в стеке, автоматически возвращается при возврате, и её эффективность управления памятью выше, чем в куче. Фактором, определяющим, выделять ли в стеке, является количество уровней цикла. Эта логика может быть прослежена до метода escape.goDeferStmt в cmd/compile/ssagen:
func (e *escape) goDeferStmt(n *ir.GoDeferStmt) {
...
if n.Op() == ir.ODEFER && e.loopDepth == 1 {
k = e.later(e.discardHole())
n.SetEsc(ir.EscNever)
}
...
}e.loopDepth представляет количество уровней цикла для текущего оператора. Если текущий оператор defer не находится в цикле, он будет выделен в стеке.
case ir.ODEFER:
n := n.(*ir.GoDeferStmt)
if s.hasOpenDefers {
s.openDeferRecord(n.Call.(*ir.CallExpr))
} else {
d := callDefer
if n.Esc() == ir.EscNever {
d = callDeferStack
}
s.callResult(n.Call.(*ir.CallExpr), d)
}Если выделено в стеке, структура defer создаётся напрямую в стеке, и в конечном итоге функция runtime.deferprocStack завершает создание структуры defer.
if k == callDeferStack {
// Создаём структуру defer d в стеке.
if stksize != 0 {
s.Fatalf("deferprocStack with non-zero stack size %d: %v", stksize, n)
}
t := deferstruct()
...
// Вызываем runtime.deferprocStack с указателем на запись _defer.
ACArgs = append(ACArgs, types.Types[types.TUINTPTR])
aux := ssa.StaticAuxCall(ir.Syms.DeferprocStack, s.f.ABIDefault.ABIAnalyzeTypes(nil, ACArgs, ACResults))
callArgs = append(callArgs, addr, s.mem())
call = s.newValue0A(ssa.OpStaticLECall, aux.LateExpansionResultType(), aux)
call.AddArgs(callArgs...)
call.AuxInt = int64(types.PtrSize) // deferprocStack принимает аргумент *_deferСигнатура функции deferprocStack следующая:
func deferprocStack(d *_defer)Конкретная логика создания не сильно отличается от deferproc. Основное различие в том, что при выделении в стеке структура defer происходит из напрямую созданной структуры, в то время как defer, выделенный в куче, происходит из функции new.
Выполнение
Когда функция собирается вернуться или когда происходит panic, входит в функцию runtime.deferreturn, которая отвечает за взятие defer из связного списка goroutine и его выполнение.
func deferreturn() {
gp := getg()
for {
d := gp._defer
sp := getcallersp()
if d.sp != sp {
return
}
fn := d.fn
d.fn = nil
gp._defer = d.link
freedefer(d)
fn()
}
}Сначала использует getcallersp() для получения текущего стекового фрейма функции и сравнивает его с sp в структуре defer для определения, принадлежит ли defer текущей функции. Затем берёт структуру defer из головы связного списка, выполняет следующий defer используя gp._defer = d.link, освобождает структуру defer обратно в пул через функцию runtuime.freedefer, и наконец вызывает fn для выполнения. Это продолжается в цикле, пока все defer, принадлежащие текущей функции, не будут выполнены.
Open-Coded Defer
Использование defer не без затрат. Хотя оно предоставляет удобство в синтаксисе, это не прямой вызов функции и проходит через серию процессов, поэтому действительно вызывает накладные расходы на производительность. Поэтому чиновники Go позже разработали метод оптимизации — open-coded defer. Это оптимизация для defer, первоначально названная "open-coded", которая переводится как "开放编码" на китайском. Здесь "open" означает раскрыть, т.е. раскрыть код функции defer в текущий код функции, подобно встраиванию функции. Этот метод оптимизации имеет следующие ограничения:
- Количество
deferв функции не может превышать 8 - Произведение количества
deferиreturnне может превышать 15 deferне может появляться в цикле- Оптимизация компилятора не отключена
- Нет ручного вызова
os.Exit() - Не нужно копировать параметры из кучи
Эта логика суждения может быть прослежена до следующей части функции cmd/compile/ssagen.buildssa:
s.hasOpenDefers = base.Flag.N == 0 && s.hasdefer && !s.curfn.OpenCodedDeferDisallowed()
if s.hasOpenDefers && len(s.curfn.Exit) > 0 {
s.hasOpenDefers = false
}
if s.hasOpenDefers {
for _, f := range s.curfn.Type().Results().FieldSlice() {
if !f.Nname.(*ir.Name).OnStack() {
s.hasOpenDefers = false
break
}
}
}
if s.hasOpenDefers && s.curfn.NumReturns*s.curfn.NumDefers > 15 {
s.hasOpenDefers = false
}Затем Go создаёт 8-битную целочисленную переменную deferBits в текущей функции, служащую битовой картой для маркировки defer. Каждый бит маркирует один defer. 8-битное целое uint8 может представлять максимум 8 defer. Если соответствующий бит равен 1, соответствующий open-coded оптимизированный defer будет выполнен при возврате функции.
