[Golang nâng cao] Tìm hiểu về error trong Golang - P1

Post on: 2023-03-21 22:18:11 | in: Golang
Error được coi là một giá trị, tương tự như các giá trị khác như chuỗi, số, hoặc boolean. error thường được sử dụng để báo hiệu về một lỗi xảy ra trong quá trình thực thi của chương trình

Tổng quan

Trong bài viết này, chúng ta sẽ tìm hiểu về các chủ đề nâng cao liên quan đến error trong Go.

  • Wrapping và un-wrapping errors: Khi một hàm trả về một giá trị error, ta có thể sử dụng wrapping để thêm thông tin bổ sung vào error đó, ví dụ như đặt tên hàm gây ra lỗi. Việc wrapping này cho phép chúng ta theo dõi và gỡ lỗi các lỗi xảy ra trong chương trình một cách dễ dàng hơn.

  • So sánh error: Go cung cấp các phương thức để so sánh hai giá trị error với nhau, bao gồm phương thức Equal và Is. Sử dụng các phương thức này giúp chúng ta xác định được hai giá trị error có giống nhau hay không.

  • Trích xuất kiểu error gốc: Khi sử dụng wrapping, error sẽ được bọc bởi nhiều lớp error khác nhau. Để xác định được kiểu error gốc mà ta cần phải xử lý, ta có thể sử dụng hàm As để trích xuất kiểu error gốc.

  • Hàm As và Is trong gói errors: Gói errors cung cấp các phương thức As và Is để giúp xác định kiểu error một cách dễ dàng hơn.

Vui lòng tham khảo bài viết cơ bản về error trong Go theo đường link dưới đây để biết thêm thông tin:

https://golangbyexample.com/error-in-golang/

Bài viết trên sẽ giải thích về các khái niệm cơ bản về error như:

  • Tổng quan về error
  • Giao diện Error
  • Các cách khác nhau để tạo ra một error.
  • Bỏ qua error.

Đóng gói error

Trong Go, error có thể đóng gói một error khác. 

Đóng gói error có nghĩa là tạo ra một cấu trúc cây error, trong đó một trường hợp error cụ thể bao gồm một error khác và trường hợp error đó có thể được đóng gói trong một error khác. Dưới đây là cú pháp để đóng gói một error.

e := fmt.Errorf("... %w ...", ..., err, ...)

câu lệnh %w được sử dụng để bọc lỗi trong mã nguồn. Hàm fmt.Errorf chỉ nên được gọi với một câu lệnh %w duy nhất. Dưới đây là một ví dụ minh họa.

package main

import (
"fmt"
)

type errorOne struct{}

func (e errorOne) Error() string {
return "Error One happended"
}

func main() {

 e1 := errorOne{}

 e2 := fmt.Errorf("E2: %w", e1)

 e3 := fmt.Errorf("E3: %w", e2)

 fmt.Println(e2)

 fmt.Println(e3)

}

Output

E2: Error One happended
E3: E2: Error One happended

Trong chương trình trên, chúng ta đã tạo ra một struct "errorOne" có một phương thức "Error", do đó nó thực hiện giao diện "error". Sau đó, chúng ta đã tạo một thể hiện của struct "errorOne" có tên là "e1". Sau đó, chúng ta bọc thể hiện "e1" đó vào một lỗi khác "e2" như sau:

e2 := fmt.Errorf("E2: %w", e1)

Sau đó, chúng ta đã bọc "e2" vào "e3" như dưới đây:

e3 := fmt.Errorf("E3: %w", e2)

Do đó, chúng ta đã tạo ra một hệ thống lỗi phân cấp, trong đó "e3" bao bọc "e2" và "e2" bao bọc "e1". Do đó, "e3" cũng bao bọc "e1" một cách gián tiếp. Khi chúng ta in "e2", nó cũng in ra lỗi từ "e1" và đưa ra kết quả đầu ra.

E2: Error One happended

Khi chúng ta in "e3", nó sẽ in ra lỗi từ "e2" cũng như "e1" và đưa ra kết quả đầu ra.

E3: E2: Error One happended

Bây giờ câu hỏi đặt ra là tại sao chúng ta cần bọc lỗi. Để hiểu điều này, hãy xem một ví dụ.

package main

import (
"fmt"
)

type notPositive struct {
num int
}

func (e notPositive) Error() string {
return fmt.Sprintf("checkPositive: Given number %d is not a positive number", e.num)
}

type notEven struct {
num int
}

func (e notEven) Error() string {
return fmt.Sprintf("checkEven: Given number %d is not an even number", e.num)
}

func checkPositive(num int) error {
if num < 0 {
  return notPositive{num: num}
}
return nil
}

func checkEven(num int) error {
if num%2 == 1 {
  return notEven{num: num}
}
return nil
}

