👁

Goでデータベースを操作しよう

この章では、GoからMySQLへ接続してのCRUD操作をハンズオン形式で学習します。これにより、database/sqlパッケージによる基本操作からSQLインジェクション対策まで、Goでデータベースを扱う基本ができるようになります。

1. 本章の概要

1.1 本章の目的

Goからデータベースを操作するときは、標準ライブラリの database/sql パッケージと、扱いたいデータベース専用のドライバを組み合わせて使います。database/sql が「接続する」「SQLを実行する」「結果を取り出す」といった基本操作の共通APIを提供し、データベースごとの通信部分(プロトコルや認証の細かい違いなど)はドライバが担当する、という分担になっています。このおかげで、MySQL・PostgreSQL・SQLiteのように扱うデータベースが変わっても、Go側のコードはほぼ同じ書き方のまま切り替えられます。

本章ではMySQLを対象に、MySQL用のドライバ go-sql-driver/mysql を導入し、シンプルなユーザ管理テーブルに対するCRUD操作を体験します。

1.2 ハンズオンの流れ

MySQLに接続し、新しく作成した users テーブルに対してINSERT / SELECT / UPDATE / DELETEのCRUDを一通り体験します。プロジェクトの初期化からドライバの導入、接続確認、テーブル作成を経て、プリペアドステートメントを使った安全なSQL実行までを段階的に組み立てていきます。

1.3 事前準備

前提となる講座

この章では、以下の知識を前提としています。自信がない場合は先に関連講座を実施してみましょう。

講座名 必要な知識
データベースとは データベースの基本概念、テーブルやレコードの理解
SQL基本文法 SELECT・INSERT・UPDATE・DELETEなど、SQLの基本操作

必要なツール

この章では、以下のツールを使用します。まだインストールしていない場合は、リンク先の手順に沿って準備をお願いします。

ツール名 関連箇所 理由
Visual Studio Code Visual Studio Codeのインストール Goのコードを記述するエディタとして使用する
Go Goのインストール データベース操作を行うコードのランタイムとして使用する
MySQL MySQLのインストール Goから接続するデータベースとして使用する

2. 準備と接続

まずは新しいプロジェクトフォルダを用意し、database/sql とMySQLドライバを扱える状態を整えます。MySQL側の起動確認をしたうえで、Goから接続し、対象のデータベースとテーブルまで作成します。

2.1 プロジェクトの準備

Goでデータベースを扱うにあたって、まずは新しいプロジェクトフォルダを用意し、database/sql とMySQLドライバを扱える状態を整えます。

フォルダの作成

任意の場所に godb フォルダを作成し、Visual Studio Codeの「ファイル」→「フォルダーを開く」から、作成した godb フォルダを開きます。続けて、「ターミナル」→「新しいターミナル」からVisual Studio Codeのターミナルを開きます。以降の操作は、このターミナルから行います。

godb/  ← このフォルダを作成

Go のインストール確認

以下のコマンドで、Go がインストールされていることを確認します。

go version

以下のように Go のバージョンが表示されれば、インストールは確認できています。

go version go1.x.x darwin/arm64

バージョンが表示されない場合は、Goのインストール を先に実施してください。

Go モジュールの初期化

作成した godb フォルダをGoモジュールとして初期化するために、go mod init を実行します。この初期化により、以降のパッケージ導入や依存関係の管理の起点となる go.mod ファイルが作成されます。

go mod init godb

以下のような実行結果が表示されます。

go: creating new go.mod: module godb

godb フォルダに go.mod が新しく作られました。中身を確認すると、モジュール名とGoのバージョンだけが書かれたシンプルな内容になっています。

module godb

go 1.x

パッケージの導入

続いて、本章で使うパッケージを導入します。まず下記のコマンドを実行します。

go get github.com/go-sql-driver/mysql

以下のような実行結果が表示されます。依存関係のあるパッケージも一緒にダウンロード・追加されます。

go: downloading github.com/go-sql-driver/mysql v1.x.x
go: downloading filippo.io/edwards25519 v1.x.x
go: added filippo.io/edwards25519 v1.x.x
go: added github.com/go-sql-driver/mysql v1.x.x

続いて下記のコマンドを実行します。.env ファイルの書式と godotenv の基本的な使い方は「Go標準ライブラリ」の .env ファイルからの環境変数の読み込みに記載があります。

go get github.com/joho/godotenv

以下のような実行結果が表示されます。

