sqlパッケージ周りのコードリーディング

業務でGoのデバッグをするためにsqlパッケージ周りのコードを読む必要があって、いい勉強になったのでメモがてらまとめておきます

MySQLを使っているのですが、このimportをよく見かけます

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

mysqlドライバのコードを直接参照しないからこうしてるのですが、直接参照しないのにどうやってmysqlのメソッドを実行しているのだろうというのが疑問に思いました

多分interface使って呼び出してるんだろうなくらいのぼんやりした理解度でした
これはその通りだったのですが、ちゃんと理解できたら色々繋がって面白かったのでまとめます

今回はMySQLドライバを使っているので

  • MySQLドライバによる初期化処理
  • 実際にクエリ実行する時の処理

という順で見ていこうと思います
適宜コードを貼って見ていきますが、説明上不要な部分は省略します

今回見たパッケージのバージョンは以下の通り

go@1.20.2
go-sql-driver/mysql@1.7.1

目次

MySQLドライバによる初期化処理

まずDBへの接続処理を行います

import (
    ...
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
)

func main() {
    dbconf := "user:pass@tcp(127.0.0.1:3306)/database?...."
    db, err := sql.Open("mysql", dbconf)
    ...
}

さて、疑問だった _でのimportが早速出てきました

これをもう少し深ぼって見ていきます
実際に中を覗いてみると、init関数があることがわかります

func init() {
  	sql.Register("mysql", &MySQLDriver{})
}

これはsqlパッケージのRegisterメソッドを呼び出しています

type Driver interface {
  	Open(name string) (Conn, error)
}

func Register(name string, driver driver.Driver) {
    ...
    drivers[name] = driver
}

これが何をしているかというとdriverdrivers["mysql"]に設定しています
driverというのはOpenメソッドをもつinterfaceで、先ほどのMySQLDriverOpenメソッドが実装されているのでこのinterfaceを満たします

mysqlドライバを_でimportした時点ではこの処理が走るのみです

さて、次にこの状態で db, err := sql.Open("mysql", dbconf)が行われると何が起こるかを見ていきます

func Open(driverName, dataSourceName string) (*DB, error) {
	driveri, ok := drivers[driverName]

	if driverCtx, ok := driveri.(driver.DriverContext); ok {
		connector, err := driverCtx.OpenConnector(dataSourceName)
		if err != nil {
			return nil, err
		}
		return OpenDB(connector), nil
	}
}

先ほど登録したdriverがまず取り出されています。これが先ほど格納したMySQLDriverの構造体です

それがDriverContextinterfaceを満たすかどうかをチェックしています
MySQLDriverdriver.Driverinterfaceも満たしていますが、同時にこのDriverContextinterfaceも満たしています
つまりOpenConnectorメソッドを持っているのでこれを実行するとconnectorが取得できます (connectorがどういうものかは後述します)
それを引数にOpenDBメソッドを呼び出しています

OpenDBの中を見ていきます

func OpenDB(c driver.Connector) *DB {
	ctx, cancel := context.WithCancel(context.Background())
	db := &DB{
		connector:    c,
    ...
	}

	go db.connectionOpener(ctx)

	return db
}

先ほど取得したconnectorがsqlパッケージのDBconnectorフィールドに格納されます
そしてconnectionOpenerメソッドで接続処理をしています

さてここでdbがreturnされていて、これが呼び出し元に返ってきます

db, err := sql.Open("mysql", dbconf)

つまり、dbの中にはdriver.Connectorinterfaceを満たすconnectorが格納されていて、これが後々mysqlのメソッドを実行する上で重要な役割を果たします。
connectorがどういうものかはこの後じっくり追っていくのでここでは説明しません。
とりあえずこれから使用するdbの中にはconnectorが格納されていて、それはMySQLDriverOpenConnectorメソッドによって返されたものであることさえ理解できてればここではokです

実際にクエリ実行する時の処理

ここでは例としてQueryContextで処理を追っていきます

以降では先ほどのdbを使ってクエリの実行をした時の流れを見ていくわけですが、先に結論を書くと先ほど格納したconnectorが持つConnectメソッドを呼び出すとmysqlConnというmysqlのconnectionの構造体が得られるようになっており、これがQueryContextのinterfaceを満たしているのでそれを呼び出すとmysqlのQueryContextが実行されるというカラクリです

では詳細を見ていきます

query := `SELECT ...`

db.QueryContext(ctx, query)

(実際は独自のstructでラップされていると思うのでこんな風にdbから直接呼び出すことはあまりない気もしますが。)

func (db *DB) QueryContext(ctx context.Context, query string, args ...any) (*Rows, error) {
	var rows *Rows
	var err error

	err = db.retry(func(strategy connReuseStrategy) error {
		rows, err = db.query(ctx, query, args, strategy)
		return err
	})

	return rows, err
}

QueryContextの中ではretryメソッドを呼び出しつつその中で実際に処理する内容であるqueryメソッドを呼び出しています

queryメソッドの中を見ていきます

func (db *DB) query(ctx context.Context, query string, args []any, strategy connReuseStrategy) (*Rows, error) {
	dc, err := db.conn(ctx, strategy)
	if err != nil {
		return nil, err
	}

	return db.queryDC(ctx, nil, dc, dc.releaseConn, query, args)
}