func checkPostiveAndEven(num int) error {
if num > 100 {
  return fmt.Errorf("checkPostiveAndEven: Number %d is greater than 100", num)
}

 err := checkPositive(num)
if err != nil {
  return err
}

 err = checkEven(num)
if err != nil {
  return err
}

 return nil
}

func main() {
num := 3
err := checkPostiveAndEven(num)
if err != nil {
  fmt.Println(err)
} else {
  fmt.Println("Givennnumber is positive and even")
}

}

Output

checkEven: Given number 3 is not an even number

Trong chương trình trên, chúng ta có một hàm "checkPostiveAndEven" kiểm tra xem một số có phải là số chẵn và dương không. Hàm này sẽ gọi hàm "checkEven" để kiểm tra xem số đó có phải là số chẵn hay không. Sau đó, nó gọi hàm "checkPositive" để kiểm tra xem số đó có phải là số dương hay không. Nếu một số không phải là số chẵn và dương, thì sẽ xảy ra một lỗi.

Trong chương trình trên, không thể biết được stack trace của lỗi. Chúng ta biết rằng lỗi này đến từ hàm "checkEven" cho đầu ra trên. Nhưng không rõ lỗi này được gọi bởi hàm nào. Đây chính là lý do bọc lỗi trở nên quan trọng. Điều này trở nên hữu ích hơn khi dự án lớn và có rất nhiều hàm gọi lẫn nhau. Hãy viết lại chương trình bằng cách bọc lỗi.

package main

import (
"fmt"
)

type notPositive struct {
num int
}

func (e notPositive) Error() string {
return fmt.Sprintf("checkPositive: Given number %d is not a positive number", e.num)
}

type notEven struct {
num int
}

func (e notEven) Error() string {
return fmt.Sprintf("checkEven: Given number %d is not an even number", e.num)
}

func checkPositive(num int) error {
if num < 0 {
  return notPositive{num: num}
}
return nil
}

func checkEven(num int) error {
if num%2 == 1 {
  return notEven{num: num}
}
return nil
}

func checkPostiveAndEven(num int) error {
if num > 100 {
  return fmt.Errorf("checkPostiveAndEven: Number %d is greater than 100", num)
}

 err := checkPositive(num)
if err != nil {
  return fmt.Errorf("checkPostiveAndEven: %w", err)
}

 err = checkEven(num)
if err != nil {
  return fmt.Errorf("checkPostiveAndEven: %w", err)
}

 return nil
}

func main() {

num := 3
err := checkPostiveAndEven(num)
if err != nil {
  fmt.Println(err)
} else {
  fmt.Println("Given number is positive and even")
}

}

Output

checkPostiveAndEven: checkEven: Given number 3 is not an even number

Chương trình trên giống với chương trình trước đó, chỉ khác ở chỗ trong hàm "checkPostiveAndEven", chúng ta bọc lỗi như sau.

fmt.Errorf("checkPostiveAndEven: %w", err)

Vì vậy, đầu ra trở nên rõ ràng hơn và thông tin lỗi cũng rõ ràng hơn. Đầu ra rõ ràng cho biết trình tự các hàm được gọi.

checkPostiveAndEven: checkEven: Given number 3 is not an even number

Unwrap một error

Trong phần trên, chúng ta đã tìm hiểu về cách bọc lỗi. Cũng có thể giải bỏ lỗi bọc. Chúng ta có thể sử dụng hàm Unwrap trong gói lỗi để giải bỏ lỗi. Dưới đây là cú pháp của hàm.

func Unwrap(err error) error

Nếu lỗi "err" được bọc trong một lỗi khác, thì lỗi được bọc đó sẽ được trả về, nếu không, hàm Unwrap sẽ trả về giá trị nil.

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

import (
    "errors"
    "fmt"
)
type errorOne struct{}
func (e errorOne) Error() string {
    return "Error One happened"
}
func main() {
    e1 := errorOne{}
    e2 := fmt.Errorf("E2: %w", e1)
    e3 := fmt.Errorf("E3: %w", e2)
    fmt.Println(errors.Unwrap(e3))
    fmt.Println(errors.Unwrap(e2))
    fmt.Println(errors.Unwrap(e1))
}

Output

E2: Error One happended
Error One happended

Trong chương trình trên, chúng ta tạo ra một cấu trúc errorOne có một phương thức Error do đó nó triển khai giao diện error. Sau đó, chúng ta tạo một thể hiện của cấu trúc errorOne với tên là e1. Sau đó, chúng ta bọc thể hiện e1 vào một lỗi khác e2 như sau.

e2 := fmt.Errorf("E2: %w", e1)

Sau đó, chúng ta bọc e2 vào trong e3 như sau.

e3 := fmt.Errorf("E3: %w", e2)

