1 Giới thiệu về tính năng defer
trong Golang
Trong ngôn ngữ Go, câu lệnh defer
trì hoãn việc thực thi cuộc gọi hàm sau nó cho đến khi hàm chứa câu lệnh defer
chuẩn bị hoàn thành việc thực thi. Bạn có thể hiểu nó như khối finally
trong các ngôn ngữ lập trình khác, nhưng cách sử dụng của defer
linh hoạt và độc đáo hơn.
Lợi ích của việc sử dụng defer
là nó có thể được sử dụng để thực hiện các nhiệm vụ dọn dẹp, chẳng hạn như đóng các tệp, mở khóa mutex, hoặc đơn giản chỉ ghi lại thời gian kết thúc của một hàm. Điều này có thể khiến chương trình mạnh mẽ hơn và giảm thiểu công việc lập trình khi xử lý ngoại lệ. Trong triết lý thiết kế của Go, việc sử dụng defer
được khuyến nghị vì nó giúp giữ mã nguồn ngắn gọn và dễ đọc khi xử lý lỗi, dọn dẹp tài nguyên và các hoạt động sau đó khác.
2 Nguyên lý hoạt động của defer
2.1 Nguyên lý hoạt động cơ bản
Nguyên lý hoạt động cơ bản của defer
là sử dụng một ngăn xếp (nguyên tắc vào sau ra trước) để lưu trữ mỗi hàm trì hoãn sẽ được thực thi. Khi một câu lệnh defer
xuất hiện, ngôn ngữ Go không thực thi ngay lập tức hàm theo sau câu lệnh. Thay vào đó, nó đẩy nó vào một ngăn xếp riêng. Chỉ khi hàm bên ngoài chuẩn bị trả về, những hàm trì hoãn này sẽ được thực thi theo thứ tự của ngăn xếp, với hàm trong câu lệnh defer
được khai báo sau cùng được thực thi trước.
Hơn nữa, đáng lưu ý rằng các tham số trong các hàm theo sau câu lệnh defer
được tính toán và cố định vào thời điểm khi defer
được khai báo, chứ không phải vào thời điểm thực sự thực thi.
func viDu() {
defer fmt.Println("thế giới") // trì hoãn
fmt.Println("xin chào")
}
func main() {
viDu()
}
Mã trên sẽ xuất ra:
xin chào
thế giới
thế giới
được in trước khi hàm viDu
kết thúc, mặc dù nó xuất hiện trước xin chào
trong mã nguồn.
2.2 Thứ tự thực thi của nhiều câu lệnh defer
Khi một hàm có nhiều câu lệnh defer
, chúng sẽ được thực thi theo thứ tự vào sau ra trước. Điều này thường rất quan trọng để hiểu logic dọn dẹp phức tạp. Ví dụ sau minh họa thứ tự thực thi của nhiều câu lệnh defer
:
func nhieuDefer() {
defer fmt.Println("Defer đầu tiên")
defer fmt.Println("Defer thứ hai")
defer fmt.Println("Defer thứ ba")
fmt.Println("Nội dung hàm")
}
func main() {
nhieuDefer()
}
Kết quả của đoạn mã này sẽ là:
Nội dung hàm
Defer thứ ba
Defer thứ hai
Defer đầu tiên
Vì defer
tuân theo nguyên tắc vào sau ra trước, dù "Defer đầu tiên" là defer đầu tiên được khai báo, nó sẽ được thực thi sau cùng.
3 Các ứng dụng của defer
trong các kịch bản khác nhau
3.1 Giải phóng tài nguyên
Trong ngôn ngữ Go, câu lệnh defer
thường được sử dụng để xử lý logic giải phóng tài nguyên, chẳng hạn như thao tác tệp và kết nối cơ sở dữ liệu. defer
đảm bảo rằng sau khi thực thi hàm, các tài nguyên tương ứng sẽ được giải phóng đúng cách bất kể lý do nào khi rời khỏi hàm.
Ví dụ về thao tác tệp:
func DocTep(filename string) {
file, err := os.Open(filename)
if err != nil {
log.Fatal(err)
}
// Sử dụng defer để đảm bảo tệp được đóng
defer file.Close()
// Thực hiện thao tác đọc tệp...
}
Trong ví dụ này, khi os.Open
mở tệp thành công, câu lệnh defer file.Close()
sau đó đảm bảo rằng tài nguyên tệp sẽ được đóng đúng cách và tài nguyên bảng ghi tệp sẽ được giải phóng khi hàm kết thúc.
Ví dụ về kết nối cơ sở dữ liệu:
func TruyVanCSDL(truyvan string) {
db, err := sql.Open("mysql", "nguoidung:matkhau@/tendb")
if err != nil {
log.Fatal(err)
}
// Đảm bảo kết nối cơ sở dữ liệu được đóng bằng defer
defer db.Close()
// Thực hiện thao tác truy vấn cơ sở dữ liệu...
}
Tương tự, defer db.Close()
đảm bảo rằng kết nối cơ sở dữ liệu sẽ được đóng khi rời khỏi hàm TruyVanCSDL
, bất kể lý do (trả về bình thường hoặc ngoại lệ được ném).
3.2 Thao tác Khóa trong Lập Trình Đồng Thời
Trong lập trình đồng thời, sử dụng defer
để xử lý việc giải phóng khóa mutex là một tốt nghiệp thực hành. Điều này đảm bảo rằng khóa sẽ được giải phóng đúng cách sau khi thực thi mã phần quan trọng, tránh tình trạng kẹt đọng.
Ví dụ về Khóa Mutex:
var mutex sync.Mutex
func updateSharedResource() {
mutex.Lock()
// Sử dụng defer để đảm bảo rằng khóa được giải phóng
defer mutex.Unlock()
// Tiến hành sửa đổi tài nguyên chia sẻ...
}
Bất kể việc sửa đổi tài nguyên chia sẻ có thành công hay có panic xảy ra ở giữa, defer
sẽ đảm bảo rằng Unlock()
sẽ được gọi, cho phép các goroutine khác đang chờ lấy khóa.
Mẹo: Giải thích chi tiết về khóa mutex sẽ được thảo luận trong các chương tiếp theo. Hiểu về các tình huống ứng dụng của defer là đủ ở điểm này.
3 Sai Lầm Thông Thường và Lưu Ý về defer
Khi sử dụng defer
, mặc dù tính đọc và tính bảo trì của mã được cải thiện đáng kể, nhưng cũng có một số sai lầm và lưu ý cần ghi nhớ.
3.1 Tham số hàm defer được đánh giá ngay lập tức
func printValue(v int) {
fmt.Println("Giá trị:", v)
}
func main() {
value := 1
defer printValue(value)
// Sửa đổi giá trị của `value` sẽ không ảnh hưởng đến tham số đã được truyền vào defer
value = 2
}
// Kết quả sẽ là "Giá trị: 1"
Mặc dù có thay đổi giá trị của value
sau câu lệnh defer
, tham số được truyền vào printValue
trong defer
đã được đánh giá và cố định, vì vậy kết quả vẫn là "Giá trị: 1".
3.2 Cẩn thận khi sử dụng defer trong vòng lặp
Sử dụng defer
trong vòng lặp có thể dẫn đến việc tài nguyên không được giải phóng trước khi vòng lặp kết thúc, có thể dẫn đến rò rỉ tài nguyên hoặc cạn kiệt tài nguyên.
3.3 Tránh "giải phóng sau khi sử dụng" trong lập trình đa luồng
Trong các chương trình đa luồng, khi sử dụng defer
để giải phóng tài nguyên, quan trọng là đảm bảo rằng tất cả các goroutine sẽ không cố gắng truy cập tài nguyên sau khi nó đã được giải phóng, để ngăn chặn tình trạng cạnh tranh.
4. Chú ý đến thứ tự thực thi của các câu lệnh defer
Các câu lệnh defer
tuân theo nguyên lý Last-In-First-Out (LIFO), trong đó defer
được khai báo cuối cùng sẽ được thực thi trước.
Giải pháp và Thực Prát Tốt Nhất:
- Luôn nhớ rằng các tham số hàm trong các câu lệnh
defer
được đánh giá tại thời điểm khai báo. - Khi sử dụng
defer
trong vòng lặp, xem xét việc sử dụng hàm ẩn danh hoặc gọi giải phóng tài nguyên một cách rõ ràng. - Trong môi trường đa luồng, đảm bảo rằng tất cả các goroutine đã hoàn thành thao tác của họ trước khi sử dụng
defer
để giải phóng tài nguyên. - Khi viết các hàm chứa nhiều câu lệnh
defer
, cân nhắc kỹ thứ tự thực thi và logic của chúng.
Tuân theo những bài tập thực tế này có thể tránh được hầu hết các vấn đề gặp phải khi sử dụng defer
và dẫn đến việc viết mã Go mạnh mẽ và dễ bảo trì hơn.