2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Go】標準ライブラリで学ぶDB接続とCRUD操作の実装パターン

2
Last updated at Posted at 2025-12-24

はじめに

「学生ひよこ界隈が送るGo/Javaで実現する「はじめてのバックエンド」Advent Calendar 2025」5日目の記事は、GoバックエンドとDBの接続方法についてです。

この記事では、まずバックエンド開発の核となる「リレーショナルデータベース(RDB)」と「CRUD」の基本概念を解説します。

その上で、Goの標準ライブラリであるdatabase/sqlを使い、アプリケーションからデータベース(今回はPostgreSQLを例にします)に接続し、基本的なCRUD(作成、読み取り、更新、削除)操作を行う方法をハンズオン形式で紹介します。

  • データベースを扱うのに慣れていない方
  • Goでデータベースを初めて扱う方
  • database/sqlパッケージの基本的な使い方を知りたい方

この記事で、DB接続の全体像を理解して、Goバックエンドで基本的なCRUD操作ができるように、頑張っていきましょう!

RDBとCRUD操作とは?

実際のGoのコードを見る前に、すべてのバックエンド開発の基礎となる「リレーショナルデータベース(RDB)」と「CRUD」の概念をざっくりおさらいします。

RDBとは?

リレーショナルデータベース(RDB)を理解する一番簡単なものの例えは、Excelのファイルです。

  • データベース全体が、一つのExcelファイル (.xlsx)
  • ファイルの中の各テーブルが、シート(例:「記事シート」「ユーザーシート」)
  • シートの中のカラム(列)が、データの項目(例:id, title, content
  • シートの中のレコード(行)が、個々のデータ(例:1行が一つの記事データ)
id (カラム) title (カラム) content (カラム)
1 (レコード) 私の最初の投稿 こんにちは、世界!
2 (レコード) 2番目の投稿 これは2番目の記事です。

このように、RDBはExcelのように構造化された表(テーブル)形式でデータを永続的に管理します。

SQLとは?

RDBを操作するための言語が、SQLです。
SQLの構文はたくさんあるので詳しくは触れませんが、代表的な構文としては以下のようなものがあります。

  • SELECT: データを取得する
    -- postsテーブルからすべての記事を取得
    SELECT * FROM posts;
    
  • INSERT: データを追加する
    -- postsテーブルに新しい記事を追加
    INSERT INTO posts (title, content) VALUES ('新しい投稿', 'SQLで追加しました。');
    

GoのプログラムからRDBを操作するには、Goのコードを使ってこのSQL文をRDBに送って実行してもらう必要があります。

CRUD処理とは?

バックエンド開発において、データベースを操作する際の基本的な4つの処理の頭文字を取った略語が「CRUD」です。
これは、ほとんどすべてのWebアプリケーションでデータを取り扱う上で欠かせない概念です。

  • C (Create: 作成): 新しいデータをデータベースに保存すること
  • R (Read: 読み取り): データベースに保存されているデータを取得すること
  • U (Update: 更新): データベースに保存されている既存のデータを変更すること
  • D (Delete: 削除): データベースに保存されているデータを消去すること

CRUD操作はバックエンドの基本の「き」なので、これらを実装できるようになることが第一歩です。

【実践】Goとデータベースの接続準備

Goでデータベースにアクセスするには、標準ライブラリのdatabase/sqlパッケージを使います。

サードパーティ製のORMについてはまた別記事で説明しますが、今回はGoの仕組みを理解するために、標準ライブラリでハンズオンをしてみようと思います。

Goの環境構築およびgo mod initを済ませた状態から、始めていきます。

もしその方法がわからなければ、以下の記事を参考にやってみてください。

ドライバのインポート

database/sqlは、あくまで様々なデータベースと対話するための「標準的なインターフェース」を提供するパッケージです。実際に特定のデータベース(PostgreSQLやMySQLなど)と通信するためには、そのデータベース専用のドライバが別途必要になります。

今回はPostgreSQLを使うので、pgxというドライバを例に説明します。

まず、go mod initしたディレクトリ上のターミナルで、ドライバをインストールします。

go get github.com/jackc/pgx/v5/stdlib

そして、main.goなどの実行コードの中では以下のようにインポートします。

import (
    "database/sql"
    _ "github.com/jackc/pgx/v5/stdlib"
)

_(ブランクインポート)は、pgxパッケージの関数をコード内で直接使うわけではなく、そのパッケージをGoのプログラムにリンクさせ、database/sqlにPostgreSQLドライバとして登録させるために使っています。

DB接続

sql.Open()関数を使ってデータベースへの接続を確立します。

main.goファイルの中身を以下のように書いてみます。

package main

import (
	"database/sql"
	"fmt"
	"log"

	_ "github.com/jackc/pgx/v5/stdlib"
)

func main() {
	// RDB接続URL
	connStr := "postgres://myuser:mypassword@localhost:5432/mydb?sslmode=disable"
	
	// ドライバ名 "pgx" を指定して接続
	db, err := sql.Open("pgx", connStr)
	if err != nil {
	    log.Fatalf("データベースに接続できませんでした: %v\n", err)
	}
	// main関数の終了時にDB接続をクローズする
	defer db.Close()
	
	// 接続確認
	err = db.Ping()
	if err != nil {
	    log.Fatalf("データベースとの接続確認ができませんでした: %v\n", err)
	}
	fmt.Println("接続に成功しました!")
}

まず、sql.Openは、実際にはすぐに接続を確立するわけではなく、接続情報を保持した*sql.DBオブジェクトを返します。db.Ping()を呼び出すことで、実際にデータベースとの疎通確認ができます。

*sql.DBは、複数のゴルーチンから安全に使えるように設計されたコネクションプールを管理しており、アプリケーションの実行期間中、一つだけ生成して使い回すのが一般的です。

connStrでDBの接続情報が丸見えなのはまずいですが...今は放っておきます!
PostgreSQLの接続URLの書き方についてはここで触れませんが、ご自分の環境に合わせたURLを設定してみてください。

ちなみにmain.goと同じディレクトリにdocker-compose.ymlを作成して実行すると、簡単にPostgreSQLが使えます。

以下の記事を参考にやってみてください!

それでは、go run main.goなどでプログラムを実行します。

接続に成功しました!と出てきたらDB接続に成功しているので、次に進みましょう。

テーブル作成

データベースに接続できたら、次はこの後CRUD操作で使うproductsテーブルを作成します。

テーブルが存在しない場合のみ作成するために、CREATE TABLE IF NOT EXISTSというSQL文を使います。

GoのプログラムからこのSQLを実行するには、db.Exec()メソッドを使います。
Execは、行を返さないクエリ、つまり結果を返さないSQL文(CREATE TABLEINSERTUPDATEDELETEなど)を実行するのに適しています。

main関数に以下の処理を追記しましょう。

// テーブル作成のSQL
createTableSQL := `
CREATE TABLE IF NOT EXISTS products (
    id SERIAL PRIMARY KEY,
    name TEXT NOT NULL,
    price INT NOT NULL
);`

// SQLを実行
_, err = db.Exec(createTableSQL)
if err != nil {
    log.Fatalf("テーブル作成に失敗しました: %v", err)
}

fmt.Println("productテーブルが正常に作成されたか、テーブルが既に作成されています。")

これで、データを保存するための「表」の準備が整いました。

【実践】Goで行うCRUD操作

それでは、database/sqlを使って、productsテーブルに対するCRUD操作を実装してみましょう。

作成(Create)

db.QueryRow()INSERT文のRETURNING id句を組み合わせることで、新しいレコードを作成し、そのIDを取得できます。

main関数に以下の処理を追記しましょう。

var newProductID int
err = db.QueryRow(
    "INSERT INTO products (name, price) VALUES ($1, $2) RETURNING id",
    "New Gadget",
    2980,
).Scan(&newProductID)

if err != nil {
    log.Fatalf("データの挿入に失敗しました: %v", err)
}
fmt.Printf("新しく挿入されたデータのIDは以下です: %d\n", newProductID)

今回は、RETURNING id句のように、実行結果として1行のデータ(ここでは新しく生成されたID)を返しているので、db.QueryRow()を使います。

そして、QueryRow()で取得した1行の結果をGoの変数にマッピングするために、.Scan(&newProductID)を使用します。

これにより、データベースにレコードを追加すると同時に、そのレコードの自動生成されたIDを効率よく取得できます。

ちょっとセキュリティ的な話になりますが、SQLインジェクション攻撃を防ぐため、SQL文には直接値を埋め込まず、必ず?$1, $2のようなプレースホルダを使い、実際の値は関数の引数として渡す方が望ましいらしいです。

セキュリティあたりの話ももう少し詳しくなりたいところ...

読み取り(Read)

読み取り操作もdb.QueryRow()は、1行だけ結果を返すクエリに使います。
.Scan()メソッドで結果をGoの変数にマッピングします。

main関数に以下の処理を追記しましょう。

var name string
var price int
err = db.QueryRow("SELECT name, price FROM products WHERE id = $1", 1).Scan(&name, &price)

if err != nil {
    if err == sql.ErrNoRows {
        // 結果が0件だった場合の処理
        fmt.Println("データが見つかりませんでした")
    } else {
        log.Fatalf("クエリがエラーです: %v", err)
    }
} else {
    fmt.Printf("ID: 1, Name: %s, Price: %d\n", name, price)
}

Scanは結果が0件の場合、sql.ErrNoRowsという特別なエラーが返るので、これを使って見つからなかった場合の処理を実装できます。

更新 (Update)

UPDATE文には、db.Exec()を利用します。

main関数に以下の処理を追記しましょう。

// 更新後の行の内容を格納する変数
var updatedID int
var updatedName string
var updatedPrice int

err = db.QueryRow(
    "UPDATE products SET price = $1 WHERE id = $2 RETURNING id, name, price",
    3500,
    1,
).Scan(&updatedID, &updatedName, &updatedPrice)

if err != nil {
    log.Fatalf("値の更新に失敗しました: %v", err)
}

fmt.Printf("更新されたproductテーブルのデータ: ID: %d, Name: %s, Price: %d\n", updatedID, updatedName, updatedPrice)

削除 (Delete)

DELETE文も同様にdb.Exec()を使います。

main関数に以下の処理を追記しましょう。

// 削除された行の内容を格納する変数
var deletedID int
var deletedName string

err = db.QueryRow(
    "DELETE FROM products WHERE id = $1 RETURNING id, name",
    1,
).Scan(&deletedID, &deletedName)

if err != nil {
    if err == sql.ErrNoRows {
        // 削除対象の行が見つからなかった場合
        fmt.Printf("削除対象の行が見つからなかったので、データ消去できませんでした.\n")
    } else {
        // その他のエラー
        log.Fatalf("Delete failed: %v", err)
    }
} else {
    fmt.Printf("削除されたproductテーブルのデータ: ID=%d, Name=%s\n", deletedID, deletedName)
}

最後にgo run main.goなどして正常なログが出てきたら、成功です!

おわりに

この記事では、database/sqlを利用して、GoバックエンドにおけるCRUD処理についておさらいしました。

個人的にはGoでのDB接続の理解が曖昧だったので、学びになりました。
また、CRUD処理というバックエンドの基本が標準ライブラリで学べたので、応用が効きそうでよかったです。

ただ、今後これだけではあまりにもDBを使うのが不便なのでORMについての理解もさらに深めた方がいいなと思いました。

最後までお読みいただき、ありがとうございました!

この「学生ひよこ界隈が送るGo/Javaで実現する「はじめてのバックエンド」Advent Calendar 2025」では、GoJavaを使い、APIの作り方、データベースとの接続、テストやDockerといった気になったバックエンド技術の基本を振り返った学びを共有しています。

ぜひ他の記事もチェックして、筆者がこのひとりアドカレを完遂することができるか、確認してみてください(^^)

学生ひよこ界隈が送るGo/Javaで実現する「はじめてのバックエンド」Advent Calendar 2025

それでは、明日の「学生ひよこ界隈が送るGo/Javaで実現する「はじめてのバックエンド」Advent Calendar 2025」の記事もお楽しみに!

参考文献

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?