Skip to content

Từ khóa defer trong Go

Bài viết này viết về defer trong Golang

Written on 12-12-2021 · 5 min read

Khái niệm

"Defer" trong tiếng Anh có nghĩa là hoãn lại. Tương tự, defer là một từ khóa trong Go, đi kèm với một câu lệnh gọi hàm (function call). Câu lệnh gọi hàm này sẽ được hoãn thực hiện cho đến khi hàm chứa cậu lệnh defer đó thực hiện xong.

1package main 2 3import "fmt" 4 5func main() { 6 defer fmt.Println("world") 7 8 fmt.Println("hello") 9} 10// === Result: === 11// hello 12// world

Các đặc điểm

Có nhiều lần gọi defer lần lượt

1import fmt 2package main 3 4import "fmt" 5 6func main() { 7 defer fmt.Println("My name is Gopher") 8 defer fmt.Println("world") 9 10 fmt.Println("hello") 11} 12// === Result: === 13// hello 14// world 15// My name is Gopher

Ở ví dụ ở phàn Khái niệm, ta sẽ thấy hello được in ra trước world. Khi ta sử dụng từ khóa defer trước một câu lệnh gọi hàm fmt.Println("My name is Gopher"), Go sẽ đưa câu lệnh gọi hàm đó vào một stack tạm hoãn. Tương tự, fmt.Println("world") cũng được đưa vào stack tạm hoãn đó. Sau khi hàm main đã thực thi xong, nó sẽ thực thi các câu lệnh gọi hàm đang chờ trong stack. Các câu lệnh gọi hàm này được thực hiện theo thứ tự, cậu lệnh nào vào stack trước, thực thi sau:

  • Thêm fmt.Println("My name is Gopher") vào stack.
  • Thêm fmt.Println("world") vào stack.
  • Thực thi fmt.Println("hello").
  • Thực thi fmt.Println("world") bị hoãn.
  • Thực thi fmt.Println("My name is Gopher") bị hoãn.

defer với một hàm có đối số

1package main 2 3import "fmt" 4 5func main() { 6 name := "somebody" 7 defer fmt.Println(name) 8 name = "Gopher" 9 defer fmt.Println(name) 10 fmt.Println(name) 11} 12// === Result: === 13// Gopher 14// Gopher 15// somebody

Những câu lệnh fmt.Println() trong đoạn code trên nhận đối số là biến name. Giá trị của name truyền vào những câu lệnh gọi hàm với defer là giá trị của name tại thời điểm những câu lệnh gọi hàm đó bị hoãn:

  • Khai báo và gán name có giá trị "somebody".
  • Thêm fmt.Println("somebody") vào stack.
  • Gán name giá trị "Gopher".
  • Thêm fmt.Println("Gopher") vào stack.
  • Thực thi fmt.Println(name) với name có giá trị "Gopher".
  • Thực thi fmt.Println("Gopher").
  • Thực thi fmt.Println("somebody").

defer có thể đọc và ghi giá trị trả về của hàm chứa câu lệnh defer

1func c() (i int) { 2 defer func() { i++ }() 3 return 1 4} 5// === Result: === 6// 2

Bằng dùng i để trỏ tới giá trị trả về hàm c, defer có thể đọc và ghi giá trị của i, từ đó đọc và ghi giá trị trả về của hàm c.

Công dụng

defer thường được dùng cho các thao tác dọn dẹp (cleanup), ví dụ như dùng để đóng 1 file đã mở trước đó. Nó giúp giảm sai sót khi có quá nhiều đối tượng cần dọn dẹp trong hàm.

Ta viết một function CopyFile làm nhiệm vụ copy file từ srcName qua dstName, ta có thể viết như sau:

1func CopyFile(dstName, srcName string) (written int64, err error) { 2 // Open the file `srcName` 3 src, err := os.Open(srcName) 4 if err != nil { 5 return; 6 } 7 // Create the file `dstName` 8 dst, err := os.Create(dstName) 9 if err != nil { 10 return 11 } 12 // copy file from src to dst 13 written, err = io.Copy(dst, src) 14 dst.Close() 15 src.Close() 16 return 17}

Trong trường hợp os.Create(dstName) thất bại, hàm CopyFile sẽ trả về và kết thúc mà không thực hiện src.Close(). Chúng ta có thể bổ sung chúng trước khi trả về và kết thúc hàm CopyFile.

1// func CopyFile 2// ... 3 // Create the file `dstName` 4 dst, err := os.Create(dstName) 5 if err != nil { 6 src.Close() 7 return 8 } 9// ...

Ta có thể bổ sung câu lệnh src.Close trong tất cả những câu lệnh điều kiện kiểm tra thất bại. Tuy nhiên, khi hàm trở nên phức tạp, có nhiều câu điều kiện kiểm tra thát bại hơn, có nhiều yếu tố khác của cần được dọn dẹp tương tự src.Close, khả năng bị sót là rất cao.

Vậy nên với defer, ngay khi mở file os.Open(src) thành công, ta có thể hẹn trước ngay src.Close().

1func CopyFile(dstName, srcName string) (written int64, err error) { 2 src, err := os.Open(srcName) 3 if err != nil { 4 return 5 } 6 defer src.Close() 7 8 dst, err := os.Create(dstName) 9 if err != nil { 10 return 11 } 12 defer dst.Close() 13 14 return io.Copy(dst, src) 15}

Ngoài ra, việc quản lý các câu lệnh gọi hàm với defer theo stack cũng là một dụng ý hay. Nó giúp ta quản lý thứ tự dọn dẹp các thành phần trong chương trình theo mức độ quan trọng của chúng.

Xét cùng ví dụ ở trên, nhưng ta không sử dụng package os mặc định của Go để thực hiện việc mở và đóng file nữa. Ta sử dụng CustomFileManager

1func CopyFile(dstName, srcName string) (written int64, err error) { 2 manager, err := CustomFileManager.New() 3 if (err != nil) { 4 return 5 } 6 defer manager.Destroy() 7 8 src, err := manager.Open(srcName) 9 if err != nil { 10 return 11 } 12 defer src.Close() 13 14 dst, err := manager.Create(dstName) 15 if err != nil { 16 return 17 } 18 defer dst.Close() 19 20 return io.Copy(dst, src) 21}

Do việc mở và đóng file đang phụ thuộc vào CustomFileManager, câu lệnh manager.Destroy() nên được đặt sau cùng, sau khi đã thực thực src.Close()dst.Close(). Bởi vì tổ chức theo dạng stack, việc thực hiện nó trở nên vô cùng dễ dàng, cái gì khởi tạo trước tiên thì sẽ bị hủy sau cùng.

Tham khảo

A Tour of Go - Defer

Defer, Panic, and Recover