defer làm gì
defer đẩy một lời gọi hàm vào một danh sách. Khi hàm bao quanh trả về, danh sách chạy theo thứ tự ngược lại.
Đầu ra:
start
end
deferred 3
deferred 2
deferred 1
Thứ tự là vào sau, ra trước, giống một ngăn xếp. Điều đó khớp với cách tài nguyên lồng nhau: nếu bạn mở A rồi đến B, thường bạn muốn đóng B trước A.
Dọn dẹp ngay cạnh chỗ lấy tài nguyên
Công dụng chính của defer là viết phần dọn dẹp ở dòng ngay sau chỗ lấy tài nguyên, để không đường trả về nào có thể quên nó.
Để ý thứ tự: kiểm tra lỗi trước, rồi mới defer. Nếu os.Open thất bại, f là nil, và defer f.Close() trước khi kiểm tra sẽ gọi Close trên một *os.File nil (trả về một lỗi bạn không bao giờ thấy, và gây hiểu nhầm cho người đọc).
Cùng dạng đó dùng được cho khóa:
mu.Lock()
defer mu.Unlock()
Nếu đoạn code ở giữa gây panic, mutex vẫn được mở khóa.
Đối số được tính ngay lập tức
Hàm được defer và các đối số của nó được tính khi câu lệnh defer chạy. Chỉ có bản thân lời gọi là phải chờ.
Đầu ra:
x is now 2
deferred closure reads: 2
deferred with argument: 1
fmt.Println("...", x) đã lấy giá trị 1 tại dòng defer. Closure không có đối số; nó đọc x khi cuối cùng được chạy. Hãy chọn dạng khớp với điều bạn muốn ghi lại.
Điều này cũng áp dụng cho receiver của phương thức. defer t.Stop() tính t ngay lập tức, nên gán lại t sau đó không làm thay đổi giá trị nào sẽ bị dừng.
Một mẹo đo thời gian phổ biến cố ý dùng quy tắc này:
func handle() {
defer trace("handle")() // trace runs now, the returned func runs at exit
// ...
}
trace("handle") được gọi ngay (nó có thể in "enter" và ghi lại thời điểm bắt đầu), và hàm nó trả về mới là thứ được defer.
defer trong vòng lặp
Các lời gọi defer chạy khi hàm trả về, không phải ở cuối mỗi lần lặp. Trong một vòng lặp qua nhiều tệp, điều này giữ mọi tệp mở cho đến khi hàm kết thúc.
for _, path := range paths {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // all files stay open until the function returns
process(f)
}
Với hàng nghìn đường dẫn, đoạn code này sẽ cạn file descriptor. Hãy chuyển thân vòng lặp vào một hàm riêng để mỗi defer chạy theo từng lần lặp:
Mỗi lần gọi processFile đóng tệp của nó trước khi tệp tiếp theo được mở. Một function literal gọi tại chỗ (func() { ... }()) hoạt động tương tự khi một hàm hỗ trợ có tên có vẻ hơi thừa.
Thay đổi giá trị trả về
Một closure được defer chạy sau khi câu lệnh return đã gán kết quả, và nó có thể sửa các kết quả có tên trước khi bên gọi nhìn thấy chúng.
Đoạn này in ra 10 và save failed: disk full. Với kết quả không có tên, hàm defer vẫn chạy, nhưng không có cách nào thay đổi giá trị được trả về.
Giữ lại lỗi từ Close
defer f.Close() vứt bỏ lỗi từ Close. Với tệp chỉ đọc thì không sao. Với tệp bạn đã ghi, Close có thể báo một lần ghi cuối thất bại, nên lỗi đó quan trọng. Một kết quả có tên cho phép bạn giữ lại nó:
func writeReport(path string, data []byte) (err error) {
f, err := os.Create(path)
if err != nil {
return err
}
defer func() {
if cerr := f.Close(); cerr != nil && err == nil {
err = cerr
}
}()
_, err = f.Write(data)
return err
}
errors.Join(err, f.Close()) là một cách ngắn hơn khi bạn muốn báo cả hai lỗi.
defer, panic và recover
Các lời gọi defer chạy trong lúc panic tháo ngăn xếp. Đó là nơi duy nhất recover có tác dụng, và là cách một máy chủ ngăn một request lỗi làm sập cả tiến trình. Chi tiết có ở trang panic và recover.
Các lời gọi defer không chạy khi chương trình thoát qua os.Exit hoặc log.Fatal. Nếu main defer phần dọn dẹp rồi gọi os.Exit(1), phần dọn dẹp bị bỏ qua.
Chi phí
Từ Go 1.14, phần lớn defer được trình biên dịch open-code và chỉ tốn vài nano giây. Dùng defer cho mọi lần nhả mutex và đóng tệp là phong cách bình thường. Defer bên trong vòng lặp là ngoại lệ: chúng không open-code được và phải dùng đường đi chậm hơn, thêm một lý do để chuyển thân vòng lặp vào hàm riêng.
Lỗi thường gặp
- Defer trước khi kiểm tra lỗi. Kiểm tra
errtừOpentrước, rồi mớidefer Close. - Mong dọn dẹp theo từng lần lặp trong vòng lặp. Defer chạy khi hàm kết thúc.
- Mong đối số được defer thấy các thay đổi sau đó. Đối số được chốt tại dòng
defer. Dùng closure để đọc giá trị lúc kết thúc. - Trông cậy vào defer khi có
os.Exit. Nó không bao giờ chạy.
Câu hỏi thường gặp
defer trong Go dùng để làm gì?
defer f() lên lịch cho f() chạy khi hàm bao quanh trả về, dù hàm trả về bình thường, qua một return sớm, hay do panic. Nó được dùng để đặt phần dọn dẹp (đóng tệp, mở khóa mutex) ngay cạnh đoạn code lấy tài nguyên.
Các lời gọi defer trong Go chạy theo thứ tự nào?
Vào sau, ra trước. Lời gọi được defer gần nhất chạy đầu tiên. defer fmt.Println(1); defer fmt.Println(2) in ra 2 rồi mới đến 1.
Đối số của một hàm defer được tính khi nào?
Ngay lập tức, khi câu lệnh defer thực thi, không phải khi lời gọi chạy. x := 1; defer fmt.Println(x); x = 2 in ra 1. Để đọc giá trị lúc hàm kết thúc, hãy defer một closure: defer func() { fmt.Println(x) }().
defer có chạy khi panic hoặc os.Exit không?
Các lời gọi defer chạy trong lúc panic tháo ngăn xếp, đó là lý do recover hoạt động bên trong chúng. Chúng không chạy khi chương trình gọi os.Exit (hoặc log.Fatal, hàm gọi tới nó), và không chạy trong các goroutine khác khi main trả về.