Diary

Diary

日々学んだことをアウトプットする場として初めてみました

go の標準パッケージ sql がコネクションを管理する際の注意点

標準パッケージ sql一般的な SQL のインタフェースを提供しています

使用する際は各ベンダーの用意するドライバーと共に使うことが必要で、諸々の使い方は go の wiki にも書いてあります。

今回はこの sql パッケージで注意が必要な点(と自分が今認識してる範囲)についてメモしておきます。
他にも気をつけるべき点があればぜひ教えてください。

[目次]

## 扱わないこと
## 登場人物
## sql パッケージ利用時に気をつけたい点
### sql.Open はコネクションを確立しない
### コネクションを確立するには
### コネクションプールの制御
## (おまけ)db 側からコネクション数確認(postgres 編)
## おわりに

扱わないこと

以下内容については今回は扱いません。

  • sqlboiler や gorm といった ORM がどのような扱いをしているのかについて

登場人物

sql パッケージ利用時に気をつけたい

sql.Open はコネクションを確立しない

wiki の Connecting to a database にも書いてありますが、sql.Open では sql.DB の初期化を行うのみで実際のコネクションは確立しません。

そのため、ポートが間違っていたり db が起動してなかったりしてもエラーになりません

以下のように、db.Stats() で取得できる sql.DBStats でコネクションに関する情報が取得できます。

package main

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

    _ "github.com/lib/pq"
)

func main() {
    source := fmt.Sprintf(
        "host=%s port=%s user=%s password=%s dbname=%s sslmode=%s",
        "localhost", "4646", "ubuntu", "ubuntu", "testdb", "disable",
    )

    db, err := sql.Open("postgres", source)
    if err != nil {
        log.Fatal(err)
    }
    stats := db.Stats()
    // 0: アイドル状態のコネクション数。
    fmt.Printf("stats.Idle: %v\t", stats.Idle)
    // 0: アクティブ状態のコネクション数。
    fmt.Printf("stats.InUse: %v\n", stats.InUse)
}

この辺の記載は 最近発売された『Go言語 100Tips』にも記載されてますので、気になる方はぜひ読んでみることをお勧めします。

Go言語 100Tips ありがちなミスを把握し、実装を最適化する (impress top gear)

新品価格
¥3,960から
(2023/10/18 00:21時点)

コネクションを確立するには

ではどのようにコネクションを確立すれば良いかというと、sql.DB に対し db.Exec や db.Query を発行することで勝手にコネクションが貼られます。 ユーザーが意識することはありません。

特に Ping のコメントに

// PingContext verifies a connection to the database is still alive, // establishing a connection if necessary.

と丁寧にあるように、Ping でもコネクションが貼られることが分かります。

func checkDBStatus(db *sql.DB) {
    stats := db.Stats()
    fmt.Printf("stats.Idle: %v\t", stats.Idle)
    fmt.Printf("stats.InUse: %v\n", stats.InUse)
}

func main() {
    source := fmt.Sprintf(
        "host=%s port=%s user=%s password=%s dbname=%s sslmode=%s",
        "localhost", "4646", "ubuntu", "ubuntu", "testdb", "disable",
    )

    db, err := sql.Open("postgres", source)
    if err != nil {
        log.Fatal(err)
    }

    // sql.Open は実際にはコネクションをオープンしない!!
    // stats.Idle: 0   stats.InUse: 0
    checkDBStatus(db)

    defer func() {
        if closeErr := db.Close(); err != nil {
            log.Print(closeErr)
        }
    }()

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

    // Ping 等で、必要になった場合にコネクションをオープンする。
    // stats.Idle: 1   stats.InUse: 0
    checkDBStatus(db)
}

コネクションプールの制御

sql.DB に生えているメソッドを通して『コネクションプールの最大数』や『コネクションがアイドルになってから切断するまでの時間』の設定等が可能です。 デフォルトではどちらも 0 に設定されており、これは無制限を意味するため注意が必要です。

func main() {
    source := fmt.Sprintf(
        "host=%s port=%s user=%s password=%s dbname=%s sslmode=%s",
        "localhost", "4646", "ubuntu", "ubuntu", "testdb", "disable",
    )

    db, err := sql.Open("postgres", source)
    if err != nil {
        log.Fatal(err)
    }

    // オープンするコネクションの最大数を設定する。
    // デフォルトでは 0 で無制限!!
    db.SetMaxOpenConns(1)

    // 1 時間アイドル状態が続いたコネクションは閉じる!
    // デフォルトでは 0 で無制限!!
    db.SetConnMaxIdleTime(1 * time.Hour)
}

(おまけ)db 側からコネクション数確認(postgres 編)

pg_stat_activity テーブルを確認することで、現在のコネクション情報が取得できるらしいです。

SELECT pid, usename, application_name, state, query_start FROM pg_stat_activity;
testdb=# \d pg_stat_activity;
                      View "pg_catalog.pg_stat_activity"
      Column      |           Type           | Collation | Nullable | Default 
------------------+--------------------------+-----------+----------+---------
 datid            | oid                      |           |          | 
 datname          | name                     |           |          | 
 pid              | integer                  |           |          | 
 leader_pid       | integer                  |           |          | 
 usesysid         | oid                      |           |          | 
 usename          | name                     |           |          | 
 application_name | text                     |           |          | 
 client_addr      | inet                     |           |          | 
 client_hostname  | text                     |           |          | 
 client_port      | integer                  |           |          | 
 backend_start    | timestamp with time zone |           |          | 
 xact_start       | timestamp with time zone |           |          | 
 query_start      | timestamp with time zone |           |          | 
 state_change     | timestamp with time zone |           |          | 
 wait_event_type  | text                     |           |          | 
 wait_event       | text                     |           |          | 
 state            | text                     |           |          | 
 backend_xid      | xid                      |           |          | 
 backend_xmin     | xid                      |           |          | 
 query_id         | bigint                   |           |          | 
 query            | text                     |           |          | 
 backend_type     | text                     |           |          | 

testdb=# SELECT pid, usename, application_name, state, query_start FROM pg_stat_activity;
 pid | usename | application_name | state  |          query_start          
-----+---------+------------------+--------+-------------------------------
  28 | ubuntu  |                  |        | 
  27 |         |                  |        | 
  36 | ubuntu  | psql             | active | 2023-09-16 15:40:27.098589+00
  24 |         |                  |        | 
  23 |         |                  |        | 
  26 |         |                  |        | 
(6 rows)

おわりに

net.Conn の抽象化もエグいので、いつか使えるようになりたい!