func (db *DB) conn(ctx context.Context, strategy connReuseStrategy) (*driverConn, error) {
	...
	ci, err := db.connector.Connect(ctx)
	dc := &driverConn{
		db:         db,
		ci:         ci,
		...
	}
	...
	return dc, nil
}

まずconnメソッドを呼び出していてその中ではdb.connector.Connectというメソッドを呼び出しています

さて、再びconnectorが登場しました
queryメソッドの中身を見ていく前に、まずはconnectorが何者なのかを紐解いていきます
これは以下のようなConnectorというinterfaceを満たすものです

type Connector interface {
	Connect(context.Context) (Conn, error)
	Driver() Driver
}

type Conn interface {
	Prepare(query string) (Stmt, error)
	Close() error
	Begin() (Tx, error)
}

Connというinterfaceを返すConnectメソッドを持つことがわかります

このconnectorというのは初期化時にmysqlドライバによって取得されたものでした
ではmysqlドライバがconnectorを取得した実装を見ていきます

func (d MySQLDriver) OpenConnector(dsn string) (driver.Connector, error) {
	cfg, err := ParseDSN(dsn)
	if err != nil {
		return nil, err
	}
	return &connector{
		cfg: cfg,
	}, nil
}

connectorという構造体を返しています
これがConnectorinterfaceを満たしているわけですから、このconnectorConnectメソッドを実装しているはずです
実際にそれを見てみましょう

func (c *connector) Connect(ctx context.Context) (driver.Conn, error) {
	...
	mc := &mysqlConn{
		maxAllowedPacket: maxPacketSize,
		maxWriteSize:     maxPacketSize - 1,
		closech:          make(chan struct{}),
		cfg:              c.cfg,
	}
	...
	return mc, nil
}

Conninterfaceを満たすmysqlConnという構造体を返していますね

ここまでわかれば十分なので元のquery実行部分に戻ります
再掲すると以下のようになっています

func (db *DB) query(ctx context.Context, query string, args []any, strategy connReuseStrategy) (*Rows, error) {
	dc, err := db.conn(ctx, strategy)
	if err != nil {
		return nil, err
	}

	return db.queryDC(ctx, nil, dc, dc.releaseConn, query, args)
}

func (db *DB) conn(ctx context.Context, strategy connReuseStrategy) (*driverConn, error) {
	...
	ci, err := db.connector.Connect(ctx)
	dc := &driverConn{
		db:         db,
		ci:         ci,
		...
	}
	...
	return dc, nil
}

db.connector.Connect(ctx)で返されたciは先ほど見たmysqlConnです
connectorはinterfaceなのでsqlパッケージでは単にConnectメソッドを呼び出すだけで、mysqlに依存せずmysqlConnが取得できているのがわかります

connメソッドはmysqlConndriverConnciというフィールドに格納してdcを返しています
なるほど、ここでコネクションプールの管理をしているわけですね

queryメソッドはqueryDCメソッドを呼び出していて引数にこのdcを渡しています

func (db *DB) queryDC(ctx, txctx context.Context, dc *driverConn, releaseConn func(error), query string, args []any) (*Rows, error) {
	queryerCtx, ok := dc.ci.(driver.QueryerContext)
	if ok {
		...
		rowsi, err = ctxDriverQuery(ctx, queryerCtx, queryer, query, nvdargs)
	}
}

先ほどのciQueryerContextを満たしているかどうかをチェックして、満たしている場合はctxDriverQueryメソッドを呼び出しています
引数に渡しているqueryerCtxはinterfaceを満たす、つまりQueryContextメソッドを持つことが保証されており、繰り返しですがその実体はmysqlConnです

ではctxDriverQueryを見ていきます

func ctxDriverQuery(ctx context.Context, queryerCtx driver.QueryerContext, queryer driver.Queryer, query string, nvdargs []driver.NamedValue) (driver.Rows, error) {
	if queryerCtx != nil {
		return queryerCtx.QueryContext(ctx, query, nvdargs)
	}
	...
}

QueryContextを呼び出しています
このqueryerCtxは実体はmysqlConnですから、実際の処理はmysqlパッケージのQueryContextが実行されるわけです

こうしてsqlパッケージはmysqlドライバに依存せずにinterface越しにmysqlのメソッドを呼び出すことができるようになっています

まとめ

mysqlドライバのコードを直接参照していなくても実行できる仕組みをまとめると、以下のようになっていました

  • importした時のinitでsqlのdriverにOpenConnectorメソッドを持つmysqlの構造体を格納
  • sqlの初期接続時にdriverのOpenConnectorメソッドを実行し、sql.DBconnectorConnectメソッドを持つmysql用の構造体を格納しておく
  • クエリ実行時にconnectorConnectメソッドを実行し、mysqlのコネクションを取得
  • このmysqlコネクションがQueryContextメソッドを実装している

読んでないけどおそらくpostgresqlとかのドライバも同じ仕組みになっているのだろうと思われます

全体を振り返るとsqlパッケージでは様々なものがinterfaceで定義されていてOpenConnectorConnectはinterface越しに実行されています

これによってsqlパッケージはどのRDBのドライバを使うかに依存しないようになっています
設計が美しいですね

いい勉強になりました!!!

この記事が参考になったからコーヒーくらいおごってもいいぜという方は、以下からサポートいただけると次の記事書くモチベになりますのでよろしくお願いします

Buy Me A Coffee
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次