CLOVER🍀

That was when it all began.

GoでMySQLにアクセスしてみる

これは、なにをしたくて書いたもの?

Goを使って、データベースにアクセスするコードを書いてみたいなぁと思いまして。

sqlパッケージ

Goでデータベースにアクセスするには、sqlパッケージを使うようです。

sql - The Go Programming Language

sqlパッケージは、SQL(ライクな)データベースにアクセスするための、汎用インターフェースを提供するパッケージだそうです。

Package sql provides a generic interface around SQL (or SQL-like) databases.

基本的な使い方は、こちらを参照。

SQLInterface · golang/go Wiki · GitHub

そして、sqlパッケージのインターフェースを実装したドライバーは、こちらの一覧で確認できます。

SQLDrivers · golang/go Wiki · GitHub

今回は、MySQLのドライバーを使いたいと思います。

GitHub - go-sql-driver/mysql: Go MySQL Driver is a MySQL driver for Go's (golang) database/sql package

mysql · pkg.go.dev

環境

今回の環境は、こちらです。

$ go version
go version go1.16.2 linux/amd64

MySQLは8.0.23を使い、172.17.0.2で動作しているものとします。

確認用のプロジェクト。

$ go mod init mysql-example
go: creating new go.mod: module mysql-example

動作確認は、テストコードで行うことにします。

$ go get github.com/stretchr/testify

ここに、MySQLのドライバーを加えたgo.modはこちらです。

go.mod

module mysql-example

go 1.16

require (
    github.com/go-sql-driver/mysql v1.5.0 // indirect
    github.com/stretchr/testify v1.7.0 // indirect
)

GoのMySQLドライバーをインストールする

インストールは、go getすればOKです。

Go-MySQL-Driver / Installation

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

先述したgo.modに記載の通り、今回はv1.5.0を使います。

テストコードの雛形

まずは、テストコードの雛形を載せておきます。

main_test.go

package main

import (
    "context"
    "database/sql"
    "testing"

    _ "github.com/go-sql-driver/mysql"
    "github.com/stretchr/testify/assert"
)

// ここに、テストコードを書く

MySQLドライバーを使う

sqlのドライバーを使う時のimportの書き方は、こちらみたいですね。

import (

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

)

Go-MySQL-Driver / Usage

この書き方は、副作用を目的としてimportする場合に使うようです。

This table illustrates how Sin is accessed in files that import the package after the various types of import declaration.

The Go Programming Language Specification / Import declarations

importしたパッケージ自体は、ソースコード内では使いません。使うのはsqlパッケージ側です。

では、使っていってみましょう。

ドライバーが認識されているかを確認する

importしただけで、sql#Driversが認識しているドライバーの名前を返すようになります。

func TestGetRegisteredDriver(t *testing.T) {
    assert.Equal(t, []string{"mysql"}, sql.Drivers())
}

sqlパッケージがドライバーを認識しているかどうか、確認するのに良さそうですね。

データベースに接続する

データベースに接続するには、sql#Openを使います。

func TestPingMySql(t *testing.T) {
    db, err := sql.Open("mysql", "kazuhira:password@(172.17.0.2:3306)/practice")

    assert.Nil(t, err)
    assert.NotNil(t, db)

    defer db.Close()

    err = db.Ping()
    assert.Nil(t, err)
}

sql#Openの第1引数はドライバー名、第2引数はdataSourceName、DSNとも略されるみたいですね(?)を指定します。

   db, err := sql.Open("mysql", "kazuhira:password@(172.17.0.2:3306)/practice")

Go-MySQL-Driver / DSN (Data Source Name)

アドレスの部分とか、最初ちょっとピンとこなかったです…。

Go-MySQL-Driver / Address

アドレスは()で囲むんですねぇ。

[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...&paramN=valueN]

sql#Openの結果としてDBが返ってくるのですが、今回は終了時にDB#Closeするようにしています。

ですが、ドキュメントを見ていると、通常は自分でクローズすることはなさそうですね。

The returned DB is safe for concurrent use by multiple goroutines and maintains its own pool of idle connections. Thus, the Open function should be called just once. It is rarely necessary to close a DB.

Package sql / func Open

