Xử lý lỗi 'sql: no rows in result set' trong truy vấn cơ sở dữ liệu Go

beginner🔷 Go2026-06-07| Go (Golang) sử dụng package database/sql với PostgreSQL, MySQL, hoặc SQLite

Error Message

sql: no rows in result set
#go#sql#database#golang

Bẫy thiếu hàng dữ liệu (Missing Row Trap)Bạn đang xây dựng một microservice bằng Go và cần lấy thông tin hồ sơ của một người dùng. Bạn viết một lệnh gọi db.QueryRow() gọn gàng, nối chuỗi với .Scan(), và mọi thứ hoạt động hoàn hảo trong môi trường local. Sau đó, một người dùng yêu cầu một ID như 9999 vốn không tồn tại, và log của bạn bùng nổ với các lỗi 500 Internal Server Error. Thủ phạm chính là một sentinel error bị hiểu lầm.

Thông báo cụ thể xuất hiện trong log của bạn là:

sql: no rows in result set

Không giống như Python (nơi bạn có thể nhận được None) hoặc Node.js (trả về undefined), Go coi việc thiếu hàng dữ liệu là một trạng thái lỗi rõ ràng. Đây không phải là bug trong driver cơ sở dữ liệu của bạn. Thay vào đó, đó là một lựa chọn thiết kế có chủ đích buộc bạn phải quyết định chính xác điều gì sẽ xảy ra khi một truy vấn không có kết quả.

Nơi logic bị phá vỡXử lý lỗi tiêu chuẩn thường thất bại ở đây vì nó coi mọi lỗi non-nil là một sự cố thảm khốc. Nhiều nhà phát triển viết mã như thế này:

func GetUserEmail(id int) (string, error) {
    var email string
    // Truy vấn này nhìn qua có vẻ hoàn hảo
    err := db.QueryRow("SELECT email FROM users WHERE id = $1", id).Scan(&email)
    
    if err != nil {
        // Sai lầm: coi lỗi "không tìm thấy" giống hệt như "cơ sở dữ liệu bị sập"
        return "", err
    }
    
    return email, nil
}

Nếu bạn gọi hàm này với một ID không tồn tại, hàm sẽ trả về một lỗi non-nil. Trình xử lý (handler) gọi hàm này thấy một lỗi, giả định rằng cơ sở dữ liệu đã bị sập và trả về lỗi 500 Internal Server Error cho người dùng. Trong thực tế, việc thiếu người dùng lẽ ra nên kích hoạt phản hồi 404 Not Found.

Cách khắc phục chuẩn idiomaticKể từ Go 1.13, cách tiêu chuẩn để xử lý vấn đề này là sử dụng errors.Is() để kiểm tra sentinel error sql.ErrNoRows. Điều này cho phép bạn tách biệt logic nghiệp vụ khỏi các lỗi hạ tầng.

Mẫu xử lý đúng```

import ( "database/sql" "errors" "fmt" )

func GetUserEmail(id int) (string, error) { var email string err := db.QueryRow("SELECT email FROM users WHERE id = $1", id).Scan(&email)

if err != nil {
    if errors.Is(err, sql.ErrNoRows) {
        // Phân biệt giữa bản ghi bị thiếu và sự cố kết nối
        return "", fmt.Errorf("người dùng %d không tồn tại", id)
    }
    // Xử lý các lỗi hết hạn kết nối thực tế hoặc lỗi cú pháp
    return "", fmt.Errorf("lỗi cơ sở dữ liệu: %w", err)
}

return email, nil

}


## Cơ chế hoạt động bên dướiTại sao `QueryRow` lại hoạt động theo cách này? Nó là một wrapper tiện ích. Nó trả về một đối tượng `*Row` ngay lập tức, thậm chí trước khi truy vấn kết thúc thực thi. Vì `Scan()` là nơi kết quả cơ sở dữ liệu thực sự được ánh xạ vào các biến của bạn, nên đây cũng là nơi duy nhất mà package có thể báo cáo rằng tập kết quả bị trống.
Luôn phân biệt giữa hai trạng thái sau:
- **sql.ErrNoRows:** Truy vấn chạy hoàn hảo, nhưng trả về không có kết quả nào.- **Các lỗi khác:** Hết hạn kết nối (timeout), lỗi xác thực, hoặc cú pháp SQL không hợp lệ.## Giải pháp thay thế: Sử dụng db.QueryNếu bạn muốn tránh hoàn toàn các sentinel error, hãy sử dụng `db.Query()`. Khác với người anh em của nó, `Query` trả về một trình lặp (iterator) và không đưa ra lỗi nếu không tìm thấy hàng nào.

rows, err := db.Query("SELECT email FROM users WHERE id = $1", id) if err != nil { return "", err } defer rows.Close()

if rows.Next() { var email string if err := rows.Scan(&email); err != nil { return "", err } return email, nil }

// Không tìm thấy hàng nào, nhưng không có lỗi nào được kích hoạt return "", nil


Mặc dù `Query` linh hoạt hơn, nhưng `QueryRow` vẫn là tiêu chuẩn cho việc tra cứu một mục đơn lẻ vì nó ngắn gọn và tự động quản lý vòng đời của hàng dữ liệu.
## Các bước xác minh- **Kiểm tra các trường hợp biên:** Chạy unit test với ID như `-1` hoặc `0`. Xác minh rằng mã của bạn trả về lỗi tùy chỉnh thay vì lỗi SQL thô.- **Kiểm tra mức độ Log:** Đảm bảo rằng `ErrNoRows` được ghi log ở mức `INFO`, trong khi các lỗi kết nối thực sự được ghi log ở mức `ERROR`.- **Xác minh mã trạng thái (Status Codes):** Nếu đang xây dựng API, hãy xác nhận rằng endpoint hiện trả về trạng thái `404` thay vì lỗi `500` chung chung.## Bài học kinh nghiệm- `sql.ErrNoRows` là một giá trị sentinel, không phải là dấu hiệu của lỗi hệ thống.- Sử dụng `errors.Is()` để giữ cho việc xử lý lỗi luôn sạch sẽ và dễ đọc.- Luôn xử lý lỗi `Scan()` một cách tỉ mỉ để cung cấp phản hồi tốt hơn cho người dùng.

Related Error Notes