Do đó

fmt.Println(errors.Unwrap(e3))

hàm Unwrap sẽ trả về lỗi được bọc là e2 vì e3 bọc e2 và kết quả sẽ là:

E2: Error One happened

Trong khi

fmt.Println(errors.Unwrap(e1))

kết quả sẽ là nil vì e1 không bọc bất kỳ lỗi nào.

{nil}

Kiểm tra xem hai error có bằng nhau

Đầu tiên, điều gì được hiểu bởi tính bằng nhau của lỗi? Như bạn đã biết, lỗi được biểu thị bởi giao diện error trong Go. Trong Go, hai giao diện được coi là bằng nhau nếu:

  • Cả hai tham chiếu đến cùng một loại cơ sở.
  • Giá trị cơ sở bằng nhau (hoặc cả hai đều là nil)

Do đó, hai điểm trên cũng áp dụng để so sánh lỗi. Có hai cách để kiểm tra xem các lỗi đã cho có bằng nhau không.

Sử dụng toán tử bằng nhau (==)

Toán tử == có thể được sử dụng để so sánh hai lỗi trong Golang.

Sử dụng hàm Is của gói errors

https://golang.org/pkg/errors/ . Sử dụng hàm Is ưu tiên hơn so sánh bằng toán tử bằng nhau vì nó kiểm tra tính bằng nhau bằng cách giải phóng lần lượt lỗi đầu tiên và so khớp với lỗi mục tiêu ở mỗi bước giải phóng. Chúng ta sẽ thấy một ví dụ sau đây để hiểu rõ tại sao nó được ưu tiên. Dưới đây là cú pháp của hàm Is.

func Is(err, target error) bool

Hãy xem một ví dụ

package main
import (
    "errors"
    "fmt"
)
type errorOne struct{}
func (e errorOne) Error() string {
    return "Error One happended"
}
func main() {
    var err1 errorOne
    err2 := do()
    if err1 == err2 {
        fmt.Println("Equality Operator: Both errors are equal")
    }
    if errors.Is(err1, err2) {
        fmt.Println("Is function: Both errors are equal")
    }
}
func do() error {
    return errorOne{}
}

Output
Equality Operator: Both errors are equal
Is function: Both errors are equal

Trong chương trình trên, chúng ta tạo ra một struct errorOne mà định nghĩa phương thức Error nên triển khai interface error. Chúng ta tạo biến err1 là một instance của struct errorOne. Chúng ta cũng tạo hàm do() trả về một lỗi thuộc kiểu errorOne và nó được ghi nhận vào biến err2 trong hàm main.

Sau đó, chúng ta so sánh hai lỗi bằng cách sử dụng

  • Sử dụng toán tử bằng (==)
err1 == err2
  • Sử dụng hàm Is của gói errors
errors.Is(err1, err2)

Trong cả hai phương pháp, kết quả đều cho thấy rằng lỗi bằng nhau vì cả err1 và err2 đều:

  • Tham chiếu đến cùng một kiểu cơ bản là errorOne
  • Có giá trị cơ bản giống nhau

Như đã đề cập ở trên, sử dụng hàm Is là phương pháp ưu tiên hơn so sánh bằng toán tử bằng vì nó kiểm tra tính bằng nhau bằng cách giải bọc lỗi đầu tiên theo thứ tự và so khớp với lỗi mục tiêu ở mỗi bước giải bọc. Hãy xem một ví dụ về điều đó.

package main

import (
"errors"
"fmt"
)

type errorOne struct{}

func (e errorOne) Error() string {
return "Error One happended"
}

func main() {
err1 := errorOne{}

 err2 := do()

 if err1 == err2 {
  fmt.Println("Equality Operator: Both errors are equal")
} else {
  fmt.Println("Equality Operator: Both errors are not equal")
}

 if errors.Is(err2, err1) {
  fmt.Println("Is function: Both errors are equal")
}
}

func do() error {
return fmt.Errorf("E2: %w", errorOne{})
}

Output

Equality Operator: Both errors are not equal
Is function: Both errors are equal

Chương trình trên gần như giống với chương trình trước, chỉ khác biệt là trong hàm do(), chúng ta đã bọc lỗi.

return fmt.Errorf("E2: %w", errorOne{})
  • Kết quả của toán tử bằng
Equality Operator: Both errors are not equal
  • Trong khi đó, hàm Is trả về:
Is function: Both errors are equal

Điều này là do giá trị trả về của err2 bao gồm một instance của errorOne mà không bị bắt bởi toán tử bằng nhưng lại bị bắt bởi hàm Is.

Lấy ra lỗi cơ sở được bao bọc bởi một lỗi được đại diện bởi giao diện lỗi

Có hai cách để lấy kiểu cơ bản