最後にpingして、接続確認。

   err = db.Ping()

Package sql / func (*DB) Ping

接続時にパラメーターを指定する

MySQLドライバーのDSNには、パラメーターを付与できます。
※他のドライバーは見ていないので、わかりません

[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...&paramN=valueN]

QueryStringな形式ですね。

Go-MySQL-Driver / Parameters

こんな感じで。

func TestConnectMysqlParameter(t *testing.T) {
    db, err := sql.Open("mysql", "kazuhira:password@(172.17.0.2:3306)/practice?charset=utf8mb4&interpolateParams=true")

    assert.Nil(t, err)

    defer db.Close()

    err = db.Ping()
    assert.Nil(t, err)
}

今回の例ではcharsetとinterpolateParamsを指定しています。

ところで、charsetは非推奨で、collationを使った方がよいという話のようです。

Go-MySQL-Driver / charset

なのですが、utf8mb4_ja_0900_as_cs_ksみたいなCollationを指定すると、以下のようにエラーになったりします。

unknown collation

ここに定義されているCollationでないと、ダメそうですねぇ…。

https://github.com/go-sql-driver/mysql/blob/v1.5.0/collations.go

https://github.com/go-sql-driver/mysql/blob/v1.5.0/packets.go#L342-L349

余談でした。

DDLを実行してみる

テーブルのcreate & dropをしてみましょう。DB#Execを使うようです。

func TestExecuteDDL(t *testing.T) {
    db, err := sql.Open("mysql", "kazuhira:password@(172.17.0.2:3306)/practice")

    assert.Nil(t, err)

    defer db.Close()

    _, err = db.Exec(`create table if not exists book(isbn varchar(14), title varchar(200), price int, primary key(isbn))`)

    assert.Nil(t, err)

    _, err = db.Exec(`drop table if exists book`)

    assert.Nil(t, err)
}

これ以降は、このcreate & dropで挟み込む感じで書いていきます。

select文やinsert文を実行してみる

次は、select文やinsert文を実行してみましょう。

func TestExecuteQueryUsingInterpolateParams(t *testing.T) {
    db, err := sql.Open("mysql", "kazuhira:password@(172.17.0.2:3306)/practice?interpolateParams=true")

    assert.Nil(t, err)

    defer db.Close()

    _, err = db.Exec(`create table if not exists book(isbn varchar(14), title varchar(200), price int, primary key(isbn))`)

    assert.Nil(t, err)

        // insert文やselect文を書く

    _, err = db.Exec(`drop table if exists book`)

    assert.Nil(t, err)
}

まずはinsert文から。クエリではない場合は、DB#Execを使います。パラメーターは?でバインドさせるようです。

   // insert
    result, err := db.Exec(`insert into book(isbn, title, price) values(?, ?, ?)`, "978-4798161488", "MySQL徹底入門 第4版", 4180)

    assert.Nil(t, err)

    rowsAffected, err := result.RowsAffected()

    assert.Nil(t, err)
    assert.Equal(t, int64(1), rowsAffected)

    result, err = db.Exec(`insert into book(isbn, title, price) values(?, ?, ?)`, "978-4873116389", "実践ハイパフォーマンスMySQL 第3版", 5280)

    assert.Nil(t, err)

    rowsAffected, err = result.RowsAffected()

    assert.Nil(t, err)
    assert.Equal(t, int64(1), rowsAffected)

    result, err = db.Exec(`insert into book(isbn, title, price) values(?, ?, ?)`, "978-4798147406", "詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド", 3960)

    assert.Nil(t, err)

    rowsAffected, err = result.RowsAffected()

    assert.Nil(t, err)
    assert.Equal(t, int64(1), rowsAffected)

Resultからは、影響のあった行数を取得できます。また、auto incrementなどを使っている場合は、LastInsertIdでデータベースが
生成したIDを取得できるようです。

   rowsAffected, err := result.RowsAffected()

    assert.Nil(t, err)
    assert.Equal(t, int64(1), rowsAffected)