go: downloading github.com/joho/godotenv v1.x.x
go: added github.com/joho/godotenv v1.x.x

MySQL ドライバと godotenv がバージョン付きで go.mod に追加されたことが分かります。go.mod の中身を確認すると、require ディレクティブに導入したパッケージと、その依存関係のあるパッケージが並んでいます。

module godb

go 1.x

require (
    filippo.io/edwards25519 v1.x.x // indirect
    github.com/go-sql-driver/mysql v1.x.x // indirect
    github.com/joho/godotenv v1.x.x // indirect
)

この時点ではまだ main.go を書いていないため、go get で導入したパッケージもコード上で import していない状態です。そのため、すべてのパッケージに // indirect(間接的な依存関係)というマーカーが付いています。// indirect は「自分のコードから直接は import していないが、依存先のパッケージが利用しているため間接的に必要」という意味のマーカーで、次のステップで main.goimport を書くと、直接使うパッケージからは外れます。

2.2 MySQL の起動確認

本章では、GoからMySQLへの接続を試す前に、MySQL自体が起動していて、想定するユーザでログインできることをCLIから確認します。

MySQL のインストール確認

まずは、MySQLがインストールされていることをバージョン表示で確認します。

mysql --version

以下のようにMySQLのバージョンが表示されれば、インストールは確認できています。

mysql  Ver 8.x.x for macos on arm64 (Homebrew)

バージョンが表示されない場合は、MySQLのインストール を先に実施してください。

MySQL へのログイン確認

次に、root ユーザでMySQLにログインできることを確認します。

mysql -u root -p

パスワードを求められるので、MySQLインストール時に設定したパスワードを入力します。以下のようにMySQLのプロンプトに切り替われば、ログインは成功しています。

mysql>

ログインできない場合は、MySQLサーバが起動しているか、パスワードが正しいかを確認してください。

以降のハンズオンでは、この root ユーザとパスワードを、Goのコードから .env ファイル経由で読み込む形で接続します。ログイン確認ができたら、exit; でMySQLのプロンプトを抜けます。

exit;

2.3 MySQL への接続

プロジェクトの準備でドライバの導入まで済みましたが、この時点ではまだGoからMySQLに接続できるかは確認できていません。まずは接続情報をまとめた .env ファイルを用意し、そのうえで最小限のコードで接続を試して、MySQLに到達できることを確かめます。

.env ファイルの作成

MySQL への接続情報を .env ファイルにまとめ、Go のコードからは godotenv 経由で環境変数として読み込みます。

Visual Studio Codeのエクスプローラーで godb フォルダを右クリックし、「新しいファイル」を選択して .env という名前のファイルを作成します。

godb/
├── .env  ← このファイルを作成
├── go.mod
└── go.sum

作成したファイルに以下の内容を記述して保存します。各項目の意味は下記のとおりです。

変数名 説明
MYSQL_USER 接続先MySQLのユーザ名を指定する。ここでは root を使う
MYSQL_PASSWORD 上記ユーザのパスワードを指定する。先ほど mysql -u root -p で入力した自分のMySQLパスワードに書き換える
MYSQL_HOST 接続先MySQLのホスト名またはIPアドレスを指定する。ローカルで動作しているMySQLに接続するため 127.0.0.1 を使う
MYSQL_PORT 接続先MySQLの待ち受けポートを指定する。MySQLのデフォルトである 3306 を使う
MYSQL_DATABASE 接続先のデータベース名を指定する。まだ作成していないが、後続のステップで使うためこの時点で書いておく
MYSQL_USER=root
MYSQL_PASSWORD=あなたのパスワード
MYSQL_HOST=127.0.0.1
MYSQL_PORT=3306
MYSQL_DATABASE=godb_test

main.go の作成

Visual Studio Codeのエクスプローラーで godb フォルダを右クリックし、「新しいファイル」を選択して main.go という名前で作成してください。

godb/
├── .env
├── go.mod
├── go.sum
└── main.go  ← このファイルを作成

書き方は以下のとおりです。sql.Open に DSN を渡すと接続オブジェクト(*sql.DB)が返りますが、この時点ではまだ実際の接続は行われません。続けて db.Ping() を呼ぶことで、DSN が指す MySQL に実際に到達できるかを確認します。

db, err := sql.Open("mysql", DSN)   // DSN から接続オブジェクトを作る(この時点では未接続)
defer db.Close()

