エラーの内容
クエリを実行し、rows.Scan()を呼び出すと、Goから以下のエラーが返されます:
sql: Scan error on column index 2, name "description": converting NULL to string is an invalid operation
または、類似した以下のエラーも発生します:
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
クエリ自体は正常に実行されています。構文エラーもネットワークの問題もありません。クラッシュは純粋にスキャン時に発生し、返された列のいずれかにSQL NULLが含まれている場合に起こります。
根本原因
Goのプリミティブ型 — string、int、bool、float64 — には「値なし」という概念がありません。これらは常に何らかの値を保持します。database/sqlがデータベースからNULLをこれらの型にコピーしようとすると、格納する場所がないためエラーを返します。
このエラーが確実に発生するケースはいくつかあります:
- スキーマ上でNULL許容列(
VARCHAR NULL、INT NULL)であるにもかかわらず、素のstringやintにスキャンしている場合。 LEFT JOINにより、右側テーブルの一致しない行に対してNULLが返される場合。- デフォルト値のない列に、その列を指定せずに行が挿入された場合。
- 本番環境で
ALTER TABLE ... MODIFY COLUMN ... NULLが実行されたが、Goのコードが更新されていない場合。
再現方法
type User struct {
ID int
Name string
Description string // DBではNULL許容
}
row := db.QueryRow("SELECT id, name, description FROM users WHERE id = ?", 1)
var u User
err := row.Scan(&u.ID, &u.Name, &u.Description) // descriptionがNULLの場合クラッシュ
if err != nil {
log.Fatal(err) // sql: Scan error on column index 2, name "description": converting NULL to string is an invalid operation
}
解決策1:sql.Null*型を使用する(推奨)
database/sqlパッケージには、まさにこのような状況のために設計されたNULL許容ラッパーが付属しています。NULL許容列に対応するプリミティブ型を以下と入れ替えます:
sql.NullStringsql.NullInt64/sql.NullInt32/sql.NullInt16sql.NullFloat64sql.NullBoolsql.NullTime
import "database/sql"
type User struct {
ID int
Name string
Description sql.NullString // 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)
}
// 値を読み取る前に必ず.Validを確認する
if u.Description.Valid {
fmt.Println("Description:", u.Description.String)
} else {
fmt.Println("DescriptionはNULLです")
}
各sql.Null*型は2つのフィールドを持ちます:.Valid(非NULLかどうか)と実際の値(.String、.Int64など)。追加の依存関係やサードパーティライブラリは不要です。
解決策2:ポインタにスキャンする
構造体を変更したくない場合は、代わりにポインタにスキャンします。nilポインタは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)
}
他の部分を変更せずに、1つの関数・1つのクエリだけをピンポイントで修正したい場合に有効です。テーブル全体への変更には、解決策1の方がスケールします。
解決策3:SQLでCOALESCEを使用してNULLを根本から回避する
最もクリーンな修正が上流にある場合もあります。アプリケーション内でNULLと空文字列(またはゼロ)が同じ意味を持つ場合、データがGoに渡る前にデフォルト値に置き換えるようクエリを書き直します:
SELECT id, name, COALESCE(description, '') AS description, COALESCE(age, 0) AS age
FROM users
WHERE id = ?;
var description string // 安全 — COALESCEが非NULL値を保証する
var age int64
err := row.Scan(&id, &name, &description, &age)
NULLとゼロの区別が実際に重要な場合(例:スコア0と未入力のスコアの違い)はこの方法を避けてください。
複数のNULL許容列への対処
6〜8列ものNULL許容列があるテーブルはすぐに混乱します。一つのクリーンなパターン:SQL結果をそのまま反映する専用のスキャン構造体を定義し、その後でドメイン構造体にマッピングします。
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
}
スキャン構造体がNULL処理のノイズをすべて吸収します。Userドメイン構造体はクリーンなままを保てます。
NULL許容列の特定方法
どの列がNULLを返す可能性があるか分からない場合は、データベースに直接問い合わせます:
-- 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';
IS_NULLABLE = YESと表示される行は、Go側でNULL許容のスキャンターゲットが必要です。見慣れないテーブルのスキャンコードを書く前に、この出力を確認してください。
動作確認
修正を適用したら、データのある行だけでなく、実際のNULL行に対してもテストします:
-- 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", err)
}
fmt.Printf("ID: %d, Name: %s, Description valid: %v\n", id, name, description.Valid)
// 出力: ID: 999, Name: Test, Description valid: false
エラーなし、かつValid: false — これが修正が実際に機能していることの確認です。
得られた教訓
- **スキャンターゲットを書く前にスキーマのNULL許容性を確認すること。**列が
NULLを許容する場合、GoのコードもNULLを許容する型を使用する必要があります。 - **LEFT JOINはほぼ必ずNULLを生成します。**右側テーブルのすべての列がNULL許容型の候補となります。
- **目的に合ったツールを選ぶこと:**NULLとゼロが異なる意味を持つ場合は
sql.Null*型を使用し、素早いローカル修正にはポインタを使用し、意味が同じ場合はCOALESCEを使用します。 - **テストデータにNULLを含めること。**完全なデータの行しか挿入しないテストスイートでは、このクラスのバグは本番環境で発見されるまで決して検出されません。

