defer
defer trong phát triển go hàng ngày là một từ khóa xuất hiện với tần suất rất cao, nó sẽ thực thi hàm liên kết với defer theo cách vào trước ra sau, trong nhiều trường hợp chúng ta tận dụng cơ chế này để thực hiện các thao tác giải phóng tài nguyên, như đóng file.
fd, err := os.Open("/dev/stdin")
if err != nil{
return err
}
defer fd.Close()
...Từ khóa xuất hiện với tần suất cao như vậy khiến chúng ta cần phải hiểu rõ cấu trúc đằng sau nó.
Cấu trúc
Từ khóa defer tương ứng với cấu trúc runtime._defer của runtime, cấu trúc của nó không phức tạp
type _defer struct {
started bool
heap bool
openDefer bool
sp uintptr // sp at time of defer
pc uintptr // pc at time of defer
fn func() // can be nil for open-coded defers
_panic *_panic // panic that is running defer
link *_defer // next defer on G; can point to either heap or stack!
fd unsafe.Pointer // funcdata for the function associated with the frame
varp uintptr // value of varp for the stack frame
framepc uintptr
}Trong đó trường fn là hàm tương ứng với từ khóa defer, link biểu thị defer liên kết tiếp theo, sp và pc ghi lại thông tin hàm của bên gọi, dùng để phán đoán defer thuộc về hàm nào. defer tồn tại dưới hình thức danh sách liên kết trong runtime, đầu của danh sách nằm trên goroutine G, nên defer thực tế liên kết trực tiếp với goroutine.
type g struct {
...
_panic *_panic // innermost panic - offset known to liblink
_defer *_defer // innermost defer
...
}Khi goroutine thực thi hàm, sẽ theo thứ tự thêm defer trong hàm vào đầu danh sách
defer fn1()
defer fn2()
defer fn3()Đoạn code trên tương ứng với hình này

Ngoài goroutine ra, P cũng có liên hệ nhất định với defer, trong cấu trúc của P có một trường deferpool, như dưới đây.
type p struct {
...
deferpool []*_defer // pool of available defer structs (see panic.go)
deferpoolbuf [32]*_defer
...
}Trong deferpool chứa các cấu trúc defer được phân phối trước, dùng để phân phối cấu trúc defer mới cho goroutine G liên kết với P, có thể giảm bớt chi phí.
Phân phối
Trong cú pháp, việc sử dụng từ khóa defer sẽ được compiler chuyển thành lời gọi đến hàm runtime.deferproc. Ví dụ code go viết như sau
defer fn1(x, y)Mà code thực tế sau khi biên dịch là như sau
deferproc(func(){
fn1(x, y)
})Nên thực tế hàm mà defer truyền vào không có tham số cũng không có giá trị trả về, hàm deferproc có code như sau
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()
}Hàm này负责 tạo cấu trúc defer và thêm nó vào đầu danh sách liên kết của goroutine G, trong đó hàm runtime.newdefer sẽ thử lấy cấu trúc defer được phân phối trước từ deferpool trong 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)
}Nó trước tiên sẽ điền một nửa cấu trúc defer từ deferpool toàn cục sched.deferpool vào deferpool cục bộ, sau đó thử lấy từ deferpool trong 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 {
// Allocate new defer.
d = new(_defer)
}
d.heap = trueCuối cùng nếu thực sự không tìm thấy thì mới sử dụng cách phân phối thủ công. Cuối cùng có thể thấy có một đoạn code như sau
d.heap = trueĐiều này biểu thị defer được phân phối trên heap, tương ứng khi nó là false thì sẽ được phân phối trên stack, bộ nhớ phân phối trên stack sẽ tự động thu hồi khi trả về, hiệu suất quản lý bộ nhớ của nó cao hơn so với trên heap, và yếu tố quyết định có phân phối trên stack hay không là số tầng vòng lặp, phần logic này có thể truy ngược đến phương thức escape.goDeferStmt trong cmd/compile/ssagen như sau
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 biểu thị số tầng vòng lặp của câu lệnh hiện tại, nếu câu lệnh defer hiện tại không nằm trong vòng lặp, sẽ được phân phối vào stack.
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)
}Nếu được phân phối trên stack thì sẽ trực tiếp tạo cấu trúc defer trên stack, cuối cùng sẽ do hàm runtime.deferprocStack hoàn thành việc tạo cấu trúc defer.
if k == callDeferStack {
// Make a defer struct d on the stack.
if stksize != 0 {
s.Fatalf("deferprocStack with non-zero stack size %d: %v", stksize, n)
}
t := deferstruct()
...
// Call runtime.deferprocStack with pointer to _defer record.
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 takes a *_defer argHàm deferprocStack có chữ ký như sau
func deferprocStack(d *_defer)Logic tạo cụ thể của nó không khác biệt nhiều so với deferproc, khác biệt chủ yếu là, khi phân phối trên stack thì nguồn gốc của cấu trúc defer là cấu trúc được tạo trực tiếp, còn nguồn gốc của defer phân phối trên heap là hàm new.
Thực thi
Khi hàm sắp trả về hoặc xảy ra panic, sẽ đi vào hàm runtime.deferreturn, nó负责 lấy defer từ danh sách liên kết của goroutine và thực thi.
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()
}
}Đầu tiên sẽ thông qua getcallersp() để lấy stack frame của hàm hiện tại và so sánh với sp trong cấu trúc defer để phán đoán defer có thuộc về hàm hiện tại không, sau đó lấy cấu trúc defer từ đầu danh sách, và sử dụng gp._defer = d.link để thực thi defer tiếp theo, rồi thông qua hàm runtuime.freedefer để giải phóng cấu trúc defer vào pool, cuối cùng gọi fn để thực thi, cứ tiếp tục vòng lặp như vậy cho đến khi thực thi xong tất cả defer thuộc về hàm hiện tại thì kết thúc.
Open-coded
Việc sử dụng defer không phải không có chi phí, mặc dù nó cung cấp sự tiện lợi cho chúng ta về cú pháp, nhưng dù sao nó không phải là lời gọi hàm trực tiếp, ở giữa sẽ trải qua một loạt quá trình, nên vẫn sẽ gây ra tổn hao hiệu suất, nên sau này go official đã thiết kế một phương thức tối ưu hóa——open-coded, nó là một phương thức tối ưu hóa cho defer, tên tiếng Anh gốc là open-coded, trong nước基本上 đều dịch thành开放编码, open ở đây có nghĩa là triển khai, tức là triển khai code của hàm defer vào code của hàm hiện tại, giống như hàm inline. Phương thức tối ưu hóa này có các điều kiện hạn chế sau:
- Số lượng
defertrong hàm không được vượt quá 8 - Tích số lượng của
defervàreturnkhông được vượt quá 15 deferkhông được xuất hiện trong vòng lặp- Không bị vô hiệu hóa tối ưu hóa biên dịch
- Không thủ công gọi
os.Exit() - Không cần sao chép tham số từ heap
Phần logic phán đoán này có thể truy ngược đến phần code dưới đây của hàm 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
}Rồi go sẽ tạo một biến số nguyên 8 bit deferBits trong hàm hiện tại để làm bitmap dùng để đánh dấu defer, mỗi bit đánh dấu một cái, số nguyên 8 bit uint8 nhiều nhất biểu thị 8 cái, nếu bit tương ứng là 1, thì defer sau khi tối ưu hóa open-coded tương ứng sẽ được thực thi khi hàm sắp trả về.
