[Golang cơ bản] Từ khóa defer trong Go

Post on: 2023-03-06 23:22:40 | in: Golang
Trong Golang, tính năng Defer cho phép đăng ký một hàm hoặc phương thức để thực thi ngay trước khi một hàm khác trả về.

Tổng quan

Defer, như tên gọi của nó, được sử dụng để trì hoãn các hoạt động dọn dẹp trong một hàm. Các hoạt động này sẽ được thực hiện vào cuối hàm. Các hoạt động dọn dẹp này sẽ được thực hiện trong một hàm khác được gọi bằng defer. Hàm khác này được gọi vào cuối của hàm bao quanh trước khi nó trả về. Dưới đây là cú pháp của hàm defer:

defer {function_or_method_call}

Những điều cần lưu ý về hàm defer

  • Việc thực thi của hàm defer sẽ bị trì hoãn cho đến khi hàm bao quanh nó trả về.
  • Hàm defer sẽ được thực thi nếu hàm bao quanh nó kết thúc một cách bất thường, ví dụ như khi xảy ra panic.

Một ví dụ tốt để hiểu về hàm defer là khi xử lý ghi dữ liệu vào một file. Một file được mở để ghi cũng phải được đóng sau khi hoàn thành việc ghi.   

package main

import (
    "fmt"
    "log"
    "os"
)

func main() {
    err := writeToTempFile("Some text")
    if err != nil {
        log.Fatalf(err.Error())
    }
    fmt.Printf("Write to file succesful")
}

func writeToTempFile(text string) error {
    file, err := os.Open("temp.txt")
    if err != nil {
        return err
    }
    n, err := file.WriteString("Some text")
    if err != nil {
        return err
    }
    fmt.Printf("Number of bytes written: %d", n)
    file.Close()
    return nil
}

Trong chương trình trên, trong hàm writeToTempFile, chúng ta mở một file và sau đó cố gắng ghi một số nội dung vào file. Sau khi đã ghi nội dung vào file, chúng ta đóng file đó. Có thể xảy ra trường hợp trong quá trình ghi, chương trình gặp lỗi và kết thúc mà không đóng file. Hàm defer giúp tránh những vấn đề này. Hàm defer luôn được thực thi trước khi hàm bao quanh nó trả về. Hãy viết lại chương trình trên với hàm defer.

package main

import (
    "fmt"
    "log"
    "os"
)

func main() {
    err := writeToTempFile("Some text")
    if err != nil {
        log.Fatalf(err.Error())
    }
    fmt.Printf("Write to file succesful")
}

func writeToTempFile(text string) error {
    file, err := os.Open("temp.txt")
    if err != nil {
        return err
    }
    defer file.Close()

    n, err := file.WriteString("Some text")
    if err != nil {
        return err
    }
    fmt.Printf("Number of bytes written: %d", n)
    return nil
}

Trong chương trình trên, chúng ta sử dụng hàm defer file.Close() sau khi đã mở file. Điều này sẽ đảm bảo rằng việc đóng file sẽ được thực thi ngay cả khi việc ghi dữ liệu vào file gặp lỗi. Hàm defer đảm bảo rằng file sẽ được đóng bất kể có bao nhiêu câu lệnh return trong hàm.

Hàm tùy chỉnh trong defer

Chúng ta cũng có thể gọi một hàm tùy chỉnh trong defer. Hãy xem một ví dụ về điều đó.

package main
import "fmt"
func main() {
    defer test()
    fmt.Println("Executed in main")
}
func test() {
    fmt.Println("In Defer")
}

Output

Executed in main
In Defer

Trong chương trình trên, có một câu lệnh defer gọi đến hàm tùy chỉnh có tên là test. Như đã thấy từ đầu ra, hàm test được gọi sau khi tất cả mọi thứ trong main được thực thi và trước khi main kết thúc. Đó là lý do tại sao.

Executed in main

được in ra trước

In Defer

Hàm trên cũng cho thấy rằng việc sử dụng defer trong hàm main cũng hoàn toàn ổn

Hàm nội suy trong defer

Cũng có thể sử dụng hàm nội suy với defer. Hãy xem một ví dụ về điều đó.

package main

