Go Zero to Hero - Bài 40: Cơ chế xử lý lỗi cơ bản (Error Handling)
Khác với các ngôn ngữ lập trình sử dụng cơ chế ngoại lệ (Exceptions như try...catch...throw), Go tiếp cận việc xử lý lỗi theo một triết lý cực kỳ thực dụng và minh bạch: Errors are Values (Lỗi cũng chỉ là một giá trị bình thường).
1. Triết lý "Errors are Values"
Trong Go, error thực chất là một Interface nội tích hợp sẵn (Built-in Interface). Nhớ lại các bài học trước, Interface chỉ là một bản hợp đồng. Và bản hợp đồng của error cực kỳ đơn giản: Nó chỉ yêu cầu đúng một Method trả về chuỗi mô tả lỗi.
// Định nghĩa nội bộ của Go về kiểu error
type error interface {
Error() string
}
Bởi vì error là một giá trị thông thường, bạn có thể truyền nó vào hàm, trả nó về từ hàm, lưu nó vào biến, hoặc kiểm tra nó bằng lệnh if. Không có sự gián đoạn (interrupt) ngầm nào xảy ra trong CPU cả.
2. Khởi tạo lỗi: errors.New và fmt.Errorf
Làm sao để hệ thống của bạn chủ động báo lỗi khi dữ liệu không hợp lệ? Go cung cấp 2 công cụ cơ bản nhất:
Cách 1: Dùng errors.New()
Đây là cách nhanh nhất để tạo ra một đối tượng lỗi với thông điệp tĩnh.
package main
import (
"errors"
"fmt"
)
// Hàm kiểm tra thẻ trước khi cho qua cổng xoay
func validateCard(cardID string) error {
if cardID == "" {
// Chủ động tạo và trả về một đối tượng lỗi
return errors.New("mã thẻ không được để trống")
}
// Nếu không có lỗi, trả về giá trị Zero Value của interface là nil
return nil
}
Cách 2: Dùng fmt.Errorf()
Khi bạn muốn thông báo lỗi có kèm theo các tham số động (như mã ID, số tiền, ngày tháng), fmt.Errorf là vũ khí tối thượng. Nó hoạt động giống hệt fmt.Printf nhưng thay vì in ra màn hình, nó trả về một biến error.
func deductFare(balance, fare float64) error {
if balance < fare {
// Chèn tham số động vào câu báo lỗi
return fmt.Errorf("số dư (%.0f) không đủ để thanh toán vé (%.0f)", balance, fare)
}
return nil
}
3. Mẫu kinh điển: if err != nil
Đây là đoạn code bạn sẽ gõ nhiều nhất trong cuộc đời làm Gopher. Nó xuất hiện ở khắp mọi nơi: sau khi gọi Database, gọi API, hay thao tác File.
func main() {
// 1. Lấy dữ liệu và hứng lỗi ngay lập tức
err := validateCard("")
// 2. Xử lý lỗi NGAY TẠI CHỖ
if err != nil {
fmt.Println("🚨 Cảnh báo:", err) // Go sẽ tự động gọi hàm .Error() của biến err
return // Dừng luồng thực thi, không cho code chạy tiếp xuống dưới
}
fmt.Println("✅ Thẻ hợp lệ, mở cổng!")
}
Tại sao dân kỹ sư hệ thống lại tôn sùng if err != nil?
Trong try...catch, bạn có thể bọc 10 dòng code vào một khối. Khi lỗi xảy ra, bạn không biết chính xác dòng nào ném ra lỗi nếu không đọc Stack Trace rườm rà. Còn ở Go, việc buộc phải check lỗi ở TỪNG BƯỚC một giúp luồng điều khiển (Control Flow) rõ ràng tuyệt đối. Đọc code Go, bạn thấy ngay: Làm bước A -> Lỗi thì xử lý A. Làm bước B -> Lỗi thì xử lý B.
4. Error Wrapping: Dấu chân truy vết (Go 1.13+)
Trong kiến trúc Microservices, một request thường đi qua nhiều tầng: Controller -> Service -> Repository (Database).
Giả sử tầng Database báo lỗi "Connection timeout". Nếu bạn trả cái lỗi đó thẳng ra Controller, Controller sẽ in log là "Connection timeout", và bạn sẽ mù tịt không biết Timeout ở bảng nào, chức năng nào.
Để giữ nguyên lỗi gốc mà vẫn đắp thêm được "vị trí xảy ra lỗi", Go 1.13 giới thiệu tính năng Error Wrapping (Bọc lỗi) bằng động từ %w trong fmt.Errorf.
package main
import (
"errors"
"fmt"
)
// Tầng Database
func fetchUserFromDB(id int) error {
return errors.New("database connection timeout") // Lỗi gốc
}
// Tầng Service
func processTransaction(userID int) error {
err := fetchUserFromDB(userID)
if err != nil {
// DÙNG %w ĐỂ BỌC LỖI GỐC LẠI
// Ta thêm bối cảnh: "khi đang xử lý giao dịch..."
return fmt.Errorf("lỗi tầng Service khi truy vấn user %d: %w", userID, err)
}
return nil
}
func main() {
err := processTransaction(1024)
if err != nil {
// Kết quả in ra sẽ nối chuỗi từ ngoài vào trong:
// "lỗi tầng Service khi truy vấn user 1024: database connection timeout"
fmt.Println("CRITICAL:", err)
}
}
Việc bọc lỗi bằng %w giúp bạn tạo ra một sợi dây chuyền (Error Chain), hỗ trợ cực mạnh cho việc đọc Log và dò Bug trên hệ thống production.
Tổng kết
Bạn đã nắm được triết lý "nhập gia tùy tục" của xử lý lỗi trong Go:
- Errors are values: Lỗi chỉ là những biến trả về bình thường, implement interface
error. - Chủ động tạo lỗi: Bằng
errors.New()cho chuỗi tĩnh vàfmt.Errorf()cho chuỗi động. - Kỷ luật thép: Luôn kiểm tra
if err != nilngay sau khi gọi hàm. - Truy vết: Dùng
%wđể bọc lỗi qua các tầng kiến trúc.
All rights reserved