Go Zero to Hero - Bài 27: Từ khoá defer và thứ tự thực thi LIFO
Từ khóa defer (trì hoãn) được đặt ngay trước một lời gọi hàm. Nó có tác dụng: Đẩy hàm đó vào một danh sách chờ, và chỉ thực thi hàm đó NGAY TRƯỚC KHI hàm cha chứa nó chuẩn bị return (kết thúc).
1. Cơ chế hoạt động cơ bản
Hãy xem ví dụ đơn giản sau:
package main
import "fmt"
func main() {
defer fmt.Println("1. Dòng này có chữ defer, nhưng sẽ chạy CUỐI CÙNG.")
fmt.Println("2. Đang thực thi logic hệ thống...")
fmt.Println("3. Chuẩn bị kết thúc hàm main.")
// Ngay tại điểm này, trước khi hàm main thực sự thoát,
// Go sẽ lôi dòng số (1) ra để chạy.
}
Kết quả in ra:
2. Đang thực thi logic hệ thống...
3. Chuẩn bị kết thúc hàm main.
1. Dòng này có chữ defer, nhưng sẽ chạy CUỐI CÙNG.
2. Ứng dụng thực chiến: Viết ngay sau khi Mở (Open)
Tại sao không viết lệnh dọn dẹp (cleanup) ở cuối hàm cho rồi, mà phải dùng defer?
Trong thực tế, một hàm Backend có thể dài hàng chục dòng, chứa chằng chịt các lệnh if err != nil { return err }. Nếu bạn để lệnh dọn dẹp ở cuối hàm, bạn sẽ phải lặp lại lệnh đó ở mọi ngóc ngách có từ khóa return. Điều này vi phạm nguyên tắc DRY (Don't Repeat Yourself) và cực kỳ dễ rò rỉ bộ nhớ nếu bạn lỡ quên viết ở một nhánh rẽ nào đó.
Triết lý của Go: Vừa khởi tạo thành công, hãy defer việc dọn dẹp nó ngay lập tức ở dòng tiếp theo!
func queryTicketData() error {
// 1. Mở kết nối Database
db, err := sql.Open("postgres", "postgres://user:pass@localhost/afc_db")
if err != nil {
return err // Lỗi ngay lúc mở thì không cần đóng
}
// 2. NGAY LẬP TỨC đặt lịch đóng kết nối
defer db.Close()
// --- Bắt đầu phần logic xử lý ---
// Giả sử đoạn dưới này có hàng tá lệnh if-else, return giữa chừng
// Bạn sẽ KHÔNG BAO GIỜ phải lo về việc connection db bị treo (leak) nữa
// vì db.Close() chắc chắn sẽ được tự động gọi trước mọi lệnh return.
return nil
}
3. Thứ tự LIFO: Vào Sau - Ra Trước (Last-In, First-Out)
Điều gì xảy ra nếu bạn gọi nhiều lệnh defer trong cùng một hàm? Go quản lý các lệnh defer này bằng một cấu trúc dữ liệu Stack (Ngăn xếp).
Quy tắc của Stack là LIFO: Thằng nào bị đẩy vào ngăn xếp cuối cùng (Last-In), thì sẽ được lôi ra chạy đầu tiên (First-Out).
Hãy tưởng tượng bạn đang mặc quần áo: Bạn mặc áo thun (1), rồi mặc áo khoác (2). Khi về nhà cởi ra (dọn dẹp), bạn phải cởi áo khoác (2) trước, rồi mới cởi áo thun (1).
func main() {
fmt.Println("Bắt đầu:")
defer fmt.Println("Defer số 1")
defer fmt.Println("Defer số 2")
defer fmt.Println("Defer số 3")
fmt.Println("Kết thúc.")
}
Kết quả in ra:
Bắt đầu:
Kết thúc.
Defer số 3
Defer số 2
Defer số 1
Thực chiến LIFO với Mutex Lock:
Khi làm việc với các hệ thống đa luồng (High Concurrency), việc khóa (Lock) dữ liệu dùng chung là bắt buộc để tránh Data Race. defer kết hợp LIFO giúp bạn giải quyết việc này hoàn hảo:
var mutex sync.Mutex
func updateBalance() {
mutex.Lock()
// Mở khóa ngay sau khi lock thành công
defer mutex.Unlock()
// ... logic cộng trừ tiền phức tạp
}
4. "Cái bẫy" phỏng vấn: Tham số của defer được đánh giá khi nào?
Đây là câu hỏi kinh điển để phân loại junior và mid-level Gopher.
Quy tắc thép: Tuy hàm
deferbị trì hoãn thực thi đến cuối hàm cha, nhưng CÁC THAM SỐ (arguments) truyền vào hàmdefersẽ được tính toán (evaluate) NGAY TẠI THỜI ĐIỂM dòngdeferđược khai báo.
Hãy xem đoạn code đánh lừa thị giác này:
func main() {
ticketCount := 10
// Tham số 'ticketCount' được chụp lại NGAY TẠI ĐÂY (giá trị là 10)
defer fmt.Println("Số vé được defer:", ticketCount)
// Sau đó ta thay đổi biến
ticketCount = 50
fmt.Println("Số vé thực tế cuối hàm:", ticketCount)
}
Kết quả:
Số vé thực tế cuối hàm: 50
Số vé được defer: 10 <-- Chú ý! Nó in ra 10 chứ không phải 50!
Nếu bạn muốn lệnh defer in ra giá trị mới nhất (50) ở cuối hàm, bạn bắt buộc phải sử dụng Closure (Hàm ẩn danh) để tạo tham chiếu đến biến ticketCount thay vì copy giá trị của nó ngay lập tức:
// Chạy hàm ẩn danh bằng () ở cuối
defer func() {
fmt.Println("Số vé được defer qua Closure:", ticketCount) // Sẽ in ra 50
}()
Tổng kết
Bạn có thể quên nhiều thứ, nhưng hãy khắc cốt ghi tâm 3 nguyên tắc này khi làm Backend với Go:
- Vừa Mở xong là Defer: Gắn
defer resource.Close()ngay dưới dòng mở resource thành công. Nó cứu sống server của bạn khỏi cảnh sập vì hết RAM/Connection. - LIFO Stack:
defergọi sau sẽ chạy trước (ngược từ dưới lên trên). - Bẫy tham số: Giá trị truyền trực tiếp vào
deferđược "chụp" ngay lúc khai báo. Muốn lấy giá trị muộn, hãy bọc nó trong một Closure (func() {...}()).
All rights reserved