[Golang nâng cao] Panic và Recover trong Golang

Post on: 2023-03-24 23:46:03 | in: Golang
Panic và Recover là hai tính năng của ngôn ngữ Go, được sử dụng để quản lý lỗi trong chương trình.

Tổng quan

Panic trong Go tương tự như ngoại lệ (exception) trong các ngôn ngữ khác. Panic được thiết kế để thoát khỏi chương trình trong các trường hợp bất thường. Panic có thể xảy ra trong chương trình theo hai cách:

  • Lỗi runtime trong chương trình

  • Bằng cách gọi hàm panic một cách tường minh. Điều này có thể được gọi bởi người lập trình khi chương trình không thể tiếp tục và phải thoát ra

Go cung cấp một hàm đặc biệt để tạo ra panic. Dưới đây là cú pháp của hàm:

func panic(v interface{})

Hàm này có thể được gọi một cách tường minh bởi người lập trình để tạo ra panic. Nó nhận một interface rỗng làm đối số. Khi panic xảy ra trong chương trình, nó sẽ xuất ra hai điều:

  • Thông báo lỗi được chuyển đến hàm panic như một đối số

  • Trace của ngăn xếp (stack trace) nơi xảy ra panic

Panic lỗi thời gian chạy (Runtime Error Panic)

Lỗi thời gian chạy trong chương trình có thể xảy ra trong các trường hợp sau đây:

  • Truy cập mảng vượt quá giới hạn
  • Gọi một hàm trên một con trỏ nil (null pointer)
  • Gửi tín hiệu trên một kênh đã đóng
  • Khẳng định kiểu không chính xác

Hãy xem một ví dụ về lỗi thời gian chạy do truy cập mảng vượt quá giới hạn.

package main

import "fmt"

func main() {

 a := []string{"a", "b"}
print(a, 2)
}

func print(a []string, index int) {
fmt.Println(a[index])
}

Output

panic: runtime error: index out of range [2] with length 2

goroutine 1 [running]:
main.checkAndPrint(...)
        main.go:12
main.main()
        /main.go:8 +0x1b
exit status 2

Trong chương trình trên, chúng ta có một slice có độ dài là 2 và chúng ta đang cố gắng truy cập slice tại chỉ số 3 trong hàm print. Truy cập vượt quá giới hạn không được phép và sẽ tạo ra panic như đã thấy từ đầu ra. Lưu ý rằng trong đầu ra có hai điều:

  • Thông báo lỗi
  • Trace của ngăn xếp nơi xảy ra panic

Có rất nhiều trường hợp khác mà lỗi thời gian chạy có thể xảy ra trong một chương trình. Chúng ta không đề cập đến tất cả những trường hợp này, nhưng bạn đã hiểu ý tưởng.

Gọi hàm panic một cách tường minh

Một số trường hợp mà hàm panic có thể được gọi một cách tường minh bởi người lập trình là:

  • Hàm mong đợi một đối số hợp lệ nhưng thay vào đó lại được cung cấp một đối số nil. Trong trường hợp này, chương trình không thể tiếp tục và sẽ gây ra một panic cho đối số nil được truyền vào.
  • Bất kỳ trường hợp nào khác mà chương trình không thể tiếp tục.

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

package main

import "fmt"

func main() {

a := []string{"a", "b"}
checkAndPrint(a, 2)
}

func checkAndPrint(a []string, index int) {
if index > (len(a) - 1) {
  panic("Out of bound access for slice")
}
fmt.Println(a[index])
}

Output

panic: Out of bound access for slice

goroutine 1 [running]:
main.checkAndPrint(0xc000104f58, 0x2, 0x2, 0x2)
      main.go:13 +0xe2
main.main()
        main.go:8 +0x7d
exit status 2

Trong chương trình trên, chúng ta lại có một hàm checkAndPrint nhận một slice và một chỉ số làm đối số. Sau đó, hàm kiểm tra xem chỉ số truyền vào có lớn hơn độ dài của slice trừ đi 1 không. Nếu đúng, thì đó là truy cập vượt quá giới hạn cho slice nên nó sẽ panic. Nếu không thì hàm in ra giá trị tại chỉ số đó. Lưu ý rằng trong đầu ra có hai điều:

  • Thông báo lỗi
  • Trace của ngăn xếp nơi xảy ra panic