import "fmt"

func main() {
    defer func() { fmt.Println("In inline defer") }()
    fmt.Println("Executed")
}

Output

Executed
In inline defer

Trong đoạn mã trên, chúng ta có defer với một hàm nội suy.

defer func() { fmt.Println("In inline defer") }()

Điều này được phép trong Go. Chú ý rằng bắt buộc phải thêm () sau hàm, nếu không, trình biên dịch sẽ báo lỗi.

expression in defer must be function call

Như đã thấy từ đầu ra, hàm nội suy được gọi sau khi tất cả mọi thứ trong main được thực thi và trước khi main kết thúc. Đó là lý do tại sao.

Executed in main

được in ra trước

In inline Defer

Làm thế nào để defer hoạt động

Khi trình biên dịch gặp phải một câu lệnh defer trong một hàm, nó sẽ đẩy câu lệnh này vào một danh sách. Danh sách này được hiện thực bằng một cấu trúc dữ liệu ngăn xếp (stack) bên trong. Tất cả các câu lệnh defer gặp phải trong cùng một hàm sẽ được đẩy vào danh sách này. Khi hàm bao quanh trả về, tất cả các hàm trong ngăn xếp sẽ được thực thi từ trên xuống dưới trước khi thực thi có thể bắt đầu trong hàm gọi. Bây giờ điều tương tự sẽ xảy ra trong hàm gọi cũng.

Hãy hiểu rõ điều gì sẽ xảy ra khi chúng ta có nhiều hàm defer trong các hàm khác nhau. Hãy tưởng tượng một cuộc gọi hàm từ hàm main đến hàm f1 và tiếp tục gọi đến hàm f2

main->f1->f2

Dưới đây là chuỗi các sự kiện sẽ xảy ra sau khi f2 kết thúc:

  • Các hàm defer trong f2 sẽ được thực thi nếu có. Điều khiển sẽ trở lại người gọi của nó, đó là hàm f1.
  • Các hàm defer trong f1 sẽ được thực thi nếu có. Điều khiển sẽ trở lại người gọi của nó, đó là hàm main. Lưu ý rằng nếu có nhiều hàm nằm giữa các hàm này, thì quá trình sẽ tiếp tục lên ngăn xếp theo cùng một cách.
  • Sau khi hàm main kết thúc, hàm defer (nếu có) trong main sẽ được thực thi.

Hãy xem một chương trình minh họa cho điều đó.

package main

import "fmt"

func main() {
defer fmt.Println("Defer in main")
fmt.Println("Stat main")
f1()
fmt.Println("Finish main")
}

func f1() {
defer fmt.Println("Defer in f1")
fmt.Println("Start f1")
f2()
fmt.Println("Finish f1")
}

func f2() {
defer fmt.Println("Defer in f2")
fmt.Println("Start f2")
fmt.Println("Finish f2")
}

Output

Stat main
Start f1
Start f2
Finish f2
Defer in f2
Finish f1
Defer in f1
Finish main
Defer in main

Đánh giá các tham số của defer.

Các tham số của defer được đánh giá tại thời điểm câu lệnh defer được đánh giá.

Hãy xem một chương trình minh họa cho điều đó.

package main

import "fmt"

func main() {
sample := "abc"

defer fmt.Printf("In defer sample is: %s\n", sample)
sample = "xyz"
}

Output

In defer sample is: abc

Trong chương trình trên, khi câu lệnh defer được đánh giá, giá trị của biến sample là “abc”. Trong hàm defer, chúng ta in giá trị của biến sample. Sau câu lệnh defer, chúng ta thay đổi giá trị của biến sample thành “xyz”. Tuy nhiên, chương trình đưa ra kết quả là “abc” thay vì “xyz” vì khi đánh giá các tham số của defer, giá trị của biến sample là “abc”.

Nhiều hàm defer trong cùng một hàm.

Trong trường hợp chúng ta có nhiều hàm defer trong cùng một hàm, thì tất cả các hàm defer sẽ được thực thi theo thứ tự LIFO (Last-In-First-Out).

Hãy xem một chương trình minh họa cho điều đó.

