Sửa lỗi 'sql: Scan error on column index X, name: converting NULL to X is an invalid operation' trong Go

beginner🔷 Go2026-04-02| Go 1.16+, package database/sql, MySQL / PostgreSQL / SQLite, Linux / macOS / Windows

Error Message

sql: Scan error on column index X, name "column_name": converting NULL to string is an invalid operation
#go#golang#database#sql#null#scan#mysql#postgresql

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ào string hoặc int thông thường.
  • Câu lệnh LEFT JOIN trả về NULL cho 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 ... NULL trê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.NullString
  • sql.NullInt64 / sql.NullInt32 / sql.NullInt16
  • sql.NullFloat64
  • sql.NullBool
  • sql.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ùng COALESCE khi 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.

Related Error Notes