Panic với defer

Khi panic được kích hoạt trong một hàm, thì việc thực thi của hàm đó sẽ dừng lại và bất kỳ hàm defer nào 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 ngăn xếp cũng sẽ được thực thi cho đến khi tất cả các hàm đã được trả về. Lúc đó, chương trình sẽ thoát và nó sẽ in ra thông báo panic.

Vì vậy, nếu một hàm defer có sẵn, nó sẽ được thực thi và điều khiển sẽ được trả lại cho hàm gọi, hàm gọi này 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 trước và sau đó chúng ta tạo ra panic bằng cách gọi hàm panic(). Như bạn có thể thấy từ đầu ra, hàm defer được thực thi vì dòng sau được in ra trong đầu ra:

Defer in main

Hãy hiểu xem điều gì xảy ra khi panic xảy ra trong một chương trình. Hãy tưởng tượng một cuộc gọi hàm từ hàm main đến hàm f1 đến hàm f2

main->f1->f2

Giả sử rằng panic xảy ra trong hàm f2, sau đây là các sự kiện sẽ xảy ra

  • Thực thi của hàm f2 sẽ dừng lại. Hàm defer trong hàm f2 sẽ được thực thi nếu có. Điều khiển sẽ trở lại hàm gọi là hàm f1.
  • Hàm f1 sẽ hoạt động giống như nếu panic xảy ra trong hàm đó và sau đó cuộc gọi sẽ trả về cho hàm gọi là main. Lưu ý rằng nếu có nhiều hàm nằm ở giữa thì quá trình sẽ tiếp tục lên theo stack theo cùng một cách
  • Hàm main cũng sẽ hoạt động giống như nếu panic xảy ra trong hàm đó và sau đó, chương trình sẽ bị lỗi
  • Sau khi chương trình bị lỗi, nó sẽ in ra thông điệp panic cùng với stack trace của nó.

Hãy xem một chương trình để hiểu rõ hơn.

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

Output

Defer in f2
Defer in f1
panic: Panic Demo


goroutine 1 [running]:
main.f2()
        main.go:17 +0x95
main.f1()
        main.go:11 +0x96
main.main()
        main.go:6 +0x20
exit status 2

Trong chương trình trên, panic đã xảy ra trong hàm f2 như sau:

panic("Panic Demo")

Đoạn defer trong f2 được gọi sau đó và in ra thông báo sau đây:

Defer in f2

Chú ý rằng ngay khi panic xảy ra trong hàm f2, thì việc thực thi của nó dừng lại, vì vậy dòng mã dưới đây trong hàm f2 không được thực thi:

fmt.Println("After painc in f2")

Điều khiển trả về cho f1 và nó có một hàm defer. Hàm defer này được thực thi và nó in ra thông báo sau.

Defer in f1

Khi đó, control sẽ trở lại hàm main và chương trình sẽ bị dừng lại. Đầu ra sẽ in ra thông báo lỗi và toàn bộ định tuyến ngăn xếp (stack trace) từ hàm main đến f1 đến f2.

Recover trong golang

Go cung cấp một hàm tích hợp sẵn để xử lý recover từ một panic. Dưới đây là cú pháp của hàm này.

func recover() interface{}

Chúng ta đã học ở trên rằng hàm defer là hàm duy nhất được gọi sau khi panic xảy ra. Vì vậy, đặt hàm recover trong hàm defer là hợp lý nhất. Nếu hàm recover không nằm trong hàm defer, thì nó sẽ không ngăn chặn được panic.

Dưới đây là một ví dụ về cách sử dụng hàm recover:

package main

import "fmt"

func main() {

a := []string{"a", "b"}
checkAndPrint(a, 2)
fmt.Println("Exiting normally")
}