続いて、クエリです。1行取得すればいいものは、DB#QueryRowを使います。結果は、Row型で返ります。

   // query row
    row := db.QueryRow(`select count(*) from book`)

    assert.Nil(t, row.Err())

    var count int
    row.Scan(&count)
    assert.Equal(t, 3, count)

    row = db.QueryRow(`select * from book where isbn = ?`, "978-4798161488")

    assert.Nil(t, row.Err())

    var isbn, name string
    var price int
    row.Scan(&isbn, &name, &price)

    assert.Equal(t, "978-4798161488", isbn)
    assert.Equal(t, "MySQL徹底入門 第4版", name)
    assert.Equal(t, 4180, price)

値の取得は、Row#Scanで行います。

   var isbn, name string
    var price int
    row.Scan(&isbn, &name, &price)

複数行が返る可能性がある場合は、DB#Queryですね。この場合は、Rowsが返ってきます。

   // query rows
    rows, err := db.Query(`select title from book where price > ? order by price desc`, 4000)

    assert.Nil(t, err)

    names := []string{}

    for rows.Next() {
        var name string

        err := rows.Scan(&name)

        assert.Nil(t, err)

        names = append(names, name)
    }

    assert.Equal(t, []string{"実践ハイパフォーマンスMySQL 第3版", "MySQL徹底入門 第4版"}, names)