if err := db.Ping(); err != nil {   // 実際に接続を試す
    // 接続エラーの処理
}

作成したファイルに以下の内容を記述して保存します。これは、.env から接続情報を読み込み、MySQLに接続を試み、成功したら 接続成功 と表示するだけの最小コードです。

package main

import (
    "database/sql"
    "fmt"
    "os"

    _ "github.com/go-sql-driver/mysql"
    "github.com/joho/godotenv"
)

func main() {
    if err := godotenv.Load(); err != nil {
        fmt.Println(".envファイル読み込み失敗:", err)
        return
    }

    dsn := fmt.Sprintf(
        "%s:%s@tcp(%s:%s)/?parseTime=true",
        os.Getenv("MYSQL_USER"),
        os.Getenv("MYSQL_PASSWORD"),
        os.Getenv("MYSQL_HOST"),
        os.Getenv("MYSQL_PORT"),
    )
    db, err := sql.Open("mysql", dsn)
    if err != nil {
        fmt.Println("Open失敗:", err)
        return
    }
    defer db.Close()

    if err := db.Ping(); err != nil {
        fmt.Println("Ping失敗:", err)
        return
    }
    fmt.Println("接続成功")
}

コードを解説します。

_ "github.com/go-sql-driver/mysql"

import のアンダースコアは副作用のためだけの import を意味します。ドライバの init() 関数が呼ばれ、database/sql に MySQL ドライバが登録されます。パッケージ内の関数を直接呼ばないので、_ で受けます。

if err := godotenv.Load(); err != nil {
    ...
}

godotenv.Load() で、カレントディレクトリの .env を読み込んで環境変数として登録し、以降の os.Getenv("MYSQL_USER") で各値を取り出せるようにしています。詳しい仕組みは「Go標準ライブラリ」の .env ファイルからの環境変数の読み込みに記載があります。

dsn := fmt.Sprintf(
    "%s:%s@tcp(%s:%s)/?parseTime=true",
    os.Getenv("MYSQL_USER"),
    os.Getenv("MYSQL_PASSWORD"),
    os.Getenv("MYSQL_HOST"),
    os.Getenv("MYSQL_PORT"),
)

DSN(Data Source Name)は接続文字列です。書式は ユーザ名:パスワード@tcp(ホスト:ポート)/データベース名?パラメータ で、fmt.Sprintf.env から読み込んだ値を組み立てています。まだデータベースを作っていないので、データベース名は空にしてあります。parseTime=true は MySQL の DATETIME を Go の time.Time として読み込む指定です。

db, err := sql.Open("mysql", dsn)

sql.Open はドライバ名とDSNを受け取り、*sql.DB を返します。この時点ではまだ実際の接続は行われません。

if err := db.Ping(); err != nil {
    ...
}

db.Ping() が実際に接続を試みる関数です。ここでエラーが返れば、接続情報が誤っている・MySQL が起動していない・認証に失敗した、などが考えられます。

実行して接続を確認します。

go run main.go

以下のような実行結果が表示されます。

接続成功

「接続成功」と表示されれば、GoからMySQLに接続できています。エラーが表示される場合は、以下のトラブルシューティングを確認してください。

⚠️ 「Ping失敗: Error 1045 (28000): Access denied for user 'root'@...」というエラーが出る場合
.envMYSQL_PASSWORD が実際のMySQLパスワードと一致していない可能性が高いです。.env を、先ほど mysql -u root -p でログインできたパスワードに書き換えてから、再度 go run main.go を実行してください。
⚠️ 「Ping失敗: dial tcp 127.0.0.1:3306: connect: connection refused」というエラーが出る場合
MySQLサーバが起動していない、または3306以外のポートで動作している可能性があります。まず mysql -u root -p でCLIからログインできるかを確認し、ログインできない場合はMySQLサーバを起動してください。ポートを変更している場合は、.envMYSQL_HOSTMYSQL_PORT を実際の値に書き換えます。
⚠️ 「no required module provides package github.com/joho/godotenv」というエラーが出る場合
godotenv パッケージが go.mod に登録されていない状態で go run main.go を実行するとこのエラーが出ます。プロジェクトの準備で紹介した go get github.com/joho/godotenv を実行してから、もう一度 go run main.go を試してください。github.com/go-sql-driver/mysql について同じメッセージが出た場合も、同じ手順で go get github.com/go-sql-driver/mysql を実行します。

2.4 データベースとテーブルの作成