func checkAndPrint(a []string, index int) {
defer handleOutOfBounds()
if index > (len(a) - 1) {
  panic("Out of bound access for slice")
}
fmt.Println(a[index])
}

func handleOutOfBounds() {
if r := recover(); r != nil {
  fmt.Println("Recovering from panic:", r)
}
}

Output

Recovering from panic: Out of bound access for slice
Exiting normally

Trong chương trình trên, chúng ta có một hàm checkAndPrint kiểm tra và in phần tử của slice với index được truyền vào đối số. Nếu index vượt quá độ dài của slice thì chương trình sẽ gây ra panic. Chúng ta đã thêm một hàm defer có tên là handleOutIfBounds ở đầu của hàm checkAndPrint. Hàm này chứa lời gọi hàm recover như sau.

if r := recover(); r != nil {
    fmt.Println("Recovering from panic:", r)
}

Hàm recover sẽ bắt được sự cố panic và chúng ta có thể in ra thông điệp từ sự cố đó.

Recovering from panic: Out of bound access for slice

Sau khi hàm recover được gọi, chương trình tiếp tục thực hiện và điều khiển trả về cho hàm gọi của nó, đó là hàm main. Đó là lý do tại sao chúng ta có đầu ra là:

Exiting normally

Hàm recover sẽ trả về giá trị được truyền vào hàm panic. Do đó, việc kiểm tra giá trị trả về của hàm recover là một thực hành tốt. Nếu giá trị trả về là nil thì nghĩa là panic không xảy ra và hàm recover không được gọi với panic. Đó là lý do tại sao chúng ta có mã sau trong hàm defer handleOutofBounds:

if r := recover(); r != nil

Ở đây, nếu rnil thì có nghĩa là không có sự cố panic đã xảy ra. Vì vậy, nếu không có panic xảy ra thì cuộc gọi đến recover sẽ trả về giá trị nil.

Lưu ý rằng nếu hàm defer và hàm recover không được gọi từ hàm gây ra sự cố panic thì trong trường hợp đó cũng có thể phục hồi được từ panic ở hàm được gọi. Trên thực tế, có thể phục hồi từ panic theo sau lên theo chuỗi lệnh gọi.

Hãy xem một ví dụ về điều này

package main

import "fmt"

func main() {
    a := []string{"a", "b"}
    checkAndPrintWithRecover(a, 2)
    fmt.Println("Exiting normally")
}
func checkAndPrintWithRecover(a []string, index int) {
    defer handleOutOfBounds()
    checkAndPrint(a, 2)
}
func checkAndPrint(a []string, index int) {
    if index > (len(a) - 1) {
        panic("Out of bound access for slice")
    }
    fmt.Println(a[index])
}
func handleOutOfBounds() {
    if r := recover(); r != nil {
        fmt.Println("Recovering from panic:", r)
    }
}

Output

Recovering from panic: Out of bound access for slice
Exiting normally

Chương trình trên khá giống với chương trình trước đó với ngoại lệ là chúng ta có một hàm bổ sung là checkAndPrintWithRecover chứa cuộc gọi đến

  • hàm defer với recover là handleOutOfBounds
  • gọi đến hàm checkAndPrint

Chức năng checkAndPrint ném ra panic nhưng không chứa hàm recover mà gọi đến hàm recover nằm trong hàm checkAndPrintWithRecover. Tuy nhiên, chương trình vẫn có thể phục hồi từ panic vì panic cũng có thể được phục hồi trong hàm được gọi và tiếp tục phục hồi trong chuỗi các hàm gọi nữa.

Chúng ta đã đề cập ở trên rằng nếu hàm recover không nằm trong hàm defer thì nó sẽ không dừng được sự cố panic.

Hãy xem một chương trình ví dụ cho điều đó.

package main

import "fmt"

func main() {

 a := []string{"a", "b"}
checkAndPrint(a, 2)
fmt.Println("Exiting normally")
}

func checkAndPrint(a []string, index int) {
handleOutOfBounds()
if index > (len(a) - 1) {
  panic("Out of bound access for slice")
}
fmt.Println(a[index])
}

