42日でバックエンドエンジニアの基礎を完全に理解する #4 - GoからDBに接続する
この記事で学ぶこと
前回まではデータをメモリ上で扱っていました。サーバーを再起動するとデータは消えてしまいます。この記事では、Goからデータベース(PostgreSQL)に接続し、データを永続化する方法を学びます。
この記事を終えると以下ができるようになります。
DockerでPostgreSQLを起動できる
GoからDBに接続できる
SQLでテーブルを作り、データの保存(INSERT)と取得(SELECT)ができる
データがDBに永続化される仕組みを理解できる
なぜDBが必要なのか
前回まで作っていたAPIは、データをGoのメモリ上(変数やスライス)で持っていました。これには大きな問題があります。
サーバーを再起動するとデータが全部消える。
メモリはプログラムが動いている間だけ存在します。Ctrl + C でサーバーを止めたら、それまでに作ったデータは消えてしまいます。
そこでデータベース(DB)を使います。DBはデータをディスクに保存するので、サーバーを再起動してもデータは残ります。これを 永続化 といいます。
PostgreSQLをDockerで起動する
DBを使うには、まずDBソフトウェアを起動する必要があります。今回はPostgreSQL(ポスグレ)という代表的なRDB(リレーショナルデータベース)を使います。
Dockerについて
Dockerは「アプリを箱(コンテナ)に閉じ込めて動かす技術」です。詳しくは後の回で扱いますが、今は「docker run というコマンド一発で、PostgreSQLを自分のPCにインストールせずに起動できる便利な道具」と思ってください。不要になったら箱ごと捨てられるので、PCを汚しません。
自分のPCに直接インストールしてもいいですが、Dockerを使うと一行で起動できて、不要になったら綺麗に削除できるので便利です。
bashdocker run --name pg-practice
-e POSTGRES_PASSWORD=password
-e POSTGRES_DB=testdb
-p 5432:5432
-d postgres:16
各オプションの意味:
オプション意味--name pg-practiceコンテナに名前をつける-e POSTGRES_PASSWORD=passwordDBのパスワードを設定(環境変数)-e POSTGRES_DB=testdb初期データベース名を設定-p 5432:5432ポート5432を公開(PostgreSQLのデフォルト)-dバックグラウンドで起動postgres:16使うイメージ(PostgreSQLのバージョン16)
起動確認:
bashdocker ps
pg-practice が Up 状態で表示されればOK
GoのDBライブラリをインストール
GoからPostgreSQLに接続するためのドライバをインストールします。
bashcd ~/go-practice/day3-db
go get github.com/lib/pq
go get は外部ライブラリをダウンロードして go.mod に依存関係を記録するコマンドです。
【手を動かす①】GoからDBに接続してCRUD APIを作る
以下のコードを main.go に書いて実行します。コードは長いですが、ブロックごとに後で解説します。
ポートについての注意
このコードでは8080番ポートを使っています。8080は開発でよく使われるポートなので、他のアプリ(PythonのFastAPIなど)が既に使っていることがあります。その場合は起動時に競合します。自分の環境で8080が使われている場合は、コード末尾の :8080 を :8081 などに変えてください。どのポートが使われているかは lsof -i :8080 で確認できます。
gopackage main
import (
"database/sql"
"encoding/json"
"fmt"
"log"
"net/http"
_ "github.com/lib/pq"
)
var db *sql.DB
type User struct {
ID int json:"id"
Name string json:"name"
Age int json:"age"
}
func initDB() {
var err error
db, err = sql.Open("postgres", "host=localhost port=5432 user=postgres password=password dbname=testdb sslmode=disable")
if err != nil {
log.Fatal(err)
}
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
age INTEGER NOT NULL
)
`)
if err != nil {
log.Fatal(err)
}
fmt.Println("Database connected and table created")
}
func createUserHandler(w http.ResponseWriter, r *http.Request) {
var user User
err := json.NewDecoder(r.Body).Decode(&user)
if err != nil {
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
err = db.QueryRow(
"INSERT INTO users (name, age) VALUES ($1, $2) RETURNING id",
user.Name, user.Age,
).Scan(&user.ID)
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
}
func getUsersHandler(w http.ResponseWriter, r *http.Request) {
rows, err := db.Query("SELECT id, name, age FROM users")
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer rows.Close()
var users []User
for rows.Next() {
var u User
err := rows.Scan(&u.ID, &u.Name, &u.Age)
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
users = append(users, u)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(users)
}
func main() {
initDB()
http.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
getUsersHandler(w, r)
} else if r.Method == http.MethodPost {
createUserHandler(w, r)
} else {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
})
fmt.Println("Server started on :8080")
http.ListenAndServe(":8080", nil)
}
実行:
bashgo run main.go
→ Database connected and table created
→ Server started on :8080
別のターミナルで:
bash# データを作成
curl -X POST -H "Content-Type: application/json" -d '{"name":"washi","age":30}' http://localhost:8080/users
→ {"id":1,"name":"washi","age":30}
さらに追加
curl -X POST -H "Content-Type: application/json" -d '{"name":"taro","age":25}' http://localhost:8080/users
→ {"id":2,"name":"taro","age":25}
全件取得
curl http://localhost:8080/users
→ [{"id":1,"name":"washi","age":30},{"id":2,"name":"taro","age":25}]
IDが 1, 2, 3... と自動で増えていき、GETで全件が配列で返ってきます。
コードの解説
① import の _ "github.com/lib/pq"
まず import のおさらいです。import は「この道具箱(パッケージ)を使いますよ」という宣言でした。今回は3つの新しい道具箱が増えています。
goimport (
"database/sql" // DB操作の道具箱(標準ライブラリ)
"encoding/json" // JSON変換の道具箱(前回も使った)
"fmt" // 画面表示の道具箱(前回も使った)
"log" // ログ出力・エラー終了の道具箱(今回新登場)
"net/http" // HTTPサーバーの道具箱(前回も使った)
_ "github.com/lib/pq" // PostgreSQL専用ドライバ(今回新登場)
)
新しいのは database/sql、log、github.com/lib/pq の3つです。
database/sql → Goの標準ライブラリ。DB操作の共通の窓口を提供する
lib/pq → PostgreSQL専用のドライバ(DBと実際に通信する部品)
ここで _(アンダースコア)に注目してください。これは 「このパッケージを直接は使わないが、読み込んでおく」 という意味の特別な書き方です。
database/sql は「DBと話すための共通の窓口」ですが、実際にPostgreSQLと通信するには専用の部品(ドライバ)が必要です。lib/pq がその部品で、読み込まれた瞬間に「自分はPostgreSQL担当ですよ」と database/sql に裏で登録します。
コードの中で pq.なんとか() と直接呼ぶことはないので、_ をつけてインポートだけします。Goは「インポートしてるのに使ってないパッケージ」があるとエラーにしますが、_ をつけると「使わないけど必要だから読み込む」と明示できてエラーになりません。
② グローバル変数 db
govar db *sql.DB
これは db という変数を 関数の外 で宣言しています。
前回、変数には2つの宣言方法があると学びました。var name string = "..."(型を明示)と name := "..."(型を自動推測)です。ただし := は関数の中でしか使えません。今回は関数の外で宣言しているので var を使っています。
なぜ関数の外で宣言するのか。それは複数の関数(後で出てくる createUserHandler と getUsersHandler)から同じDB接続を使いたいからです。
前回スコープ(変数の生存範囲)を学びました。関数の中で作った変数はその関数の中でしか存在しません。複数の関数から使いたい変数は、関数の外に置くことでどこからでもアクセスできるようになります。db がまさにそれです。
sql.DB はDB接続を表す型です。 がついているのは「ポインタ型」だからですが、今は深く考えず「DB接続を入れる箱の型」と思ってください。
③ User構造体(前回の復習)
gotype User struct {
ID int json:"id"
Name string json:"name"
Age int json:"age"
}
前回学んだ構造体です。構造体は「違う型のデータをまとめるプロフィールカード」でした。ここでは ID(整数)、Name(文字列)、Age(整数)という異なる型をまとめています。
json:"id" の部分はJSONタグです。これも前回やりました。Goのフィールド名は大文字始まり(ID、Name)ですが、JSONに変換するときは小文字(id、name)にしたいので、タグで指定しています。タグを囲んでいるのはバッククォート ` で、シングルクォート ' とは別物です。
前回と違うのは ID フィールドが増えたことです。これはDBが自動採番するID(識別番号)を入れるためです。
④ DB接続(sql.Open)
godb, err = sql.Open("postgres", "host=localhost port=5432 user=postgres password=password dbname=testdb sslmode=disable")
この1行を分解します。まず左側から。
db, err = → 左辺に2つの変数。Goの関数は複数の値を返せるので、sql.Open の結果を db(DB接続)と err(エラー)の2つで受け取っている。db はさっき関数の外で宣言した変数、err はこの直前に var err error で用意した変数。すでに宣言済みの変数に代入するので := ではなく = を使っている点に注意
sql.Open(...) → database/sql 道具箱の Open 関数を呼び出している。DBへの接続を準備する
sql.Open には2つの引数を渡します。
第1引数 "postgres" → ドライバ名。「PostgreSQLに接続するよ」という指定。importした lib/pq が「postgres」という名前で自分を登録しているので、この名前で呼び出せる。
第2引数(接続情報の文字列) → どのDBに、どんな情報で繋ぐかを1つの文字列で書く。スペース区切りで複数の項目を並べる。
パラメータ意味host=localhostDBが動いているマシン(今は自分のPC)port=5432PostgreSQLのポート番号user=postgresDBのユーザー名password=passwordパスワード(Docker起動時に設定した値)dbname=testdb接続するデータベース名sslmode=disableローカルなのでSSL暗号化は不要
この host・port・user・password・dbname は、すべて docker run でPostgreSQLを起動したときの設定と一致している必要があります。たとえば docker run で -e POSTGRES_PASSWORD=password と設定したから、ここでも password=password と書いています。食い違うと接続に失敗します。
⑤ log.Fatal(致命的エラーで止める)
goif err != nil {
log.Fatal(err)
}
if err != nil は前回学んだエラーチェックです。「エラーが発生していたら」という意味。
log.Fatal(err) は エラーメッセージを出力して、プログラムを即座に終了させる 関数です。log 道具箱の Fatal(致命的)という関数です。
ここで使う理由は、DB接続に失敗したらサーバーを動かしても意味がないからです。DBに繋がらないAPIは何もできません。だから起動時のこういう致命的なエラーは、その場でプログラムごと止めてしまいます。
前回出てきた http.Error との違いを押さえてください。
http.Error → リクエスト1件にエラーを返すだけ。サーバーは動き続ける
log.Fatal → プログラム全体を強制終了する
「リクエスト処理中のエラー」は http.Error、「起動時の致命的なエラー」は log.Fatal、と使い分けます。
⑥ テーブル作成(CREATE TABLE)
go_, err = db.Exec(CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, age INTEGER NOT NULL ))
まずGoの部分から。
_, err = → db.Exec は2つの値(実行結果, エラー)を返す。今回は実行結果は使わないので (アンダースコア)で捨てて、エラーだけ err で受け取る。 は「この戻り値はいらない」という意味
db.Exec(...) → db(DB接続)の Exec(execute=実行)関数。SQLをDBに送って実行する
バッククォート ` で囲まれた複数行 → これがDBに送るSQL文。バッククォートで囲むと改行を含む文字列を書ける。SQLは長くなるのでこう書くと読みやすい
次にSQLの中身。これは「usersという表を作って」という命令です。
SQL意味CREATE TABLE IF NOT EXISTSテーブルを作る。IF NOT EXISTS は「すでにあれば作らずスキップ」。これがないと2回目の起動でエラーになるid SERIAL PRIMARY KEYidという列。SERIALは自動採番(1,2,3...と自動で増える)。PRIMARY KEYは主キー(その行を一意に識別する重複しない値)name TEXT NOT NULLnameという列。TEXTは文字列型。NOT NULLは「空を許さない」age INTEGER NOT NULLageという列。INTEGERは整数型。NOT NULLで空を許さない
db.Exec は 結果の行を必要としないSQL に使います。CREATE TABLE、INSERT、UPDATE、DELETEなどです(データを取ってくるのではなく、何かを実行するだけのSQL)。
⑦ INSERT(データの保存)
goerr = db.QueryRow(
"INSERT INTO users (name, age) VALUES ($1, $2) RETURNING id",
user.Name, user.Age,
).Scan(&user.ID)
少し複雑なので、まずSQL部分を見ます。
sqlINSERT INTO users (name, age) VALUES ($1, $2) RETURNING id
INSERT INTO users (name, age) → usersテーブルの name と age 列に
VALUES ($1, $2) → 値を入れる。$1 と $2 は「あとで埋める穴」(プレースホルダー)
RETURNING id → 挿入した行の id(自動採番された値)を返してもらう
プレースホルダー $1, $2 とは
$1、$2 は値を直接書かずに「あとで埋める穴」として空けておく仕組みです。実際の値は、SQL文の後ろに続けて渡します。
godb.QueryRow(
"INSERT INTO users (name, age) VALUES ($1, $2) RETURNING id",
user.Name, // ← $1 に入る
user.Age, // ← $2 に入る
)
user.Name が $1 に、user.Age が $2 に、順番に入ります。
なぜ直接 SQL に値を埋め込まないのか? SQLインジェクション攻撃を防ぐためです。
たとえば値を直接つなげて書くと、悪意あるユーザーが名前欄に '; DROP TABLE users; -- のような文字列を入力したとき、それがSQLの一部として実行され、テーブルごと削除されてしまう危険があります。
プレースホルダーを使うと、渡された値は「ただのデータ」として扱われ、絶対にSQL命令としては実行されません。だから安全です。
これはセキュリティの基本中の基本です。 値を文字列連結でSQLに埋め込むのは絶対にやってはいけません。プレースホルダーを必ず使ってください。
db.QueryRow(...).Scan(&user.ID) とは
godb.QueryRow("... RETURNING id", user.Name, user.Age).Scan(&user.ID)
これも .(ドット)で2つの処理がつながっています。
db.QueryRow(...) → SQLを実行して「1行の結果」を受け取る。今回は RETURNING id で「挿入した行のid」が1行返ってくる。QueryRow は「1行だけ取得する」関数
.Scan(&user.ID) → 返ってきた1行のデータを、変数に書き込む。Scan(スキャン=読み取る)で、返ってきた id を user.ID に入れている
&user.ID の & に注目してください。前回学んだ通り、& は「変数の場所(アドレス)を渡す」という意味です。Scan は「渡された場所に直接データを書き込む」ので、& をつけて「ここに書き込んでね」と場所を教えています。
結果として、DBが自動採番した id(たとえば 1)が user.ID に入ります。だからレスポンスに {"id":1,...} と id が含まれるわけです。
⑧ SELECT(複数行の取得)
gorows, err := db.Query("SELECT id, name, age FROM users")
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer rows.Close()
var users []User
for rows.Next() {
var u User
err := rows.Scan(&u.ID, &u.Name, &u.Age)
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
users = append(users, u)
}
1つずつ見ていきます。
rows, err := db.Query("SELECT id, name, age FROM users")
db.Query(...) → SQLを実行して 複数行の結果 を受け取る関数。SELECTで複数件取りたいときに使う
SELECT id, name, age FROM users → usersテーブルから id, name, age を全件取得するSQL
rows → 取得した複数行が入っている。ただしこの時点では「結果への入り口」を受け取っただけで、中身は1行ずつ取り出していく必要がある
err → エラー
defer rows.Close()
defer は 「この関数が終わる直前に実行してね」という予約 です。
rows(取得結果)は使い終わったら閉じる(Close)必要があります。閉じないとDBとの接続リソースが解放されず、無駄に占有し続けてしまいます。
defer をつけておくと、関数がどこで終わっても(途中で return してもエラーで抜けても)、必ず最後に rows.Close() が実行されます。「開けたら閉じる」を確実にする仕組みで、閉じ忘れを防げます。
var users []User
[]User → 「User型のスライス(可変長の配列)」という型。複数のUserをまとめて入れられる
取得した全ユーザーをここに溜めていく。最初は空
for rows.Next()
rows.Next() → 次の行があれば true、なければ false を返す
for と組み合わせることで「行がある限りループする」=「1行ずつ最後まで処理する」という動きになる
DBから取った複数行を、1行ずつ順番に処理していくイメージです。
ループの中身
govar u User // 1行分を入れる空のUserを用意
err := rows.Scan(&u.ID, &u.Name, &u.Age) // 現在の行のデータを u に読み込む
...
users = append(users, u) // u を users スライスに追加
rows.Scan(&u.ID, &u.Name, &u.Age) → 現在の行の id, name, age を、それぞれ u.ID, u.Name, u.Age に書き込む。& をつけているのは「書き込む場所を渡す」ため(INSERTのときと同じ)
append(users, u) → users スライスの末尾に u を追加する。append は「追加する」という組み込み関数
ループが終わると、users には全ユーザーが入っています。これをJSONに変換して返すと、[{...},{...}] という配列のレスポンスになります。
エラー処理パターン(前回の復習)
このコードのあちこちに出てくる以下のパターン、前回学んだGoのエラー処理です。
goresult, err := なんかの処理()
if err != nil {
// エラー処理
return
}
// 正常処理
err != nil は「エラーが発生した(errがnilではない)」という意味です。nil は「何もない」を表す値でした。DB操作は失敗する可能性がある(接続が切れる、SQLが間違っているなど)ので、毎回 if err != nil でチェックして、エラーがあれば500エラーを返して return で処理を止めます。return を忘れると、エラーなのに処理が続いてしまうので注意です。
⑨ main関数とルーティング(前回の復習 + 応用)
gofunc main() {
initDB()
http.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
getUsersHandler(w, r)
} else if r.Method == http.MethodPost {
createUserHandler(w, r)
} else {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
})
fmt.Println("Server started on :8080")
http.ListenAndServe(":8080", nil)
}
前回学んだ通り、http.HandleFunc で「このURLにアクセスが来たらこの処理を実行する」とルーティングを設定します。http.ListenAndServe(":8080", nil) でポート8080でサーバーを起動します(nil はデフォルト設定を使うという意味でした)。
今回新しいのは、/users という1つのURLに対して、HTTPメソッド(GET/POST)で処理を分けている点です。前回も r.Method でメソッドを見て分岐するパターンをやりました。
GET /users → getUsersHandler(一覧取得)
POST /users → createUserHandler(新規作成)
それ以外 → 405エラー
冒頭の initDB() は、サーバー起動前にDB接続とテーブル作成を済ませるための呼び出しです。
db.Exec と db.Query と db.QueryRow の使い分け
メソッド用途戻り値db.Exec結果の行が不要なSQL(CREATE/INSERT/UPDATE/DELETE)実行結果(影響を受けた行数など)db.Query複数行を取得するSELECT複数行(rows)db.QueryRow1行だけ取得するSELECT、またはRETURNING付きINSERT1行
メモリとDBの違い(永続化の確認)
ここまでできたら、サーバーを一度止めて(Ctrl + C)、もう一度起動してみてください。
bashgo run main.go
そして再度GETでデータを取得:
bashcurl http://localhost:8080/users
→ 前に作ったデータがそのまま残っている!
メモリ上のデータならサーバー再起動で消えますが、DBに保存したデータは残ります。これが永続化です。
後始末(コンテナの停止・削除)
学習が終わったら、起動したPostgreSQLコンテナを止めましょう。起動しっぱなしにするとPCのリソースを消費し続けます。
まず動いているコンテナを確認します。
bashdocker ps
pg-practice が表示される
一時的に止める場合
bashdocker stop pg-practice
止めるだけならデータは残ります。次回また使うときは以下で再開できます。
bashdocker start pg-practice
完全に削除する場合
もう使わない、まっさらにしたい場合は削除します。
bashdocker stop pg-practice # まず止める
docker rm pg-practice # コンテナを削除
注意:削除するとコンテナ内のデータも消えます。 次に使うときは最初の docker run からやり直しになります。
学習を区切るときは docker stop で止めるだけにしておき、完全に不要になったら docker rm で削除する、という使い分けがおすすめです。
確認テスト
知識問題
なぜデータをメモリではなくDBに保存するのですか?
「永続化」とは何ですか?
_ "github.com/lib/pq" の _ は何を意味しますか?
db をなぜ関数の外(グローバル)で宣言しているのですか?
sql.Open の接続情報に含まれる host、port、dbname はそれぞれ何ですか?
log.Fatal と http.Error の違いは何ですか?
SERIAL PRIMARY KEY とは何ですか?
SQLのプレースホルダー($1, $2)を使う理由は何ですか?
SQLインジェクション攻撃とは何ですか?
db.Exec、db.Query、db.QueryRow の使い分けを説明してください
defer rows.Close() は何をしていますか?なぜ必要ですか?
for rows.Next() は何をしていますか?
実技問題
PostgreSQLをDockerで起動するコマンドを書いてください(DB名は appdb、パスワードは secret)
以下のテーブルを作成するSQLを書いてください
テーブル名: products
カラム: id(自動採番の主キー)、name(文字列、NULL不可)、price(整数、NULL不可)
products テーブルに新しい商品を1件INSERTし、採番されたIDを取得するGoのコードを書いてください(プレースホルダーを使うこと)
確認テスト 解答・解説
知識問題
- なぜデータをメモリではなくDBに保存するのですか?
メモリ上のデータはプログラムが動いている間しか存在せず、サーバーを再起動すると消えてしまう。DBはディスクに保存するので、再起動してもデータが残る。
- 「永続化」とは何ですか?
データをディスクなどに保存して、プログラムやサーバーを再起動しても残るようにすること。
- _ "github.com/lib/pq" の _ は何を意味しますか?
「直接は使わないが読み込んでおく」という意味。lib/pqは読み込まれるときに自分をPostgreSQLドライバとして登録する。コード中で直接呼ばないので _ をつける。
- db をなぜ関数の外(グローバル)で宣言しているのですか?
複数の関数(createUserHandler、getUsersHandler)から同じDB接続を使いたいから。関数の中で宣言するとその関数内でしか使えない(スコープ)。
- sql.Open の接続情報に含まれる host、port、dbname はそれぞれ何ですか?
host = DBが動いているマシン(localhostは自分のPC)、port = DBが待ち受けるポート番号(PostgreSQLは5432)、dbname = 接続するデータベースの名前。
- log.Fatal と http.Error の違いは何ですか?
log.Fatal はエラーを出力してプログラム全体を終了させる(起動時の致命的エラー用)。http.Error はリクエスト1件に対してエラーレスポンスを返すだけでサーバーは動き続ける。
- SERIAL PRIMARY KEY とは何ですか?
SERIALは自動採番(1,2,3...と自動で増える)。PRIMARY KEYは主キーで、その行を一意に識別するための重複しない値。
- SQLのプレースホルダー($1, $2)を使う理由は何ですか?
SQLインジェクション攻撃を防ぐため。入力値を「ただのデータ」として扱い、SQL文として実行されないようにする。
- SQLインジェクション攻撃とは何ですか?
入力欄に悪意あるSQL文を埋め込んで、データベースを不正に操作する攻撃。たとえば名前欄に '; DROP TABLE users; -- を入れてテーブルを削除するなど。プレースホルダーを使えば防げる。
- db.Exec、db.Query、db.QueryRow の使い分けを説明してください
db.Exec は結果の行が不要なSQL(CREATE/INSERT/UPDATE/DELETE)。db.Query は複数行を取得するSELECT。db.QueryRow は1行だけ取得する場合やRETURNING付きINSERT。
- defer rows.Close() は何をしていますか?なぜ必要ですか?
関数が終わる直前に rows.Close() を実行する予約。DBの接続リソースを解放するため。defer で書くことで閉じ忘れを防げる。
- for rows.Next() は何をしていますか?
取得した複数行を1行ずつ処理するループ。次の行があれば true、なければ false を返す。各行を rows.Scan で変数に読み込む。
実技問題
- PostgreSQLをDockerで起動するコマンド
bashdocker run --name pg-app
-e POSTGRES_PASSWORD=secret
-e POSTGRES_DB=appdb
-p 5432:5432
-d postgres:16
- productsテーブルを作成するSQL
sqlCREATE TABLE IF NOT EXISTS products (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
price INTEGER NOT NULL
)
- productsにINSERTするGoコード
gotype Product struct {
ID int json:"id"
Name string json:"name"
Price int json:"price"
}
var product Product
product.Name = "Coffee"
product.Price = 500
err := db.QueryRow(
"INSERT INTO products (name, price) VALUES ($1, $2) RETURNING id",
product.Name, product.Price,
).Scan(&product.ID)
if err != nil {
log.Fatal(err)
}
プレースホルダー $1, $2 を使い、RETURNING id で採番されたIDを取得して .Scan(&product.ID) で書き込んでいる。
次回: #5 SQL基礎(CRUD)