MySQLへ接続できることは確認できましたが、CRUD操作を行う受け皿となるデータベースとテーブルはまだ存在しません。ここでは、これから使う godb_test データベースと users テーブルを、GoからSQLを発行して作成します。

godb_test データベースと users テーブルの作成

Go からは database/sqlExec メソッドを使って SQL を実行できます。CREATE DATABASECREATE TABLE などのSQL構文自体については SQL基本文法 に記載があります。

書き方は以下のとおりです。db.Exec は結果セットを返さないSQL(DDL / INSERT / UPDATE / DELETE)を実行するメソッドで、戻り値は sql.Resulterror です。ここではDDLで戻り値の中身は使わないため、_ で捨てます。

_, err := db.Exec("SQL文字列")   // 結果セットを返さないSQLを実行する

main.go を以下のように書き換えて、データベースとテーブルを作成します。これは、godb_test データベースを作り、その中に users テーブルを作成するコードです。

package main

import (
    "database/sql"
    "fmt"
    "os"

    _ "github.com/go-sql-driver/mysql"
    "github.com/joho/godotenv"
)

func main() {
    if err := godotenv.Load(); err != nil {
        fmt.Println(".envファイル読み込み失敗:", err)
        return
    }

    dsn := fmt.Sprintf(
        "%s:%s@tcp(%s:%s)/?parseTime=true",
        os.Getenv("MYSQL_USER"),
        os.Getenv("MYSQL_PASSWORD"),
        os.Getenv("MYSQL_HOST"),
        os.Getenv("MYSQL_PORT"),
    )
    db, err := sql.Open("mysql", dsn)
    if err != nil {
        fmt.Println("Open失敗:", err)
        return
    }
    defer db.Close()

    if _, err := db.Exec("CREATE DATABASE IF NOT EXISTS godb_test"); err != nil {
        fmt.Println("DB作成失敗:", err)
        return
    }
    if _, err := db.Exec("USE godb_test"); err != nil {
        fmt.Println("USE失敗:", err)
        return
    }

    createTable := `
    CREATE TABLE IF NOT EXISTS users (
        id INT AUTO_INCREMENT PRIMARY KEY,
        name VARCHAR(50) NOT NULL,
        email VARCHAR(100) NOT NULL UNIQUE,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP
    )`
    if _, err := db.Exec(createTable); err != nil {
        fmt.Println("テーブル作成失敗:", err)
        return
    }

    fmt.Println("DBとテーブルを準備しました")
}

コードを解説します。