package main
import "fmt"
func main() {
    i := 0
    i = 1
    defer fmt.Println(i)
    i = 2
    defer fmt.Println(i)
    i = 3
    defer fmt.Println(i)
}

Output

3
2
1

Trong chương trình trên, chúng ta có ba hàm defer, mỗi hàm in ra giá trị của biến i. Biến i được tăng giá trị trước mỗi hàm defer. Chương trình đầu tiên đưa ra kết quả là 3, có nghĩa là hàm defer thứ ba được thực thi đầu tiên. Sau đó, chương trình đưa ra kết quả là 2, có nghĩa là hàm defer thứ hai được thực thi sau đó, và sau đó đưa ra kết quả là 1, có nghĩa là hàm defer đầu tiên được thực thi cuối cùng. Điều này cho thấy rằng khi có nhiều hàm defer trong cùng một hàm, chúng tuân theo quy tắc "Last in first out". Và đó là lý do tại sao chương trình đưa ra kết quả như vậy.

3
2
1

Hàm defer và Giá trị trả về Được Đặt Tên.

Trong trường hợp giá trị trả về được đặt tên trong hàm, hàm defer có thể đọc và sửa đổi các giá trị trả về được đặt tên đó. Nếu hàm defer sửa đổi giá trị trả về được đặt tên thì giá trị được sửa đổi đó sẽ được trả về.

Hãy xem một chương trình minh họa cho điều đó.

package main
import "fmt"
func main() {
    s := test()
    fmt.Println(s)
}
func test() (size int) {
    defer func() { size = 20 }()
    size = 30
    return
}

Output

20

Trong chương trình trên, chúng ta có giá trị trả về được đặt tên là "size" trong hàm test. Trong hàm defer, chúng ta thay đổi giá trị trả về được đặt tên và đặt giá trị đó thành 20. Sau đó, chúng ta đặt size thành 30. Trong hàm main, chúng ta in giá trị trả về của hàm test và kết quả là 20 thay vì 30 vì hàm defer đã sửa đổi giá trị của biến size trong hàm test.

Hàm defer và Phương thức

Câu lệnh defer cũng áp dụng cho phương thức tương tự như nó áp dụng cho hàm. Trong ví dụ đầu tiên, chúng ta đã thấy phương thức Close được gọi trên một đối tượng tệp. Điều đó cho thấy rằng câu lệnh defer cũng áp dụng cho các phương thức.

Defer và Panic

Hàm defer cũng sẽ được thực thi ngay cả khi chương trình gặp lỗi panic. Khi một panic được ném ra từ một hàm, việc thực thi của hàm đó sẽ dừng lại và bất kỳ hàm defer nào được sử dụng trong hàm đó sẽ được thực thi. Trong thực tế, tất cả các hàm defer của các cuộc gọi hàm trong stack cũng sẽ được thực thi cho đến khi tất cả các hàm đó đã trả về. Lúc đó chương trình sẽ thoát và in ra thông báo panic.

Do đó, nếu một hàm defer được sử dụng, nó sẽ được thực thi và kiểm soát sẽ được trả về cho hàm gọi. Hàm gọi đó sẽ tiếp tục thực thi hàm defer của nó nếu có và chuỗi tiếp tục cho đến khi chương trình thoát.

Hãy xem một ví dụ:

package main
import "fmt"
func main() {
    defer fmt.Println("Defer in main")
    panic("Panic with Defer")
    fmt.Println("After painc in f2")
}

Output

Defer in main
panic: Panic Create


goroutine 1 [running]:
main.main()
        /Users/slohia/go/src/github.com/golang-examples/articles/tutorial/panicRecover/deferWithPanic/main.go:7 +0x95
exit status 2

Trong chương trình trên, chúng ta có một hàm defer và sau đó chúng ta tạo lỗi panic thủ công. Như bạn có thể thấy từ đầu ra rằng hàm defer được thực thi vì dòng dưới đây được in trong đầu ra:

Defer in main

Kết luận

Đó là tất cả về cách sử dụng defer trong ngôn ngữ lập trình Golang. Hy vọng bạn đã thích bài viết này. Vui lòng chia sẻ phản hồi/cải thiện/sửa lỗi trong phần bình luận.

Tag: golang cơ bản go