Sử dụng .({type}) assert
Nếu assert thành công thì nó sẽ trả về lỗi tương ứng, nếu không sẽ panic. Dưới đây là cú pháp của nó.

err := err.({type})

Cách tốt hơn là sử dụng biến ok để tránh xảy ra lỗi panic trong trường hợp xác nhận không thành công. Dưới đây là cú pháp cho việc sử dụng biến ok. Biến ok sẽ được đặt thành true nếu kiểu cơ bản của lỗi là chính xác.

err, ok := err.({type})

Sử dụng hàm As của gói lỗi (errors package)

https://golang.org/pkg/errors/. Việc sử dụng hàm As ưu tiên hơn so với việc sử dụng .({type}) assert vì nó kiểm tra sự khớp nhau bằng cách unwrap lỗi đầu tiên tuần tự và so sánh nó với lỗi mục tiêu tại mỗi bước unwrap. Dưới đây là cú pháp của hàm As.

func As(err error, target interface{}) bool

Hàm As sẽ tìm kiếm lỗi đầu tiên trong đối số đầu tiên mà có thể khớp với mục tiêu. Sau khi tìm thấy sự khớp, nó sẽ đặt giá trị mục tiêu thành giá trị lỗi đó.

Hãy xem một ví dụ

package main

import (
"errors"
"fmt"
"os"
)

func main() {

 err := openFile("non-existing.txt")

 if e, ok := err.(*os.PathError); ok {
  fmt.Printf("Using Assert: Error e is of type path error. Path: %v\n", e.Path)
} else {
  fmt.Println("Using Assert: Error not of type path error")
}

 var pathError *os.PathError
if errors.As(err, &pathError) {
  fmt.Printf("Using As function: Error e is of type path error. Path: %v\n", pathError.Path)
}
}

func openFile(fileName string) error {
_, err := os.Open("non-existing.txt")
if err != nil {
  return err
}
return nil
}

Output:

Using Assert: Error e is of type path error. Path: non-existing.txt
Using As function: Error e is of type path error. Path: non-existing.txt

Trong chương trình trên, chúng ta có một hàm openFile trong đó chúng ta đang cố gắng mở một loại tệp không tồn tại, do đó nó sẽ gây ra một lỗi. Sau đó, chúng ta đang khẳng định lỗi theo hai cách

  • Sử dụng toán tử assert .({type}). Biến ok sẽ được thiết lập thành true nếu kiểu lỗi được bao bọc bởi *os.PathError, ngược lại nó sẽ được thiết lập thành false.
e,ok := err.(*os.PathError); ok
  • Sử dụng hàm As của gói errors.
errors.As(err, &pathError)

Cả hai phương thức đều khẳng định đúng rằng lỗi là kiểu *os.PathError vì lỗi được trả về bởi hàm openFile là kiểu *os.PathError.

Chúng ta đã đề cập ở trên rằng việc sử dụng hàm As là tốt hơn việc sử dụng phương thức .({type}) khẳng định vì nó kiểm tra sự khớp bằng cách mở gói lỗi đầu tiên một cách tuần tự và so sánh với lỗi mục tiêu ở mỗi bước mở gói. Hãy xem một ví dụ để hiểu điều đó.

import (
"errors"
"fmt"
"os"
)


func main() {
var pathError *os.PathError
err := openFile("non-existing.txt")

 if e, ok := err.(*os.PathError); ok {
  fmt.Printf("Using Assert: Error e is of type path error. Error: %v\n", e)
} else {
  fmt.Println("Using Assert: Error not of type path error")
}

 if errors.As(err, &pathError) {
  fmt.Printf("Using As function: Error e is of type path error. Error: %v\n", pathError)
}
}

func openFile(fileName string) error {
_, err := os.Open("non-existing.txt")
if err != nil {
  return fmt.Errorf("Error opening: %w", err)
}
return nil
}

Output:

Using Assert: Error not of type path error
Using As function: Error e is of type path error. Error: open non-existing.txt: no such file or directory

Chương trình trên gần như giống với chương trình trước, chỉ khác ở chỗ trong hàm openFile chúng ta đang bọc gói lỗi cũng như.

return fmt.Errorf("Error opening: %w", err)
  • Phương thức . assert đầu ra.
Using Assert: Error not of type path error
  • Trong khi hàm As đầu ra.
Using As function: Error e is of type path error. Error: open non-existing.txt: no such file or directory

Điều này là do lỗi được trả về bởi hàm openFile bọc lỗi *os.Patherror mà không bị bắt bởi phương thức dot (‘.’) assert nhưng lại được bắt bởi hàm As.

Tổng kết

Đó là tất cả những nội dung nâng cao về error 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 tiến / sai sót trong phần bình luận.

Tag: go Golang nâng cao