if _, err := db.Exec("CREATE DATABASE IF NOT EXISTS godb_test"); err != nil {

db.Exec は結果セットを返さないSQL(INSERT / UPDATE / DELETE / DDL)に使うメソッドです。戻り値は sql.Resulterror です。ここでは Result は不要なので _ で捨てています。CREATE DATABASE IF NOT EXISTS で、まだ存在しない場合のみデータベースを作ります。

createTable := `
CREATE TABLE IF NOT EXISTS users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(50) NOT NULL,
    email VARCHAR(100) NOT NULL UNIQUE,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`

複数行にわたる SQL はバッククォート ` で囲むと改行込みで書けます。id を主キーの自動採番、email を一意制約、created_at を作成時刻の自動設定として定義しています。

実行して、想定通りにセットアップされることを確認します。

go run main.go

以下のような実行結果が表示されます。

DBとテーブルを準備しました

期待通り、DB とテーブルの作成が完了しました。以降のコードでは、DSN のデータベース名の部分に .envMYSQL_DATABASEgodb_test)を差し込む形にします。

3. CRUD 操作

users テーブルに対して、レコードの追加(INSERT)、複数行の取得(SELECT / Query)、1行の取得(QueryRow)、更新・削除(UPDATE / DELETE)までを順に扱います。

3.1 ユーザの追加(INSERT)

本章では、空の users テーブルにレコードを追加するINSERTから始めます。SQLに外部から受け取った値を埋め込む場面では、SQLインジェクションを避けるためにプレースホルダを使うのが基本になります。

プリペアドステートメント

SQLに値を埋め込むときは、プレースホルダ ? を使ってプリペアドステートメントで実行するのが原則です。文字列連結でSQLを組み立てるとSQLインジェクションの原因になります。

書き方は以下のとおりです。? の位置に、第2引数以降で渡した値がドライバ側で安全にエスケープされて埋め込まれます。Exec の戻り値である sql.Result からは、LastInsertId()AUTO_INCREMENT により採番されたIDを取り出せます。

result, err := db.Exec(
    "INSERT INTO テーブル名 (カラム1, カラム2) VALUES (?, ?)",
    値1, 値2,
)
id, _ := result.LastInsertId()   // 自動採番されたIDを取り出す

main.go を以下のように書き換えます。これは、users テーブルに1件ユーザを追加し、自動採番されたIDを表示するコードです。今回はDSNのデータベース名部分に .envMYSQL_DATABASE を差し込んでいます。

package main

import (
    "database/sql"
    "fmt"
    "os"

    _ "github.com/go-sql-driver/mysql"
    "github.com/joho/godotenv"
)

func main() {
    if err := godotenv.Load(); err != nil {
        fmt.Println(".envファイル読み込み失敗:", err)
        return
    }

    dsn := fmt.Sprintf(
        "%s:%s@tcp(%s:%s)/%s?parseTime=true",
        os.Getenv("MYSQL_USER"),
        os.Getenv("MYSQL_PASSWORD"),
        os.Getenv("MYSQL_HOST"),
        os.Getenv("MYSQL_PORT"),
        os.Getenv("MYSQL_DATABASE"),
    )
    db, err := sql.Open("mysql", dsn)
    if err != nil {
        fmt.Println("Open失敗:", err)
        return
    }
    defer db.Close()

    result, err := db.Exec(
        "INSERT INTO users (name, email) VALUES (?, ?)",
        "Taro", "taro@example.com",
    )
    if err != nil {
        fmt.Println("INSERT失敗:", err)
        return
    }

    id, _ := result.LastInsertId()
    fmt.Println("追加したユーザのID:", id)
}

コードを解説します。

result, err := db.Exec(
    "INSERT INTO users (name, email) VALUES (?, ?)",
    "Taro", "taro@example.com",
)

? の部分に、db.Exec の第2引数以降で値を渡します。ドライバが値を安全にエスケープしてくれるため、SQLインジェクションを防げます。

id, _ := result.LastInsertId()
fmt.Println("追加したユーザのID:", id)

result.LastInsertId()AUTO_INCREMENT で自動採番されたIDを返します。ここでは2つ目の戻り値(エラー)は _ で捨てています。

実行してユーザを追加します。

go run main.go

以下のような実行結果が表示されます(IDは実行のたびに変わります)。

追加したユーザのID: 1

AUTO_INCREMENT が自動で 1 を割り振り、そのIDが表示されました。想定通りIDが表示されていれば、INSERTが成功しています。

3.2 ユーザ一覧の取得(SELECT)

INSERTで追加したユーザが実際にテーブルに格納されているかは、SELECTで取り出して確認します。Exec とは異なり、結果セットを扱うためのAPIが用意されているので、その使い方を押さえます。

Query メソッドによる複数行の取得

複数行を返すSELECTには Query を使います。返ってきた行は Rows として受け、Next() でループしながら1行ずつ取り出します。

書き方は2ブロックに分けて見ます。まず db.Query にSQLと引数を渡すと、複数行の結果を保持する *sql.Rows が返ります。取得したあとは必ず defer rows.Close() で解放します。閉じ忘れると接続がプールに戻らず、いずれ接続不足になります。

rows, err := db.Query("SELECT カラム名 FROM テーブル名 WHERE 条件カラム = ?", 条件値)
defer rows.Close()

続けて、for rows.Next() でループしながら各行を rows.Scan で変数に詰めます。Scan にはSELECTで選んだ列と同じ順序でフィールドのポインタを渡します。ループを抜けた後は rows.Err() でイテレーション中に発生したエラーを最終確認します。

for rows.Next() {
    var 変数 モデル型
    if err := rows.Scan(&変数.フィールド1, &変数.フィールド2); err != nil {
        // Scan エラーの処理
    }
}
if err := rows.Err(); err != nil {
    // イテレーション中のエラー処理
}

main.go を以下のように書き換えます。これは、users テーブルの全レコードを取り出し、User struct に詰め替えて一覧表示するコードです。前ステップで登録した1件がそのまま残っている前提です。

package main

import (
    "database/sql"
    "fmt"
    "os"

    _ "github.com/go-sql-driver/mysql"
    "github.com/joho/godotenv"
)

type User struct {
    ID    int
    Name  string
    Email string
}

func main() {
    if err := godotenv.Load(); err != nil {
        fmt.Println(".envファイル読み込み失敗:", err)
        return
    }

    dsn := fmt.Sprintf(
        "%s:%s@tcp(%s:%s)/%s?parseTime=true",
        os.Getenv("MYSQL_USER"),
        os.Getenv("MYSQL_PASSWORD"),
        os.Getenv("MYSQL_HOST"),
        os.Getenv("MYSQL_PORT"),
        os.Getenv("MYSQL_DATABASE"),
    )
    db, err := sql.Open("mysql", dsn)
    if err != nil {
        fmt.Println("Open失敗:", err)
        return
    }
    defer db.Close()

    rows, err := db.Query("SELECT id, name, email FROM users")
    if err != nil {
        fmt.Println("SELECT失敗:", err)
        return
    }
    defer rows.Close()

    var users []User
    for rows.Next() {
        var u User
        if err := rows.Scan(&u.ID, &u.Name, &u.Email); err != nil {
            fmt.Println("Scan失敗:", err)
            return
        }
        users = append(users, u)
    }
    if err := rows.Err(); err != nil {
        fmt.Println("イテレーション中エラー:", err)
        return
    }

    fmt.Println("--- ユーザ一覧 ---")
    for _, u := range users {
        fmt.Printf("[%d] %s <%s>\n", u.ID, u.Name, u.Email)
    }
}

コードを解説します。

rows, err := db.Query("SELECT id, name, email FROM users")
...
defer rows.Close()

db.Query*sql.Rows を返します。必ず defer rows.Close() で解放します。閉じ忘れると接続がプールに戻らず、いずれ接続不足になります。

for rows.Next() {
    var u User
    if err := rows.Scan(&u.ID, &u.Name, &u.Email); err != nil {
        ...
    }
    users = append(users, u)
}

rows.Scanフィールドのポインタを SELECT で選んだ列順に渡すと、値が詰め込まれます。ここでも & を忘れると値が渡らずコンパイルエラーになる点に注意します。

if err := rows.Err(); err != nil {
    ...
}

ループを抜けた後で rows.Err() を確認するのが Go の慣用パターンです。イテレーション中に発生したエラーはここで検出できます。

実行してユーザ一覧が取得できることを確認します。

go run main.go

以下のような実行結果が表示されます。

--- ユーザ一覧 ---
[1] Taro <taro@example.com>

前ステップの INSERT で追加された1件のユーザが [ID] Name <email> の形で表示されました。

3.3 1行のみの取得(QueryRow)

一覧取得では QueryRows の解放を扱いましたが、IDで単一のユーザを引くように結果が1行と分かっているケースでは、より簡潔に書ける専用のメソッドが用意されています。ここではその使い方と、行が見つからなかった場合のエラー判定まで確認します。

QueryRow の使いどころ

「IDでユーザを1人だけ取得したい」のように結果が1行と分かっているときは、QueryRow を使うと Rows の解放を気にせず直接 Scan できます。

書き方は以下のとおりです。QueryRow は返された1行に対して直接 .Scan(...) を呼び出せます。行が見つからなかった場合は、sql.ErrNoRows という決まったエラーが返るので、それ以外のエラーとは分岐して扱うのが定石です。

var 変数 モデル型
err := db.QueryRow(
    "SELECT カラム名 FROM テーブル名 WHERE 条件カラム = ?",
    条件値,
).Scan(&変数.フィールド1, &変数.フィールド2)

if err == sql.ErrNoRows {
    // 行が見つからなかったときの処理
} else if err != nil {
    // その他のエラー処理
}

main.go を以下のように書き換えます。これは、id = 1 のユーザを QueryRow で1件だけ取得し、存在するかどうかで分岐して表示するコードです。前ステップで登録した id = 1 のユーザが残っている前提です。

package main

import (
    "database/sql"
    "fmt"
    "os"

    _ "github.com/go-sql-driver/mysql"
    "github.com/joho/godotenv"
)

type User struct {
    ID    int
    Name  string
    Email string
}

func main() {
    if err := godotenv.Load(); err != nil {
        fmt.Println(".envファイル読み込み失敗:", err)
        return
    }

    dsn := fmt.Sprintf(
        "%s:%s@tcp(%s:%s)/%s?parseTime=true",
        os.Getenv("MYSQL_USER"),
        os.Getenv("MYSQL_PASSWORD"),
        os.Getenv("MYSQL_HOST"),
        os.Getenv("MYSQL_PORT"),
        os.Getenv("MYSQL_DATABASE"),
    )
    db, err := sql.Open("mysql", dsn)
    if err != nil {
        fmt.Println("Open失敗:", err)
        return
    }
    defer db.Close()

    var one User
    err = db.QueryRow(
        "SELECT id, name, email FROM users WHERE id = ?",
        1,
    ).Scan(&one.ID, &one.Name, &one.Email)

    if err != nil {
        if err == sql.ErrNoRows {
            fmt.Println("ID=1 のユーザは見つかりませんでした")
        } else {
            fmt.Println("QueryRow失敗:", err)
        }
        return
    }
    fmt.Printf("取得: [%d] %s <%s>\n", one.ID, one.Name, one.Email)
}

コードを解説します。

err = db.QueryRow(
    "SELECT id, name, email FROM users WHERE id = ?",
    1,
).Scan(&one.ID, &one.Name, &one.Email)

QueryRow は1行分だけ返す SELECT に使います。返された行に対して直接 Scan を呼び、フィールドに詰め込みます。Rows を扱わないので defer rows.Close() が不要になります。

if err == sql.ErrNoRows {
    fmt.Println("ID=1 のユーザは見つかりませんでした")
}

行が見つからない場合、Scansql.ErrNoRows を返します。他のエラーと区別して扱うのが定石です。

実行して1件だけ取得できることを確認します。

go run main.go

以下のような実行結果が表示されます。

取得: [1] Taro <taro@example.com>

取得: [1] Taro <taro@example.com> のように該当ユーザが表示されれば、QueryRow が正しく動作しています。

3.4 ユーザの更新(UPDATE)

INSERT・SELECTで登録と参照ができるようになったので、次はUPDATEで既存レコードを更新する方法を確認します。UPDATEはINSERTと同じ Exec メソッドで扱えますが、実際に何行に反映されたかを確認する RowsAffected を組み合わせるのがポイントです。

RowsAffected による更新結果の確認

UPDATEはINSERTと同じく Exec を使い、RowsAffected で影響行数を確認できます。

書き方は以下のとおりです。Exec の戻り値 sql.Result から RowsAffected() を呼ぶと、実際に更新された行数を取り出せます。WHERE の条件にマッチする行がなかったときは 0 が返るため、想定通りに更新されたかをこの値で判断できます。

result, err := db.Exec(
    "UPDATE テーブル名 SET カラム名 = ? WHERE 条件カラム = ?",
    新しい値, 条件値,
)
affected, _ := result.RowsAffected()   // 実際に更新された行数を取り出す

main.go を以下のように書き換えます。これは、id = 1 のユーザ名を Taro Yamada に更新するコードです。前ステップまでで登録した id = 1 のユーザが残っている前提です。

package main

import (
    "database/sql"
    "fmt"
    "os"

    _ "github.com/go-sql-driver/mysql"
    "github.com/joho/godotenv"
)

func main() {
    if err := godotenv.Load(); err != nil {
        fmt.Println(".envファイル読み込み失敗:", err)
        return
    }

    dsn := fmt.Sprintf(
        "%s:%s@tcp(%s:%s)/%s?parseTime=true",
        os.Getenv("MYSQL_USER"),
        os.Getenv("MYSQL_PASSWORD"),
        os.Getenv("MYSQL_HOST"),
        os.Getenv("MYSQL_PORT"),
        os.Getenv("MYSQL_DATABASE"),
    )
    db, err := sql.Open("mysql", dsn)
    if err != nil {
        fmt.Println("Open失敗:", err)
        return
    }
    defer db.Close()

    upRes, err := db.Exec(
        "UPDATE users SET name = ? WHERE id = ?",
        "Taro Yamada", 1,
    )
    if err != nil {
        fmt.Println("UPDATE失敗:", err)
        return
    }
    affected, _ := upRes.RowsAffected()
    fmt.Println("更新行数:", affected)
}

コードを解説します。

upRes, err := db.Exec(
    "UPDATE users SET name = ? WHERE id = ?",
    "Taro Yamada", 1,
)
...
affected, _ := upRes.RowsAffected()

UPDATEExec で実行します。RowsAffected は「実際に更新された行数」を返します。0 だった場合、条件にマッチする行がなかったことを意味します。

実行して結果を確認します。

go run main.go

以下のような実行結果が表示されます。

更新行数: 1

「更新行数: 1」となっていれば、id = 1 の1件が更新されています。今回はSELECTの実行を省略していますが、先ほどのSELECTのコードをもう一度実行すると、id = 1nameTaro から Taro Yamada に変わっていることが確認できます。

3.5 ユーザの削除(DELETE)

登録・参照・更新まで扱えるようになったので、最後にDELETEでレコードを削除する方法を確認します。DELETEもUPDATEと同じく ExecRowsAffected の組み合わせで扱えます。

RowsAffected による削除結果の確認

DELETEはUPDATEと同じく Exec を使い、RowsAffected で影響行数を確認できます。

書き方は以下のとおりです。WHERE の条件にマッチする行がなかった場合は RowsAffected0 を返すため、想定通りに削除されたかを行数で判断できます。

result, err := db.Exec(
    "DELETE FROM テーブル名 WHERE 条件カラム = ?",
    条件値,
)
deleted, _ := result.RowsAffected()   // 実際に削除された行数を取り出す

main.go を以下のように書き換えます。これは、id = 1 のユーザを削除するコードです。前ステップまでで登録した id = 1 のユーザが残っている前提です。

package main

import (
    "database/sql"
    "fmt"
    "os"

    _ "github.com/go-sql-driver/mysql"
    "github.com/joho/godotenv"
)

func main() {
    if err := godotenv.Load(); err != nil {
        fmt.Println(".envファイル読み込み失敗:", err)
        return
    }

    dsn := fmt.Sprintf(
        "%s:%s@tcp(%s:%s)/%s?parseTime=true",
        os.Getenv("MYSQL_USER"),
        os.Getenv("MYSQL_PASSWORD"),
        os.Getenv("MYSQL_HOST"),
        os.Getenv("MYSQL_PORT"),
        os.Getenv("MYSQL_DATABASE"),
    )
    db, err := sql.Open("mysql", dsn)
    if err != nil {
        fmt.Println("Open失敗:", err)
        return
    }
    defer db.Close()

    delRes, err := db.Exec("DELETE FROM users WHERE id = ?", 1)
    if err != nil {
        fmt.Println("DELETE失敗:", err)
        return
    }
    deleted, _ := delRes.RowsAffected()
    fmt.Println("削除行数:", deleted)
}

コードを解説します。

delRes, err := db.Exec("DELETE FROM users WHERE id = ?", 1)
...
deleted, _ := delRes.RowsAffected()

DELETE もUPDATEと同じパターンで、Exec の戻り値から RowsAffected を呼び出して「実際に削除された行数」を取得します。

実行して結果を確認します。

go run main.go

以下のような実行結果が表示されます。

削除行数: 1

「削除行数: 1」となっていれば、id = 1 の1件が削除されています。今回はSELECTの実行を省略していますが、先ほどのSELECTのコードをもう一度実行すると、id = 1 のレコードが一覧に表示されなくなっていることが確認できます。

4. 不要リソースの削除

ハンズオンで作成したデータベースを削除します。MySQL CLI から以下を実行してください。

DROP DATABASE godb_test;

5. まとめ

この章では、Goからデータベースを操作する流れを体験しました。

  • 標準ライブラリ database/sql にMySQLドライバを組み合わせて、DBを操作できる
  • ドライバは _ "github.com/go-sql-driver/mysql" のように副作用importで登録する
  • 接続情報は .env ファイルにまとめ、godotenv パッケージで読み込むことで、コードと設定を分離できる
  • sql.Open は接続情報を保持するだけで、実際の接続確認は Ping で行う
  • SQLに値を埋め込むときは、プレースホルダ ? によるプリペアドステートメントを使い、SQLインジェクションを防げる
  • 複数行の SELECT は Query を使い、defer rows.Close()rows.Err() を忘れない
  • 1行だけ取得するなら QueryRow を使い、行なしは sql.ErrNoRows で判定する
  • INSERT / UPDATE / DELETE は Exec を使い、RowsAffected で影響行数を確認する

次の章では、GoのORMライブラリである GORM の基本を学びつつ、実際にモデル定義とCRUD操作を体験します。

この教材は役に立ちましたか?

いいねをたくさんいただけると、制作者の励みになり、より多くのセクションが作れるようになります。

感想を一言(任意)

いただいたコメントは次の制作のヒントになります。ぜひお気軽にご投稿ください。

このコメントは他の受講生には公開されません。DevOps Camp運営が、教材改善のために確認します。

0 / 2000