Chúng ta sẽ đi qua error, fmt.Errorf, errors.Is(), errors.As(), và cách tạo Custom Error Handling một cách chuyên sâu, kèm ví dụ thực tế và các best practices.
1. error & fmt.Errorf
error là gì?
Trong Go, error là một interface được định nghĩa như sau:
type error interface {
Error() string
}
Bất kỳ struct nào implement phương thức Error() string đều là một error.
Đây là cách Go xử lý lỗi: trả về error như một giá trị, không dùng exception như Java hay Python.
fmt.Errorf
fmt.Errorf là hàm tiện ích trong package fmt để tạo một error với chuỗi định dạng.
Cú pháp: fmt.Errorf(format string, args...).
Nó trả về một đối tượng error đơn giản, không chứa thông tin ngữ cảnh phức tạp (chỉ là chuỗi).
Ví dụ cơ bản:
package main
import (
"fmt"
)
func divide(a, b int) (int, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero: %d / %d", a, b)
}
return a / b, nil
}
func main() {
result, err := divide(10, 0)
if err != nil {
fmt.Println("Error:", err) // Error: division by zero: 10 / 0
return
}
fmt.Println("Result:", result)
}
Khi nào dùng fmt.Errorf?
Khi lỗi đơn giản, không cần ngữ cảnh bổ sung.
Thường dùng trong các hàm không yêu cầu phân tích lỗi chi tiết.
Giới hạn:
fmt.Errorf chỉ tạo một chuỗi tĩnh, không hỗ trợ so sánh lỗi hoặc trích xuất thông tin chi tiết.
2. errors.Is() & errors.As()
Giới thiệu
Từ Go 1.13, package errors cung cấp hai hàm mạnh mẽ để xử lý lỗi:
errors.Is(): Kiểm tra xem một lỗi có phải là một lỗi cụ thể không (so sánh với target error).
errors.As(): Trích xuất một lỗi thành kiểu cụ thể để lấy thông tin chi tiết.
errors.Is()
So sánh lỗi với một giá trị lỗi cụ thể (thường là biến toàn cục).
Hỗ trợ error wrapping (dùng fmt.Errorf với %w để bọc lỗi).
Ví dụ:
package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("not found")
func fetchData(id int) error {
if id == 0 {
return fmt.Errorf("fetch failed: %w", ErrNotFound)
}
return nil
}
func main() {
err := fetchData(0)
if errors.Is(err, ErrNotFound) {
fmt.Println("Data not found!") // Data not found!
}
fmt.Println("Full error:", err) // Full error: fetch failed: not found
}
errors.As()
Chuyển đổi lỗi thành một kiểu cụ thể để truy cập thông tin chi tiết.
Yêu cầu lỗi được wrap hoặc implement interface phù hợp.
Ví dụ:
package main
import (
"errors"
"fmt"
)
type MyError struct {
Code int
Msg string
}
func (e *MyError) Error() string {
return fmt.Sprintf("error %d: %s", e.Code, e.Msg)
}
func process() error {
return &MyError{Code: 404, Msg: "resource not found"}
}
func main() {
err := process()
var myErr *MyError
if errors.As(err, &myErr) {
fmt.Printf("Code: %d, Msg: %s\n", myErr.Code, myErr.Msg) // Code: 404, Msg: resource not found
}
}
Wrapping và Kết hợp:
Dùng %w trong fmt.Errorf để bọc lỗi, kết hợp Is và As:
err := fmt.Errorf("additional context: %w", &MyError{Code: 500, Msg: "server error"})
if errors.Is(err, &MyError{}) { // Chỉ kiểm tra kiểu
fmt.Println("Is MyError!")
}
var myErr *MyError
if errors.As(err, &myErr) {
fmt.Println("Extracted:", myErr.Code, myErr.Msg)
}
Best Practices:
Dùng errors.Is() khi chỉ cần kiểm tra lỗi cụ thể.
Dùng errors.As() khi cần truy cập dữ liệu từ lỗi tùy chỉnh.
3. Tạo Custom Error Handling
Tại sao cần Custom Error?
Lỗi mặc định (errors.New, fmt.Errorf) không đủ linh hoạt cho các ứng dụng lớn.
Custom error cho phép:
Thêm metadata (mã lỗi, stack trace, context).
Xử lý lỗi có cấu trúc trong hệ thống phức tạp (ví dụ: API, microservices).
Cách triển khai:
Tạo struct cho lỗi:
Implement interface error.
Thêm các trường bổ sung (code, message, cause, v.v.).
Wrap lỗi:
Dùng %w hoặc một cơ chế tùy chỉnh để giữ lỗi gốc.
Xử lý lỗi:
Kết hợp errors.Is() và errors.As().
Ví dụ thực tế:
package main
import (
"errors"
"fmt"
)
// CustomError là struct lỗi tùy chỉnh
type CustomError struct {
Code int
Message string
Cause error // Lỗi gốc (nếu có)
}
// Implement interface error
func (e *CustomError) Error() string {
if e.Cause != nil {
return fmt.Sprintf("error %d: %s (%v)", e.Code, e.Message, e.Cause)
}
return fmt.Sprintf("error %d: %s", e.Code, e.Message)
}
// Unwrap để hỗ trợ errors.Is và errors.As
func (e *CustomError) Unwrap() error {
return e.Cause
}
// Hàm tạo lỗi
func NewCustomError(code int, message string, cause error) error {
return &CustomError{
Code: code,
Message: message,
Cause: cause,
}
}
func fetchResource(id int) error {
if id < 0 {
return NewCustomError(400, "invalid id", nil)
}
if id == 0 {
return NewCustomError(404, "resource not found", fmt.Errorf("db query failed"))
}
return nil
}
func main() {
// Trường hợp lỗi 400
err := fetchResource(-1)
if err != nil {
fmt.Println("Error:", err) // Error: error 400: invalid id
var customErr *CustomError
if errors.As(err, &customErr) {
fmt.Printf("Code: %d, Message: %s\n", customErr.Code, customErr.Message)
}
}
// Trường hợp lỗi 404 với cause
err = fetchResource(0)
if err != nil {
fmt.Println("Error:", err) // Error: error 404: resource not found (db query failed)
var customErr *CustomError
if errors.As(err, &customErr) {
fmt.Printf("Code: %d, Message: %s, Cause: %v\n", customErr.Code, customErr.Message, customErr.Cause)
}
}
}
Phân tích:
CustomError: Có Code, Message, và Cause để lưu trữ thông tin chi tiết.
Unwrap(): Hỗ trợ unwrap lỗi gốc, cho phép errors.Is() và errors.As() hoạt động.
NewCustomError: Hàm tiện ích để tạo lỗi một cách nhất quán.
Ứng dụng thực tế:
API Server:
package main
import (
"errors"
"fmt"
"net/http"
)
type APIError struct {
Status int
Message string
Cause error
}
func (e *APIError) Error() string {
return fmt.Sprintf("status %d: %s", e.Status, e.Message)
}
func (e *APIError) Unwrap() error {
return e.Cause
}
func handler(w http.ResponseWriter, r *http.Request) {
err := processRequest()
if err != nil {
var apiErr *APIError
if errors.As(err, &apiErr) {
http.Error(w, apiErr.Message, apiErr.Status)
return
}
http.Error(w, "internal server error", http.StatusInternalServerError)
}
}
func processRequest() error {
return &APIError{
Status: 400,
Message: "bad request",
Cause: fmt.Errorf("invalid input"),
}
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
Trả về mã HTTP và thông điệp lỗi phù hợp dựa trên APIError.
Best Practices:
Định nghĩa các lỗi toàn cục (như ErrNotFound) cho các trường hợp phổ biến.