結果セットから取得する行を進めるにはRows#Nextを

   for rows.Next() {

値の取得は、Rowと同様にRows#Scanを使います。

       var name string

        err := rows.Scan(&name)

RowsはCloseメソッドを備えているのですが、取得する行がなくなると自動的にクローズされるとは書かれています。

if Next is called and returns false and there are no further result sets, the Rows are closed automatically and it will suffice to check the result of Err.

Package sql / func (*Rows) Close

ところで、パラメーターをバインドする際に?を使っているのですが、これにはinterpolateParams=trueと指定する必要が
あるようです。

Go-MySQL-Driver / / interpolateParams

なのですが、このパラメーターを指定しなくても動作するような…?

あと、Named Parameterはサポートしていませんでした。

https://github.com/go-sql-driver/mysql/blob/v1.5.0/utils.go#L676-L686

DB#Prepareを使う場合

パラメーターをバインドする際に、ちゃんと手続きを踏む場合はDB#PrepareでStmtを取得し、Stmt#ExecやStmt#Queryを
使用します。

   // insert
    stmt, err := db.Prepare(`insert into book(isbn, title, price) values(?, ?, ?)`)

    assert.Nil(t, err)

    result, err := stmt.Exec("978-4798161488", "MySQL徹底入門 第4版", 4180)

使い終わらったらStmt#Close。

   stmt.Close()

先ほどの例を、DB#ExecやDB#Queryに一気にパラメーターをバインドさせずに、DB#PrepareとStmtを使って書き直したのが
こちらです。

func TestExecutePrepared(t *testing.T) {
    db, err := sql.Open("mysql", "kazuhira:password@(172.17.0.2:3306)/practice")

    assert.Nil(t, err)

    defer db.Close()

    _, err = db.Exec(`create table if not exists book(isbn varchar(14), title varchar(200), price int, primary key(isbn))`)

    assert.Nil(t, err)

    // insert
    stmt, err := db.Prepare(`insert into book(isbn, title, price) values(?, ?, ?)`)

    assert.Nil(t, err)

    result, err := stmt.Exec("978-4798161488", "MySQL徹底入門 第4版", 4180)

    assert.Nil(t, err)

    rowsAffected, err := result.RowsAffected()

    assert.Nil(t, err)
    assert.Equal(t, int64(1), rowsAffected)

    result, err = stmt.Exec("978-4873116389", "実践ハイパフォーマンスMySQL 第3版", 5280)

    assert.Nil(t, err)

    rowsAffected, err = result.RowsAffected()

    assert.Nil(t, err)
    assert.Equal(t, int64(1), rowsAffected)

    result, err = stmt.Exec("978-4798147406", "詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド", 3960)

    assert.Nil(t, err)

    rowsAffected, err = result.RowsAffected()

    assert.Nil(t, err)
    assert.Equal(t, int64(1), rowsAffected)

    stmt.Close()

    // query row
    row := db.QueryRow(`select count(*) from book`)

    assert.Nil(t, row.Err())

    var count int
    row.Scan(&count)
    assert.Equal(t, 3, count)

    stmt, err = db.Prepare(`select * from book where isbn = ?`)

    assert.Nil(t, err)

    row = stmt.QueryRow("978-4798161488")

    assert.Nil(t, row.Err())

    var isbn, name string
    var price int
    row.Scan(&isbn, &name, &price)

    assert.Equal(t, "978-4798161488", isbn)
    assert.Equal(t, "MySQL徹底入門 第4版", name)
    assert.Equal(t, 4180, price)

    stmt.Close()

    // query rows
    stmt, err = db.Prepare(`select title from book where price > ? order by price desc`)

    assert.Nil(t, err)

    rows, err := stmt.Query(4000)

    assert.Nil(t, err)

    names := []string{}

    for rows.Next() {
        var name string

        err := rows.Scan(&name)

        assert.Nil(t, err)

        names = append(names, name)
    }

    assert.Equal(t, []string{"実践ハイパフォーマンスMySQL 第3版", "MySQL徹底入門 第4版"}, names)

    stmt.Close()

    _, err = db.Exec(`drop table if exists book`)

    assert.Nil(t, err)
}

interpolateParams=trueとした時の違いは?というと、ラウンドトリップの回数が減るようです。

his reduces the number of roundtrips, since the driver has to prepare a statement, execute it with given parameters and close the statement again with interpolateParams=false.

Go-MySQL-Driver / / interpolateParams

クライアントでエスケープをしているみたいですからね。

https://github.com/go-sql-driver/mysql/blob/v1.5.0/connection.go#L183-L306

トランザクションを使ってみる(簡易)

次は、トランザクションを使ってみましょう。

func TestTransactionSimply(t *testing.T) {
    db, err := sql.Open("mysql", "kazuhira:password@(172.17.0.2:3306)/practice")

    assert.Nil(t, err)

    defer db.Close()

    _, err = db.Exec(`create table if not exists book(isbn varchar(14), title varchar(200), price int, primary key(isbn))`)

    assert.Nil(t, err)

        // ここに、トランザクションを使ったコードを書く

    _, err = db.Exec(`drop table if exists book`)

    assert.Nil(t, err)
}

簡単に使うには、DB#BeginでTxを取得して、Tx経由でExecやQueryを実行し、最後にTx#Commit、Tx#Rollbackします。

insertしてロールバック。

   tx, err := db.Begin()
    assert.Nil(t, err)

    // insert
    result, err := tx.Exec(`insert into book(isbn, title, price) values(?, ?, ?)`, "978-4798161488", "MySQL徹底入門 第4版", 4180)

    assert.Nil(t, err)

    rowsAffected, err := result.RowsAffected()

    assert.Nil(t, err)
    assert.Equal(t, int64(1), rowsAffected)

    err = tx.Rollback()

    assert.Nil(t, err)

Tx#CommitまたはTx#Rollbackした後には、そのTxはもう使えないようです。

   // transaction has already been committed or rolled back
    row := tx.QueryRow(`select count(*) from book`)

    assert.EqualError(t, row.Err(), "sql: transaction has already been committed or rolled back")
    assert.ErrorIs(t, sql.ErrTxDone, row.Err())

Txを使ってクエリーの実行したり、データをinsertしてコミット。

   tx, err = db.Begin()

    assert.Nil(t, err)

    // query row
    row = tx.QueryRow(`select count(*) from book`)

    assert.Nil(t, row.Err())

    var count int
    row.Scan(&count)
    assert.Equal(t, 0, count)

    result, err = tx.Exec(`insert into book(isbn, title, price) values(?, ?, ?)`, "978-4873116389", "実践ハイパフォーマンスMySQL 第3版", 5280)

    assert.Nil(t, err)

    rowsAffected, err = result.RowsAffected()

    assert.Nil(t, err)
    assert.Equal(t, int64(1), rowsAffected)

    result, err = tx.Exec(`insert into book(isbn, title, price) values(?, ?, ?)`, "978-4798147406", "詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド", 3960)

    assert.Nil(t, err)

    rowsAffected, err = result.RowsAffected()

    assert.Nil(t, err)
    assert.Equal(t, int64(1), rowsAffected)

    err = tx.Commit()

    assert.Nil(t, err)

    tx, err = db.Begin()

    assert.Nil(t, err)

    // query row
    row = tx.QueryRow(`select count(*) from book`)

    assert.Nil(t, row.Err())

    row.Scan(&count)
    assert.Equal(t, 2, count)

    row = tx.QueryRow(`select * from book where isbn = ?`, "978-4873116389")

    assert.Nil(t, row.Err())

    var isbn, name string
    var price int
    row.Scan(&isbn, &name, &price)

    assert.Equal(t, "978-4873116389", isbn)
    assert.Equal(t, "実践ハイパフォーマンスMySQL 第3版", name)
    assert.Equal(t, 5280, price)

    // query rows
    rows, err := tx.Query(`select title from book where price > ? order by price desc`, 4000)

    assert.Nil(t, err)

    names := []string{}

    for rows.Next() {
        var name string

        err := rows.Scan(&name)

        assert.Nil(t, err)

        names = append(names, name)
    }

    assert.Equal(t, []string{"実践ハイパフォーマンスMySQL 第3版"}, names)

    err = tx.Commit()

    assert.Nil(t, err)
トランザクションを使う

先ほどは簡易版的な感じで紹介しましたが、もっとちゃんと使うにはDB#BeginTxを使います。こちらを使うと、
Contextやトランザクションのオプションを指定できるようです。

   ctx := context.Background()

    tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted})
    assert.Nil(t, err)