func handleOutOfBounds() {
if r := recover(); r != nil {
  fmt.Println("Recovering from panic:", r)
}
}

Output

panic: Out of bound access for slice

goroutine 1 [running]:
main.checkAndPrint(0xc000104f58, 0x2, 0x2, 0x2)
        /Users/slohia/go/src/github.com/golang-examples/articles/tutorial/panicRecover/recoverNegativeExample/main.go:15 +0xea
main.main()
        /Users/slohia/go/src/github.com/golang-examples/articles/tutorial/panicRecover/recoverNegativeExample/main.go:8 +0x81
exit status 2

Trong chương trình trên, hàm recover không nằm trong hàm defer. Như bạn có thể thấy từ đầu ra, nó không ngăn chặn được sự cố và do đó bạn nhận được đầu ra như trên.

Panic/Recover và Goroutine

Một điểm quan trọng cần lưu ý về hàm recover là nó chỉ có thể khôi phục lỗi panic xảy ra trong cùng một goroutine. Nếu panic xảy ra ở một goroutine khác và recover nằm ở một goroutine khác thì nó sẽ không ngăn chặn được panic. Hãy xem một chương trình ví dụ cho điều đó.

package main
import "fmt"
func main() {
    a := []string{"a", "b"}
    checkAndPrintWithRecover(a, 2)
    time.Sleep(time.Second)
    fmt.Println("Exiting normally")
}
func checkAndPrintWithRecover(a []string, index int) {
    defer handleOutOfBounds()
    go checkAndPrint(a, 2)
}
func checkAndPrint(a []string, index int) {
    if index > (len(a) - 1) {
        panic("Out of bound access for slice")
    }
    fmt.Println(a[index])
}
func handleOutOfBounds() {
    if r := recover(); r != nil {
        fmt.Println("Recovering from panic:", r)
    }
}

Output

Exiting normally
panic: Out of bound access for slice


goroutine 18 [running]:
main.checkAndPrint(0xc0000a6020, 0x2, 0x2, 0x2)
        /Users/slohia/go/src/github.com/golang-examples/articles/tutorial/panicRecover/goroutine/main.go:19 +0xe2
created by main.checkAndPrintWithRecover
        /Users/slohia/go/src/github.com/golang-examples/articles/tutorial/panicRecover/goroutine/main.go:14 +0x82
exit status 2

Trong chương trình trên, chúng ta có checkAndPrint trong goroutine và nó gây ra panic trong goroutine đó. Hàm recover nằm trong goroutine gọi checkAndPrint. Như bạn có thể thấy từ đầu ra, nó không ngăn chặn được panic và do đó chúng ta thấy đầu ra như trên.

Printing stack trace

Package Debug của Golang cũng cung cấp hàm StackTrace để in ra stack trace của panic trong hàm recover.

package main
import (
    "fmt"
    "runtime/debug"
)
func main() {
    a := []string{"a", "b"}
    checkAndPrint(a, 2)
    fmt.Println("Exiting normally")
}
func checkAndPrint(a []string, index int) {
    defer handleOutOfBounds()
    if index > (len(a) - 1) {
        panic("Out of bound access for slice")
    }
    fmt.Println(a[index])
}
func handleOutOfBounds() {
    if r := recover(); r != nil {
        fmt.Println("Recovering from panic:", r)
        fmt.Println("Stack Trace:")
        debug.PrintStack()
    }
}

Output

Recovering from panic: Out of bound access for slice
Stack Trace:
goroutine 1 [running]:
runtime/debug.Stack(0xd, 0x0, 0x0)
        stack.go:24 +0x9d
runtime/debug.PrintStack()
        stack.go:16 +0x22
main.handleOutOfBounds()
        main.go:27 +0x10f
panic(0x10ab8c0, 0x10e8f60)
        /Users/slohia/Documents/goversion/go1.14.1/src/runtime/panic.go:967 +0x166
main.checkAndPrint(0xc000104f58, 0x2, 0x2, 0x2)
        main.go:18 +0x111
main.main()
        main.go:11 +0x81
Exiting normally

