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.go に import を書くと、直接使うパッケージからは外れます。
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'@...」というエラーが出る場合 |
|---|
.env の MYSQL_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サーバを起動してください。ポートを変更している場合は、.env の MYSQL_HOST と MYSQL_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/sql の Exec メソッドを使って SQL を実行できます。CREATE DATABASE や CREATE TABLE などのSQL構文自体については SQL基本文法 に記載があります。
書き方は以下のとおりです。db.Exec は結果セットを返さないSQL(DDL / INSERT / UPDATE / DELETE)を実行するメソッドで、戻り値は sql.Result と error です。ここでは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.Result と error です。ここでは 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 のデータベース名の部分に .env の MYSQL_DATABASE(godb_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のデータベース名部分に .env の MYSQL_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)
一覧取得では Query と Rows の解放を扱いましたが、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 のユーザは見つかりませんでした")
}
行が見つからない場合、Scan は sql.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()
UPDATE も Exec で実行します。RowsAffected は「実際に更新された行数」を返します。0 だった場合、条件にマッチする行がなかったことを意味します。
実行して結果を確認します。
go run main.go
以下のような実行結果が表示されます。
更新行数: 1
「更新行数: 1」となっていれば、id = 1 の1件が更新されています。今回はSELECTの実行を省略していますが、先ほどのSELECTのコードをもう一度実行すると、id = 1 の name が Taro から Taro Yamada に変わっていることが確認できます。
3.5 ユーザの削除(DELETE)
登録・参照・更新まで扱えるようになったので、最後にDELETEでレコードを削除する方法を確認します。DELETEもUPDATEと同じく Exec と RowsAffected の組み合わせで扱えます。
RowsAffected による削除結果の確認
DELETEはUPDATEと同じく Exec を使い、RowsAffected で影響行数を確認できます。
書き方は以下のとおりです。WHERE の条件にマッチする行がなかった場合は RowsAffected が 0 を返すため、想定通りに削除されたかを行数で判断できます。
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操作を体験します。