DB#BeginTxの戻り値は、TxなのはDB#Beginと同じなのであとの流れは変わりません。

DB#Beginで書いていたコードを、DB#BeginTxで書き直したのがこちら。

func TestTransaction(t *testing.T) {
    db, err := sql.Open("mysql", "kazuhira:password@(172.17.0.2:3306)/practice")

    assert.Nil(t, err)

    defer db.Close()

    _, err = db.Exec(`create table if not exists book(isbn varchar(14), title varchar(200), price int, primary key(isbn))`)

    assert.Nil(t, err)

    ctx := context.Background()

    tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted})
    assert.Nil(t, err)

    // insert
    result, err := tx.Exec(`insert into book(isbn, title, price) values(?, ?, ?)`, "978-4798161488", "MySQL徹底入門 第4版", 4180)

    assert.Nil(t, err)

    rowsAffected, err := result.RowsAffected()

    assert.Nil(t, err)
    assert.Equal(t, int64(1), rowsAffected)

    err = tx.Rollback()

    assert.Nil(t, err)

    // transaction has already been committed or rolled back
    row := tx.QueryRow(`select count(*) from book`)

    assert.EqualError(t, row.Err(), "sql: transaction has already been committed or rolled back")
    assert.ErrorIs(t, sql.ErrTxDone, row.Err())

    ctx = context.Background()
    tx, err = db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted})

    assert.Nil(t, err)

    // query row
    row = tx.QueryRow(`select count(*) from book`)

    assert.Nil(t, row.Err())

    var count int
    row.Scan(&count)
    assert.Equal(t, 0, count)

    result, err = tx.Exec(`insert into book(isbn, title, price) values(?, ?, ?)`, "978-4873116389", "実践ハイパフォーマンスMySQL 第3版", 5280)

    assert.Nil(t, err)

    rowsAffected, err = result.RowsAffected()

    assert.Nil(t, err)
    assert.Equal(t, int64(1), rowsAffected)

    result, err = tx.Exec(`insert into book(isbn, title, price) values(?, ?, ?)`, "978-4798147406", "詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド", 3960)

    assert.Nil(t, err)

    rowsAffected, err = result.RowsAffected()

    assert.Nil(t, err)
    assert.Equal(t, int64(1), rowsAffected)

    err = tx.Commit()

    assert.Nil(t, err)

    ctx = context.Background()
    tx, err = db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted})

    assert.Nil(t, err)

    // query row
    row = tx.QueryRow(`select count(*) from book`)

    assert.Nil(t, row.Err())

    row.Scan(&count)
    assert.Equal(t, 2, count)

    row = tx.QueryRow(`select * from book where isbn = ?`, "978-4873116389")

    assert.Nil(t, row.Err())

    var isbn, name string
    var price int
    row.Scan(&isbn, &name, &price)

    assert.Equal(t, "978-4873116389", isbn)
    assert.Equal(t, "実践ハイパフォーマンスMySQL 第3版", name)
    assert.Equal(t, 5280, price)

    // query rows
    rows, err := tx.Query(`select title from book where price > ? order by price desc`, 4000)

    assert.Nil(t, err)

    names := []string{}

    for rows.Next() {
        var name string

        err := rows.Scan(&name)

        assert.Nil(t, err)

        names = append(names, name)
    }

    assert.Equal(t, []string{"実践ハイパフォーマンスMySQL 第3版"}, names)

    err = tx.Commit()

    assert.Nil(t, err)

    _, err = db.Exec(`drop table if exists book`)

    assert.Nil(t, err)
}
コネクションプールの設定をする