Trong chương trình trên, chúng ta sử dụng hàm StackTrace của gói debug để in ra stack trace của lỗi trong hàm recover. Nó in ra được đúng stack trace như ta thấy ở đầu ra.

Giá trị trả về của hàm khi panic được recovered

Trong trường hợp panic được phục hồi thì giá trị trả về của một hàm gây ra panic sẽ là giá trị mặc định của kiểu trả về của hàm đó.

Hãy xem một chương trình minh họa:

package main
import (
    "fmt"
)
func main() {
    a := []int{5, 6}
    val, err := checkAndGet(a, 2)
    fmt.Printf("Val: %d\n", val)
    fmt.Println("Error: ", err)
}
func checkAndGet(a []int, index int) (int, error) {
    defer handleOutOfBounds()
    if index > (len(a) - 1) {
        panic("Out of bound access for slice")
    }
    return a[index], nil
}
func handleOutOfBounds() {
    if r := recover(); r != nil {
        fmt.Println("Recovering from panic:", r)
    }
}

Output

Recovering from panic: Out of bound access for slice
Val: 0
Error: 

Trong chương trình trên, chúng ta có hàm checkAndGet để lấy giá trị tại một chỉ mục cụ thể trong slice int. Nếu chỉ mục được truyền vào hàm này lớn hơn (độ dài của slice-1), thì nó sẽ gây ra một panic. Cũng có một hàm handleOutOfBounds được sử dụng để khôi phục từ panic. Do đó, chúng ta truyền chỉ mục 2 vào hàm checkAndGet và nó gây ra panic được khôi phục trong hàm handleOutOfBounds. Đó là lý do tại sao chúng ta nhận được đầu ra này trước:

Recovering from panic: Out of bound access for slice

Chú ý trong hàm main rằng chúng ta thu thập giá trị trả về từ hàm checkAndGet như sau:

val, err := checkAndGet(a, 2)

checkAndGet có hai giá trị trả về

  • int
  • error

checkAndGet gây ra panic mà được khôi phục trong hàm handleOutOfBounds nên giá trị trả về của checkAndGet sẽ là giá trị mặc định của các kiểu dữ liệu của nó.

Vì thế

fmt.Printf("Val: %d\n", val)

đầu ra

Val: 0

vì giá trị mặc định của kiểu int là 0

fmt.Println("Error: ", err)

đầu ra

Error: 

vì giá trị mặc định của kiểu errornil

Nếu bạn không muốn trả về giá trị mặc định bằng không cho kiểu dữ liệu thì bạn có thể sử dụng giá trị trả về có tên. Hãy xem một chương trình ví dụ về điều đó.

package main
import (
    "fmt"
)
func main() {
    a := []int{5, 6}
    val, err := checkAndGet(a, 2)
    fmt.Printf("Val: %d\n", val)
    fmt.Println("Error: ", err)
}
func checkAndGet(a []int, index int) (value int, err error) {
    value = 10
    defer handleOutOfBounds()
    if index > (len(a) - 1) {
        panic("Out of bound access for slice")
    }
    value = a[index]
    return value, nil
}
func handleOutOfBounds() {
    if r := recover(); r != nil {
        fmt.Println("Recovering from panic:", r)
    }
}

Output

Recovering from panic: Out of bound access for slice
Val: 10
Error:

Trong chương trình này, tương tự như chương trình trước đó, chỉ khác là chúng ta sử dụng giá trị trả về có tên trong hàm checkAndGet.

func checkAndGet(a []int, index int) (value int, err error)

Chúng ta đã đặt giá trị trả về tên là named return value bằng 10.

value = 10

Đó là lý do tại sao chúng ta nhận được đầu ra như sau trong chương trình này vì panic được tạo ra và nó đã được phục hồi.

Recovering from panic: Out of bound access for slice
Val: 10
Error: 

Nếu không có panic xảy ra trong chương trình, thì chương trình sẽ trả về giá trị chính xác tại vị trí chỉ mục.

Kết luận

Đó là tất cả về panic và recover trong 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/lỗi trong phần bình luận.

 
 
 
 
Tag: go Golang nâng cao