Lỗi Gặp Phải
Bạn chạy một câu query, gọi rows.Scan(), và Go ném ra lỗi này:
sql: Scan error on column index 2, name "description": converting NULL to string is an invalid operation
Hoặc các biến thể tương tự:
sql: Scan error on column index 1, name "age": converting NULL to int64 is an invalid operation
sql: Scan error on column index 3, name "active": converting NULL to bool is an invalid operation
Query chạy hoàn toàn bình thường. Không có lỗi cú pháp, không có vấn đề mạng. Crash xảy ra đúng lúc scan, khi một trong các cột trả về chứa giá trị NULL từ SQL.
Nguyên Nhân
Các kiểu nguyên thủy của Go — string, int, bool, float64 — không có khái niệm "không có gì". Chúng luôn phải chứa một giá trị. Khi database/sql cố sao chép giá trị NULL từ database vào một trong các kiểu đó, nó không có chỗ để đặt giá trị và trả về lỗi.
Một số tình huống thường xuyên gây ra lỗi này:
- Cột cho phép NULL trong schema (
VARCHAR NULL,INT NULL) nhưng bạn scan vàostringhoặcintthông thường. - Câu lệnh
LEFT JOINtrả vềNULLcho các hàng không khớp ở bảng bên phải. - Một cột không có giá trị mặc định và một số hàng được chèn mà không chỉ định giá trị cho cột đó.
- Ai đó chạy
ALTER TABLE ... MODIFY COLUMN ... NULLtrên môi trường production nhưng code Go chưa được cập nhật tương ứng.
Cách Tái Hiện Lỗi
type User struct {
ID int
Name string
Description string // nullable trong DB
}
row := db.QueryRow("SELECT id, name, description FROM users WHERE id = ?", 1)
var u User
err := row.Scan(&u.ID, &u.Name, &u.Description) // crash nếu description là NULL
if err != nil {
log.Fatal(err) // sql: Scan error on column index 2, name "description": converting NULL to string is an invalid operation
}
Giải Pháp 1: Dùng Kiểu sql.Null* (Khuyến Nghị)
Package database/sql đã cung cấp sẵn các kiểu wrapper cho nullable, thiết kế chính xác cho trường hợp này. Thay thế bất kỳ kiểu nguyên thủy nào ánh xạ tới cột có thể NULL:
sql.NullStringsql.NullInt64/sql.NullInt32/sql.NullInt16sql.NullFloat64sql.NullBoolsql.NullTime
import "database/sql"
type User struct {
ID int
Name string
Description sql.NullString // có thể chứa NULL
Age sql.NullInt64
}
row := db.QueryRow("SELECT id, name, description, age FROM users WHERE id = ?", 1)
var u User
err := row.Scan(&u.ID, &u.Name, &u.Description, &u.Age)
if err != nil {
log.Fatal(err)
}
// Luôn kiểm tra .Valid trước khi đọc giá trị
if u.Description.Valid {
fmt.Println("Description:", u.Description.String)
} else {
fmt.Println("Description là NULL")
}
Mỗi kiểu sql.Null* có hai trường: .Valid (giá trị có phải non-NULL không?) và giá trị thực (.String, .Int64, v.v.). Không cần thêm dependency hay thư viện bên ngoài.
Giải Pháp 2: Scan Vào Con Trỏ
Không muốn thay đổi struct? Hãy scan vào con trỏ thay thế. Con trỏ nil là đích scan hoàn toàn hợp lệ cho giá trị NULL:
var description *string
var age *int64
err := row.Scan(&id, &name, &description, &age)
if err != nil {
log.Fatal(err)
}
if description != nil {
fmt.Println(*description)
}
Dùng cách này khi bạn muốn sửa nhanh gọn — một hàm, một query — mà không cần tái cấu trúc gì thêm. Với các thay đổi ở quy mô toàn bảng, Giải pháp 1 sẽ dễ mở rộng hơn.
Giải Pháp 3: Dùng COALESCE Trong SQL Để Tránh NULL Từ Nguồn
Đôi khi cách sửa gọn nhất là ở tầng upstream. Nếu NULL và chuỗi rỗng (hoặc số không) có cùng ý nghĩa trong ứng dụng của bạn, hãy viết lại câu query để thay thế giá trị mặc định trước khi dữ liệu đến Go:
SELECT id, name, COALESCE(description, '') AS description, COALESCE(age, 0) AS age
FROM users
WHERE id = ?;
var description string // an toàn — COALESCE đảm bảo giá trị non-NULL
var age int64
err := row.Scan(&id, &name, &description, &age)
Bỏ qua cách này khi sự khác biệt giữa NULL và zero thực sự quan trọng — ví dụ điểm số 0 so với điểm chưa được nhập.
Xử Lý Nhiều Cột Nullable
Bảng có sáu hay tám cột nullable sẽ rất lộn xộn. Một pattern gọn gàng: định nghĩa một struct scan riêng phản ánh kết quả SQL thô, sau đó ánh xạ sang struct domain của bạn.
type userRow struct {
ID int
Name string
Description sql.NullString
Age sql.NullInt64
Score sql.NullFloat64
Active sql.NullBool
CreatedAt sql.NullTime
}
func scanUser(row *sql.Row) (*User, error) {
var r userRow
err := row.Scan(&r.ID, &r.Name, &r.Description, &r.Age, &r.Score, &r.Active, &r.CreatedAt)
if err != nil {
return nil, err
}
u := &User{
ID: r.ID,
Name: r.Name,
}
if r.Description.Valid {
u.Description = r.Description.String
}
if r.Age.Valid {
u.Age = int(r.Age.Int64)
}
return u, nil
}
Struct scan hấp thụ toàn bộ phần xử lý NULL rườm rà. Struct domain User của bạn vẫn sạch sẽ gọn gàng.
Xác Định Cột Nào Có Thể Nullable
Không chắc cột nào có thể trả về NULL? Hỏi thẳng database:
-- MySQL / MariaDB
SELECT COLUMN_NAME, IS_NULLABLE, DATA_TYPE
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = 'your_db' AND TABLE_NAME = 'users';
-- PostgreSQL
SELECT column_name, is_nullable, data_type
FROM information_schema.columns
WHERE table_name = 'users';
Bất kỳ hàng nào hiển thị IS_NULLABLE = YES đều cần một đích scan nullable ở phía Go. Hãy kiểm tra kết quả này trước khi viết code scan cho bảng chưa quen.
Kiểm Tra Sau Khi Sửa
Sau khi áp dụng bản sửa, hãy test với một hàng thực sự có giá trị NULL — không chỉ các hàng có đầy đủ dữ liệu:
-- Chèn hàng test với NULL
INSERT INTO users (id, name, description) VALUES (999, 'Test', NULL);
row := db.QueryRow("SELECT id, name, description FROM users WHERE id = ?", 999)
var id int
var name string
var description sql.NullString
err := row.Scan(&id, &name, &description)
if err != nil {
log.Fatalf("Vẫn còn lỗi: %v", err)
}
fmt.Printf("ID: %d, Name: %s, Description valid: %v\n", id, name, description.Valid)
// Output: ID: 999, Name: Test, Description valid: false
Không có lỗi và Valid: false — đó là xác nhận bản sửa đã thực sự hoạt động.
Bài Học Rút Ra
- Kiểm tra tính nullable của schema trước khi viết scan target. Nếu cột cho phép
NULL, kiểu Go của bạn cũng phải hỗ trợ điều đó. - LEFT JOIN hầu như luôn sinh ra NULL. Mọi cột từ bảng bên phải đều là ứng viên cần dùng kiểu nullable.
- Chọn đúng công cụ cho từng tình huống: dùng kiểu
sql.Null*khi NULL và zero mang ý nghĩa khác nhau; dùng con trỏ để sửa nhanh cục bộ; dùngCOALESCEkhi chúng có thể hoán đổi cho nhau. - Seed dữ liệu test với các giá trị NULL. Một bộ test chỉ chèn các hàng đầy đủ dữ liệu sẽ không bao giờ phát hiện loại bug này — cho đến khi production gặp phải.