ところで、これまでずっとDBを使っていたのですが、説明を読むとどうやらコネクションプールが裏にあるようです。

The sql package creates and frees connections automatically; it also maintains a free pool of idle connections. If the database has a concept of per-connection state, such state can be reliably observed within a transaction (Tx) or connection (Conn). Once DB.Begin is called, the returned Tx is bound to a single connection. Once Commit or Rollback is called on the transaction, that transaction's connection is returned to DB's idle connection pool.

Package sql / type DB

トランザクションを使った場合は、コネクションはそのTxに紐付けられます、と。だから、Tx経由でクエリーを実行したり
するんでしょうね。

コネクションプールの設定は、DBに対して行うようです。

func TestConfigurationPool(t *testing.T) {
    db, err := sql.Open("mysql", "kazuhira:password@(172.17.0.2:3306)/practice")

    assert.Nil(t, err)

    defer db.Close()

    assert.Zero(t, db.Stats().MaxOpenConnections)

    db.SetMaxOpenConns(10)

    assert.Equal(t, 10, db.Stats().MaxOpenConnections)
}
コネクションを使う

最後に、明示的にコネクションを使ってみましょう。

コネクション(Conn)を取得するには、DB#Connを使います。この時、Contextが必要になります。

   // handle connection
    ctx := context.Background()
    conn, err := db.Conn(ctx)

使い終わったConnはクローズしましょう。プールに返却することを意味します。

   defer conn.Close()

クエリーの使い方などはConn経由となるくらいで大きくは変わりませんが、メソッド名にContextが入り、引数にもContextが
必要になります。

   // insert
    result, err := conn.ExecContext(ctx, `insert into book(isbn, title, price) values(?, ?, ?)`, "978-4798161488", "MySQL徹底入門 第4版", 4180)


    // query row
    row := conn.QueryRowContext(ctx, `select count(*) from book`)


    // query rows
    rows, err := conn.QueryContext(ctx, `select title from book where price > ? order by price desc`, 4000)

トランザクションを使う場合。

   // handle connection & tx
    ctx := context.Background()
    conn, err := db.Conn(ctx)
    tx, err := conn.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted})

Contextを使うのは変わりません。

   // insert
    result, err := tx.ExecContext(ctx, `insert into book(isbn, title, price) values(?, ?, ?)`, "978-4798161488", "MySQL徹底入門 第4版", 4180)


    // query row
    row := tx.QueryRowContext(ctx, `select count(*) from book`)


    // query rows
    rows, err := tx.QueryContext(ctx, `select title from book where price > ? order by price desc`, 4000)

Connを使ったコード例は、こちら。

func TestConnectionPool(t *testing.T) {
    db, err := sql.Open("mysql", "kazuhira:password@(172.17.0.2:3306)/practice")

    assert.Nil(t, err)

    defer db.Close()

    _, err = db.Exec(`create table if not exists book(isbn varchar(14), title varchar(200), price int, primary key(isbn))`)

    assert.Nil(t, err)

    // handle connection
    ctx := context.Background()
    conn, err := db.Conn(ctx)

    assert.Nil(t, err)

    defer conn.Close()

    // insert
    result, err := conn.ExecContext(ctx, `insert into book(isbn, title, price) values(?, ?, ?)`, "978-4798161488", "MySQL徹底入門 第4版", 4180)

    assert.Nil(t, err)

    rowsAffected, err := result.RowsAffected()

    assert.Nil(t, err)
    assert.Equal(t, int64(1), rowsAffected)

    // query row
    row := conn.QueryRowContext(ctx, `select count(*) from book`)

    assert.Nil(t, row.Err())

    var count int
    row.Scan(&count)
    assert.Equal(t, 1, count)

    row = conn.QueryRowContext(ctx, `select * from book where isbn = ?`, "978-4798161488")

    assert.Nil(t, row.Err())

    var isbn, name string
    var price int
    row.Scan(&isbn, &name, &price)

    assert.Equal(t, "978-4798161488", isbn)
    assert.Equal(t, "MySQL徹底入門 第4版", name)
    assert.Equal(t, 4180, price)

    // query rows
    rows, err := conn.QueryContext(ctx, `select title from book where price > ? order by price desc`, 4000)

    assert.Nil(t, err)

    names := []string{}

    for rows.Next() {
        var name string

        err := rows.Scan(&name)

        assert.Nil(t, err)

        names = append(names, name)
    }

    assert.Equal(t, []string{"MySQL徹底入門 第4版"}, names)

    _, err = db.Exec(`drop table if exists book`)

    assert.Nil(t, err)
}

トランザクションを使った場合。

func TestConnectionPoolTx(t *testing.T) {
    db, err := sql.Open("mysql", "kazuhira:password@(172.17.0.2:3306)/practice")

    assert.Nil(t, err)

    defer db.Close()

    _, err = db.Exec(`create table if not exists book(isbn varchar(14), title varchar(200), price int, primary key(isbn))`)

    assert.Nil(t, err)

    // handle connection & tx
    ctx := context.Background()
    conn, err := db.Conn(ctx)
    tx, err := conn.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted})

    assert.Nil(t, err)

    defer conn.Close()

    // insert
    result, err := tx.ExecContext(ctx, `insert into book(isbn, title, price) values(?, ?, ?)`, "978-4798161488", "MySQL徹底入門 第4版", 4180)

    assert.Nil(t, err)

    rowsAffected, err := result.RowsAffected()

    assert.Nil(t, err)
    assert.Equal(t, int64(1), rowsAffected)

    err = tx.Commit()

    assert.Nil(t, err)

    ctx = context.Background()
    tx, err = conn.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted})

    // query row
    row := tx.QueryRowContext(ctx, `select count(*) from book`)

    assert.Nil(t, row.Err())

    var count int
    row.Scan(&count)
    assert.Equal(t, 1, count)

    row = tx.QueryRowContext(ctx, `select * from book where isbn = ?`, "978-4798161488")

    assert.Nil(t, row.Err())

    var isbn, name string
    var price int
    row.Scan(&isbn, &name, &price)

    assert.Equal(t, "978-4798161488", isbn)
    assert.Equal(t, "MySQL徹底入門 第4版", name)
    assert.Equal(t, 4180, price)

    // query rows
    rows, err := tx.QueryContext(ctx, `select title from book where price > ? order by price desc`, 4000)

    assert.Nil(t, err)

    names := []string{}

    for rows.Next() {
        var name string

        err := rows.Scan(&name)

        assert.Nil(t, err)

        names = append(names, name)
    }

    assert.Equal(t, []string{"MySQL徹底入門 第4版"}, names)

    err = tx.Commit()

    assert.Nil(t, err)

    _, err = db.Exec(`drop table if exists book`)

    assert.Nil(t, err)
}

ところで、DB#Queryなどを使った場合はどうなっているんでしょう?

ソースコードを見ると、裏でConnを取得しているようです。

https://github.com/golang/go/blob/go1.16.2/src/database/sql/sql.go#L1622-L1629

さらに言うと、Contextもその場で取得しているようです。

https://github.com/golang/go/blob/go1.16.2/src/database/sql/sql.go#L1619

このContext、なにに使うんでしょうね?と少し見てみたのですが、キャンセルまわりみたいですね。

まとめ

ざっくりと、Goを使ってMySQLにアクセスしてみました。

sqlパッケージまわりの使い方と、ドライバーの存在などがわかった感じです。

(途中でだんだん面倒になって雑になってますが)とりあえず雰囲気はわかったので良しとしましょう。