欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

深入了解Go語(yǔ)言中database/sql是如何設(shè)計(jì)的

 更新時(shí)間:2023年07月03日 09:50:34   作者:江湖十年  
在?Go?語(yǔ)言中內(nèi)置了?database/sql?包,它只對(duì)外暴露了一套統(tǒng)一的編程接口,便可以操作不同數(shù)據(jù)庫(kù),那么database/sql?是如何設(shè)計(jì)的呢,下面就來(lái)和大家簡(jiǎn)單聊聊吧

常見(jiàn)的關(guān)系型數(shù)據(jù)庫(kù)都支持標(biāo)準(zhǔn)的 SQL 語(yǔ)言,所以無(wú)論是 MySQL、PostgreSQL 還是 SQL Server,我們都可以使用相同的 SQL 語(yǔ)句來(lái)對(duì)其進(jìn)行操作。這種思想同樣體現(xiàn)在 Go 語(yǔ)言的數(shù)據(jù)庫(kù)操作中,在 Go 語(yǔ)言中內(nèi)置了 database/sql 包,它只對(duì)外暴露了一套統(tǒng)一的編程接口,便可以操作不同數(shù)據(jù)庫(kù)。

本文重點(diǎn)講解 database/sql 設(shè)計(jì)思想,默認(rèn)讀者已經(jīng)有了 database/sql 使用經(jīng)驗(yàn),對(duì)于 database/sql 功能則不會(huì)詳細(xì)介紹。如果你對(duì) database/sql 不熟悉,可以查看我的另一篇文章《在 Go 中如何使用 database/sql 來(lái)操作數(shù)據(jù)庫(kù)》

接口設(shè)計(jì)

首先我們來(lái)看下 database/sql 目錄結(jié)構(gòu)長(zhǎng)什么樣:

go1.20.1/src/database
└── sql
    ├── driver
    │   ├── driver.go
    │   └── types.go
    ├── convert.go
    ├── ctxutil.go
    ├── sql.go
    ...

筆記:這里沒(méi)有列出測(cè)試文件。

可以發(fā)現(xiàn),database/sql 實(shí)際上包含了 sql 包及其子包 driver。

sql 包為我們提供了操作數(shù)據(jù)庫(kù)的對(duì)象以及方法,driver 包則定義了數(shù)據(jù)庫(kù)驅(qū)動(dòng)的編程接口,這些接口都是第三方驅(qū)動(dòng)包需要實(shí)現(xiàn)的。

現(xiàn)在我們一起來(lái)看下 driver 包是如何設(shè)計(jì)的。

driver 包中定義了一個(gè) Driver 接口:

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

這個(gè)接口只有一個(gè) Open 方法,用來(lái)建立一個(gè)數(shù)據(jù)庫(kù)連接并返回。

Open 方法的 name 參數(shù)即為 DSN,返回值中的 Conn 接口則代表了一個(gè)數(shù)據(jù)庫(kù)連接,定義如下:

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

Conn 接口包含三個(gè)方法:

Prepare 用來(lái)預(yù)處理 SQL,返回一個(gè)準(zhǔn)備好的 SQL 語(yǔ)句。

Close 用來(lái)關(guān)閉數(shù)據(jù)庫(kù)連接。

Begin 顯然是對(duì)事務(wù)的支持。

其中 Prepare 返回 Stmt 類(lèi)型,這也是一個(gè)接口,定義如下:

type Stmt interface {
	Close() error
	NumInput() int
	Exec(args []Value) (Result, error)
	Query(args []Value) (Rows, error)
}

Close 用來(lái)關(guān)閉該預(yù)處理語(yǔ)句。

NumInput 返回 SQL 中占位符參數(shù)的數(shù)量。

ExecQuery 兩個(gè)方法我們?cè)偈煜げ贿^(guò)了,分別用來(lái)執(zhí)行 SQL 命令以及查詢(xún)記錄。這兩個(gè)方法都接收參數(shù) []Value,Value 其實(shí)是 any 類(lèi)型,也就是 interface{},定義如下:

type Value any

Exec 方法返回的 Result 接口定義如下:

type Result interface {
	LastInsertId() (int64, error)
	RowsAffected() (int64, error)
}

LastInsertId 返回 INSERT SQL 插入記錄的 ID。

RowsAffected 返回受影響記錄的行數(shù)。

Query 方法返回的 Rows 接口定義如下:

type Rows interface {
	Columns() []string
	Close() error
	Next(dest []Value) error
}

當(dāng)我們執(zhí)行 SQL 查詢(xún)時(shí),如果不知道列名,可以使用 rows.Columns() 查看所有列名稱(chēng)列表。

Close 用來(lái)關(guān)閉 Rows 的迭代器,關(guān)閉后無(wú)法再繼續(xù)調(diào)用 Next 查詢(xún)下一條記錄。

調(diào)用 Next 可以將下一行數(shù)據(jù)填充到提供的 dest 切片中。

Value 在上面已經(jīng)介紹過(guò)了,是 any 類(lèi)型。

現(xiàn)在 Conn 接口中定義的 Prepare 方法這條線所涉及到的類(lèi)型,我們已經(jīng)追查到底了,是時(shí)候回過(guò)頭來(lái)看下 Begin 方法返回的 Tx 類(lèi)型定義了:

type Tx interface {
	Commit() error
	Rollback() error
}

Tx 不出所料,同樣是一個(gè)接口,包含兩個(gè)方法:

Commit 用來(lái)提交事務(wù)。

Rollback 用來(lái)回滾事務(wù)。

至此,Driver 接口的設(shè)計(jì)就清晰的擺在眼前了:

除了 Driver 接口,在 database/sql/driver 包中,還有幾個(gè)常用接口定義如下:

type Connector interface {
	Connect(context.Context) (Conn, error)
	Driver() Driver
}
type Pinger interface {
	Ping(ctx context.Context) error
}
type Execer interface {
	Exec(query string, args []Value) (Result, error)
}
type ExecerContext interface {
	ExecContext(ctx context.Context, query string, args []NamedValue) (Result, error)
}
type Queryer interface {
	Query(query string, args []Value) (Rows, error)
}
type QueryerContext interface {
	QueryContext(ctx context.Context, query string, args []NamedValue) (Rows, error)
}

Connector 接口用來(lái)連接數(shù)據(jù)庫(kù)。

Pinger 接口用來(lái)檢查連接是否能被正確建立。

還有 Execer、ExecerContext、QueryerQueryerContext 這 4 個(gè)接口,正好對(duì)應(yīng)了我們?cè)诶?database/sql 時(shí)操作數(shù)據(jù)庫(kù)所使用的方法。

所有這些接口,都是第三方數(shù)據(jù)庫(kù)驅(qū)動(dòng)包要實(shí)現(xiàn)的接口(有些接口是可選的)。

看到這里,你可能有個(gè)疑惑,為什么這些接口都只定義為只有一個(gè)方法的小接口?

這其實(shí)是 Go 語(yǔ)言中的慣用法,越小的接口抽象程度越高,易于解耦,也越容易被實(shí)現(xiàn),并且非常適用于 Go 語(yǔ)言的組合機(jī)制。

好了,關(guān)于 driver 包中接口的定義部分就講解到這里,其他用的比較少接口的我就不在這里介紹了,感興趣的同學(xué)可以自行嘗試閱讀源碼學(xué)習(xí)。

以上介紹的這些接口全部定義在 driver/driver.go 文件中,而 driver/types.go 文件中則用來(lái)定義類(lèi)型,如 Bool、Int32 等方便用來(lái)類(lèi)型轉(zhuǎn)換,由于不是本文重點(diǎn),這里也就不多介紹了。

代碼實(shí)現(xiàn)

看了以上關(guān)于接口定義的講解,你可能會(huì)覺(jué)得有些云里霧里,有種學(xué)了一身功夫卻又無(wú)從下手的感覺(jué)。

沒(méi)關(guān)系,接下來(lái)我將根據(jù)一段示例代碼,帶你深入到 database/sql 的源碼中,加深你對(duì) database/sql 包的理解。

以下示例是我們使用 database/sql 操作 MySQL 最典型的場(chǎng)景:

package main
import (
	"database/sql"
	"fmt"
	_ "github.com/go-sql-driver/mysql"
)
func main() {
	db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/demo?charset=utf8mb4&parseTime=true&loc=Local")
	if err != nil {
		panic(err.Error())
	}
	defer db.Close()
	rows, err := db.Query("SELECT id, name FROM user")
	if err != nil {
		panic(err.Error())
	}
	defer rows.Close()
	for rows.Next() {
		var (
			id   int
			name string
		)
		if err := rows.Scan(id, name); err != nil {
			panic(err.Error())
		}
		fmt.Printf("id: %d, name: %s\n", id, name)
	}
}

這段代碼最讓初學(xué)者摸不著頭腦的是我們以匿名的方式導(dǎo)入了 MySQL 驅(qū)動(dòng)包:

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

但在實(shí)際的代碼中并沒(méi)有使用它。

其實(shí),這看似有些奇怪的代碼導(dǎo)入的作用,就隱藏在 go-sql-driver/mysql 包的 init 函數(shù)中:

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

go-sql-driver/mysql 包導(dǎo)入并使用 database/sql 包的 sql.Register 函數(shù),將自己實(shí)現(xiàn)的驅(qū)動(dòng)程序注冊(cè)到 database/sql 中。

調(diào)用注冊(cè)函數(shù)的代碼寫(xiě)在了 init 函數(shù)中,go-sql-driver/mysql 包正是利用了匿名導(dǎo)入時(shí) Go 語(yǔ)言會(huì)自動(dòng)調(diào)用被導(dǎo)入包的 init 方法所產(chǎn)生的副作用,來(lái)實(shí)現(xiàn)驅(qū)動(dòng)注冊(cè)。

注冊(cè)驅(qū)動(dòng)函數(shù) sql.Register 實(shí)現(xiàn)如下:

var (
	driversMu sync.RWMutex
	drivers   = make(map[string]driver.Driver)
)
func Register(name string, driver driver.Driver) {
	driversMu.Lock()
	defer driversMu.Unlock()
	if driver == nil {
		panic("sql: Register driver is nil")
	}
	if _, dup := drivers[name]; dup {
		panic("sql: Register called twice for driver " + name)
	}
	drivers[name] = driver
}

可以發(fā)現(xiàn),Register 內(nèi)部通過(guò)全局互斥鎖變量 driversMu 保證了并發(fā)操作的安全性。在加鎖的條件下,將 mysql 驅(qū)動(dòng)保存在 drivers 這個(gè)全局的 map 類(lèi)型變量中,以 mysqlkey,驅(qū)動(dòng)對(duì)象為 value

這就是為什么,我們能夠使用 sql.Open 函數(shù)與數(shù)據(jù)庫(kù)建立連接的原因。

func Open(driverName, dataSourceName string) (*DB, error) {
	driversMu.RLock()
	driveri, ok := drivers[driverName]
	driversMu.RUnlock()
	if !ok {
		return nil, fmt.Errorf("sql: unknown driver %q (forgotten import?)", driverName)
	}
	if driverCtx, ok := driveri.(driver.DriverContext); ok {
		connector, err := driverCtx.OpenConnector(dataSourceName)
		if err != nil {
			return nil, err
		}
		return OpenDB(connector), nil
	}
	return OpenDB(dsnConnector{dsn: dataSourceName, driver: driveri}), nil
}

Open 函數(shù)接收兩個(gè)參數(shù),驅(qū)動(dòng)名稱(chēng)和 DSN。

Open 函數(shù)內(nèi)部,首先從全局變量 drivers 中獲取驅(qū)動(dòng)對(duì)象。而我們調(diào)用 sql.Open 函數(shù)時(shí),傳遞的第一個(gè)參數(shù)是 mysql,這剛好與 go-sql-driver/mysql 包中注冊(cè)的驅(qū)動(dòng)名稱(chēng)對(duì)應(yīng),所以能夠獲取到 MySQL 驅(qū)動(dòng)程序。

接著,代碼中通過(guò)類(lèi)型斷言,判斷驅(qū)動(dòng)對(duì)象 driveri 是否為 driver.DriverContext 類(lèi)型。

是的話就先調(diào)用驅(qū)動(dòng)對(duì)象的 OpenConnector 方法得到 Connector 類(lèi)型的對(duì)象,然后再使用 OpenDB 打開(kāi)數(shù)據(jù)庫(kù)連接。

driver.DriverContext 接口定義如下:

type DriverContext interface {
	OpenConnector(name string) (Connector, error)
}

只包含了 OpenConnector 方法,這個(gè)方法返回 Connector 接口類(lèi)型。

Connector 接口前文已經(jīng)講過(guò),我們可以再回顧下它的定義:

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

這個(gè)接口定義了兩個(gè)方法分別用來(lái)連接數(shù)據(jù)庫(kù)和獲取驅(qū)動(dòng)對(duì)象。

而如果 driveri 不是 driver.DriverContext 類(lèi)型,則需要先構(gòu)造一個(gè) dsnConnector 對(duì)象,然后再使用 OpenDB 函數(shù)打開(kāi)數(shù)據(jù)庫(kù)連接。

dsnConnector 是一個(gè)結(jié)構(gòu)體,定義非常簡(jiǎn)單:

type dsnConnector struct {
	dsn    string
	driver driver.Driver
}

只包含了 DSN 和驅(qū)動(dòng)對(duì)象。

并且它同時(shí)也實(shí)現(xiàn)了 Connector 接口:

func (t dsnConnector) Connect(_ context.Context) (driver.Conn, error) {
	return t.driver.Open(t.dsn)
}
func (t dsnConnector) Driver() driver.Driver {
	return t.driver
}

接下來(lái),我們看看 OpenDB 函數(shù)是如何定義的:

func OpenDB(c driver.Connector) *DB {
	ctx, cancel := context.WithCancel(context.Background())
	db := &DB{
		connector:    c,
		openerCh:     make(chan struct{}, connectionRequestQueueSize),
		lastPut:      make(map[*driverConn]string),
		connRequests: make(map[uint64]chan connRequest),
		stop:         cancel,
	}
	go db.connectionOpener(ctx)
	return db
}

OpenDB 函數(shù)內(nèi)部實(shí)例化了一個(gè) *sql.DB 結(jié)構(gòu)體指針并返回。這個(gè)結(jié)構(gòu)體由 database/sql 包提供,是統(tǒng)一用戶編程接口的關(guān)鍵結(jié)構(gòu)體,我們后續(xù)的查詢(xún)操作,就是調(diào)用了這個(gè)對(duì)象上的方法。

這里實(shí)例化 *sql.DB 對(duì)象時(shí),并不不會(huì)立即建立數(shù)據(jù)庫(kù)連接,連接會(huì)在需要時(shí)被延遲建立。

sql.DB 結(jié)構(gòu)體中,我們需要關(guān)注的是 openerCh 屬性,這是一個(gè) Channel 對(duì)象,是一個(gè)用來(lái)保存連接請(qǐng)求的隊(duì)列,稍后我們將見(jiàn)到它的關(guān)鍵作用。

db 對(duì)象創(chuàng)建后,通過(guò) go db.connectionOpener(ctx) 單獨(dú)啟用了一個(gè)協(xié)程,用來(lái)處理建立連接的請(qǐng)求。

函數(shù)最終返回了這個(gè) *sql.DB 類(lèi)型的 db 對(duì)象,此對(duì)象是并發(fā)安全的,支持多個(gè) Goroutine 同時(shí)操作,并且維護(hù)了自己的空閑連接池。

OpenDB 函數(shù)只應(yīng)被調(diào)用一次,且很少需要用戶主動(dòng)關(guān)閉連接。

db.connectionOpener 方法實(shí)現(xiàn)如下:

func (db *DB) connectionOpener(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			return
		case <-db.openerCh:
			db.openNewConnection(ctx)
		}
	}
}

這里僅包含一個(gè)永不退出的無(wú)限循環(huán),當(dāng) db.openerCh 這個(gè) Channel 有值時(shí),代碼會(huì)進(jìn)入 db.openNewConnection 函數(shù)的調(diào)用。

func (db *DB) openNewConnection(ctx context.Context) {
	ci, err := db.connector.Connect(ctx)
	...
	dc := &driverConn{
		db:         db,
		createdAt:  nowFunc(),
		returnedAt: nowFunc(),
		ci:         ci,
	}
	if db.putConnDBLocked(dc, err) {
		db.addDepLocked(dc, dc)
	}
	...
}

db.openNewConnection 函數(shù)的第一行代碼中,db.connector 屬性是在之前調(diào)用 OpenDB 時(shí)進(jìn)行賦值的一個(gè) driver.Connector 接口類(lèi)型對(duì)象(還記得前文講的 dsnConnector 嗎),調(diào)用它的 Connect 方法就可以與數(shù)據(jù)庫(kù)建立連接了。

之后調(diào)用的 db.putConnDBLocked(dc, err) 方法作用是將這個(gè)連接放入數(shù)據(jù)庫(kù)空閑連接池中(db.freeConn 屬性)。

至此,我們得到了兩條函數(shù)調(diào)用線:

在驅(qū)動(dòng)包 go-sql-driver/mysql 中,通過(guò) sql.Register 進(jìn)行驅(qū)動(dòng)程序注冊(cè)。

database/sql 中,我們調(diào)用 sql.OpenDB 來(lái)開(kāi)啟數(shù)據(jù)庫(kù)連接,這不會(huì)立刻建立連接,而是通過(guò)開(kāi)啟新的 Goroutine 阻塞在 db.openerCh Channel 上,等待建立連接請(qǐng)求。

那么接下來(lái),何時(shí)觸發(fā) *sql.DB.connectionOpener 函數(shù)中 <-db.openerCh 這個(gè) case,就是我們要研究的重點(diǎn)了。

我們可以順著示例代碼繼續(xù)往下看。

在示例中,接下來(lái)使用 db.Query("SELECT id, name FROM user") 方法來(lái)查詢(xún) user 記錄。

*sql.DB.Query 方法定義如下:

func (db *DB) Query(query string, args ...any) (*Rows, error) {
	return db.QueryContext(context.Background(), query, args...)
}

它直接調(diào)用了 *sql.DB.QueryContext

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
}

*sql.DB.QueryContext 方法內(nèi)部又調(diào)用了 db.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)
}

db.query 方法內(nèi)部,首先調(diào)用了 db.conn 方法。db.conn 顧名思義,就是用來(lái)建立數(shù)據(jù)庫(kù)連接的,定義如下:

func (db *DB) conn(ctx context.Context, strategy connReuseStrategy) (*driverConn, error) {
	last := len(db.freeConn) - 1
	if strategy == cachedOrNewConn && last >= 0 {
		conn := db.freeConn[last]
		...
		return conn, nil
	}
	...
	ci, err := db.connector.Connect(ctx)
	if err != nil {
		db.mu.Lock()
		db.numOpen-- // correct for earlier optimism
		db.maybeOpenNewConnections()
		db.mu.Unlock()
		return nil, err
	}
	db.mu.Lock()
	dc := &driverConn{
		db:         db,
		createdAt:  nowFunc(),
		returnedAt: nowFunc(),
		ci:         ci,
		inUse:      true,
	}
	db.addDepLocked(dc, dc)
	db.mu.Unlock()
	return dc, nil
}

這里我省略了一些代碼,只列出了比較重要的邏輯。

在函數(shù)內(nèi)部,首先會(huì)嘗試從 db.freeConn 空閑連接池中獲取連接。

如果沒(méi)有空閑連接,則調(diào)用 db.connector.Connect 來(lái)獲取新的數(shù)據(jù)庫(kù)連接。

當(dāng)獲取連接失敗,會(huì)調(diào)用 db.maybeOpenNewConnections() 方法并返回錯(cuò)誤。

這個(gè) db.maybeOpenNewConnections() 方法是我們要關(guān)注的重點(diǎn),定義如下:

func (db *DB) maybeOpenNewConnections() {
	numRequests := len(db.connRequests)
	if db.maxOpen > 0 {
		numCanOpen := db.maxOpen - db.numOpen
		if numRequests > numCanOpen {
			numRequests = numCanOpen
		}
	}
	for numRequests > 0 {
		db.numOpen++ // optimistically
		numRequests--
		if db.closed {
			return
		}
		db.openerCh <- struct{}{}
	}
}

可以發(fā)現(xiàn),正是在這個(gè)方法內(nèi)部,調(diào)用了 db.openerCh <- struct{}{} 為 Channel 發(fā)送數(shù)據(jù)。

當(dāng) db.openerCh 有值時(shí),會(huì)被前文講解的通過(guò)子協(xié)程調(diào)用的 *sql.DB.connectionOpener 函數(shù)消費(fèi),以此來(lái)觸發(fā)異步獲取數(shù)據(jù)庫(kù)連接操作。

前文有提到,異步創(chuàng)建的數(shù)據(jù)庫(kù)連接會(huì)被放入空閑連接池 db.freeConn 中。

此時(shí),我們?cè)俅位氐?db.query 方法被調(diào)用的地方,來(lái)重新審視下 *sql.DB.QueryContext 方法的定義:

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
}

這里并不是簡(jiǎn)單的直接調(diào)用 db.query,而是將其放入了 db.retry 方法中調(diào)用。

顧名思義,db.retry 方法是用來(lái)進(jìn)行重試操作的,如果 db.query 調(diào)用失敗,則會(huì)重試一次。

這就體現(xiàn)了當(dāng)調(diào)用 db.connector.Connect(ctx) 失敗時(shí),調(diào)用 db.maybeOpenNewConnections() 方法異步建立連接的意義。

因?yàn)槿绻谝淮蝿?chuàng)建連接失敗,則 db.retry 會(huì)進(jìn)行重試,下次重試的時(shí)候,再次進(jìn)入 db.conn 方法,如果異步建立連接已經(jīng)完成,則可以直接從空閑連接池 db.freeConn 中獲取數(shù)據(jù)庫(kù)連接。即使異步建立連接來(lái)不及完成,那么空閑連接池也會(huì)有一個(gè)新的連接被創(chuàng)建,下次有另外一個(gè)請(qǐng)求進(jìn)來(lái),也能夠從空閑連接池中獲取連接。這個(gè)操作能夠提升程序的性能。

至此,database/sql 包中 sql.Open*sql.DB.Query 兩條函數(shù)調(diào)用線,我們就搞清楚了:

這兩條函數(shù)調(diào)用線通信的關(guān)鍵,就是 db.openerCh 所在。

現(xiàn)在,上圖中 *sql.DB.Query 這條函數(shù)調(diào)用線我們唯獨(dú)沒(méi)有搞清楚的就只剩下 *sql.DB.queryDC 的調(diào)用了。

*sql.DB.queryDC 定義如下:

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)
	var queryer driver.Queryer
	if !ok {
		queryer, ok = dc.ci.(driver.Queryer)
	}
	if ok {
		var nvdargs []driver.NamedValue
		var rowsi driver.Rows
		var err error
		withLock(dc, func() {
			nvdargs, err = driverArgsConnLocked(dc.ci, nil, args)
			if err != nil {
				return
			}
			rowsi, err = ctxDriverQuery(ctx, queryerCtx, queryer, query, nvdargs)
		})
		...
	}
	...
}

這里斷言了 *driverConn 中攜帶的查詢(xún)對(duì)象是 driver.QueryerContext 還是 driver.Queryer,并將斷言結(jié)果傳遞給 ctxDriverQuery 函數(shù)。

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)
	}
	dargs, err := namedValueToValue(nvdargs)
	if err != nil {
		return nil, err
	}
	select {
	default:
	case <-ctx.Done():
		return nil, ctx.Err()
	}
	return queryer.Query(query, dargs)
}

ctxDriverQuery 函數(shù)內(nèi)部,根據(jù)查詢(xún)對(duì)象類(lèi)型的不同,調(diào)用了 queryerCtx.QueryContextqueryer.Query。

這個(gè)操作正是在調(diào)用驅(qū)動(dòng)程序?qū)?yīng)的 QueryContextQuery 方法。

不管是 driver.QueryerContext 還是 driver.Queryer,都是 database/sql/driver 中定義的接口類(lèi)型,database/sql 內(nèi)部正是通過(guò)使用接口類(lèi)型,來(lái)實(shí)現(xiàn)跟驅(qū)動(dòng)程序 go-sql-driver/mysql 的解耦。

這樣,database/sql 不直接跟 go-sql-driver/mysql 中定義的具體類(lèi)型打交道,二者通過(guò) database/sql/driver 這個(gè)中間層來(lái)交互,這便是 Go 語(yǔ)言接口用法的精髓所在。

現(xiàn)在,我們的函數(shù)調(diào)用線路圖就已經(jīng)完整了。

總結(jié)

本文帶大家一起學(xué)習(xí)了 database/sql 包的設(shè)計(jì)思想,database/sql/driver 用來(lái)定義定義驅(qū)動(dòng)需要實(shí)現(xiàn)的接口,database/sql 則為用戶提供了操作數(shù)據(jù)庫(kù)的方法。

這里涉及了一個(gè)使用 init 函數(shù)的技巧,利用 init 函數(shù)的副作用,可以實(shí)現(xiàn)不改 database/sql 任何代碼的情況下,只需要 import 驅(qū)動(dòng)程序,就能注冊(cè)驅(qū)動(dòng)程序的所有功能。

通過(guò)一個(gè)簡(jiǎn)單的示例程序,我們一起閱讀了 database/sql 包的部分源碼,以 *sql.DB.Query 方法作為示例,查看了 database/sql 最終是在何處調(diào)用驅(qū)動(dòng)程序?qū)?yīng)方法的。拋磚引玉,如果你對(duì)其他方法源碼也感興趣,可以順著我講解的思路繼續(xù)深入學(xué)習(xí)。

database/sql 包統(tǒng)一了 Go 語(yǔ)言操作數(shù)據(jù)庫(kù)的編程接口,避免了操作不同數(shù)據(jù)庫(kù)需要學(xué)習(xí)多套 API 的窘境。

記住,在 Go 語(yǔ)言中使用接口來(lái)解耦是慣用方法,你一定要掌握。未來(lái)我們講解如何編寫(xiě)單元測(cè)試代碼的時(shí)候,還會(huì)用到。

注意:本文講解的 database/sql 包源碼版本為 Go 1.20.1,其他版本可能有所不同。

以上就是深入了解Go語(yǔ)言中database/sql是如何設(shè)計(jì)的的詳細(xì)內(nèi)容,更多關(guān)于Go語(yǔ)言database/sql的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • 如何使用騰訊云go sdk 查詢(xún)對(duì)象存儲(chǔ)中最新文件

    如何使用騰訊云go sdk 查詢(xún)對(duì)象存儲(chǔ)中最新文件

    這篇文章主要介紹了使用騰訊云go sdk 查詢(xún)對(duì)象存儲(chǔ)中最新文件,這包括如何創(chuàng)建COS客戶端,如何逐頁(yè)檢索對(duì)象列表,并如何對(duì)結(jié)果排序以找到最后更新的對(duì)象,我們還展示了如何優(yōu)化用戶體驗(yàn),通過(guò)實(shí)時(shí)進(jìn)度更新和檢索多個(gè)文件來(lái)改進(jìn)程序,需要的朋友可以參考下
    2024-03-03
  • Golang實(shí)現(xiàn)Dijkstra算法過(guò)程詳解

    Golang實(shí)現(xiàn)Dijkstra算法過(guò)程詳解

    Dijkstra 算法是一種用于計(jì)算無(wú)向圖的最短路徑的算法,它是基于貪心策略的,每次選擇當(dāng)前距離起始節(jié)點(diǎn)最近的未訪問(wèn)節(jié)點(diǎn)進(jìn)行訪問(wèn),并更新其相鄰節(jié)點(diǎn)的距離值,以得到最短路徑,這篇文章主要介紹了Golang實(shí)現(xiàn)Dijkstra算法,需要的朋友可以參考下
    2023-05-05
  • Golang cron 定時(shí)器和定時(shí)任務(wù)的使用場(chǎng)景

    Golang cron 定時(shí)器和定時(shí)任務(wù)的使用場(chǎng)景

    Ticker是一個(gè)周期觸發(fā)定時(shí)的計(jì)時(shí)器,它會(huì)按照一個(gè)時(shí)間間隔往channel發(fā)送系統(tǒng)當(dāng)前時(shí)間,而channel的接收者可以以固定的時(shí)間間隔從channel中讀取事件,這篇文章主要介紹了Golang cron 定時(shí)器和定時(shí)任務(wù),需要的朋友可以參考下
    2022-09-09
  • go語(yǔ)言之給定英語(yǔ)文章統(tǒng)計(jì)單詞數(shù)量(go語(yǔ)言小練習(xí))

    go語(yǔ)言之給定英語(yǔ)文章統(tǒng)計(jì)單詞數(shù)量(go語(yǔ)言小練習(xí))

    這篇文章給大家分享go語(yǔ)言小練習(xí)給定英語(yǔ)文章統(tǒng)計(jì)單詞數(shù)量,實(shí)現(xiàn)思路大概是利用go語(yǔ)言的map類(lèi)型,以每個(gè)單詞作為關(guān)鍵字存儲(chǔ)數(shù)量信息,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友參考下吧
    2020-01-01
  • Golang?Heap的源碼剖析

    Golang?Heap的源碼剖析

    這篇文章主要給大家詳細(xì)剖析了Golang?Heap源碼,文中有詳細(xì)的代碼示例,對(duì)我們學(xué)習(xí)Golang?Heap有一定的幫助,需要的朋友可以參考下
    2023-07-07
  • go語(yǔ)言定義零值可用的類(lèi)型學(xué)習(xí)教程

    go語(yǔ)言定義零值可用的類(lèi)型學(xué)習(xí)教程

    這篇文章主要為大家介紹了go語(yǔ)言定義零值可用的類(lèi)型教程學(xué)習(xí),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-06-06
  • Go每日一庫(kù)之zap日志庫(kù)的安裝使用指南

    Go每日一庫(kù)之zap日志庫(kù)的安裝使用指南

    這篇文章主要為大家介紹了Go每日一庫(kù)之zap安裝使用示例學(xué)習(xí),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-05-05
  • golang?使用sort.slice包實(shí)現(xiàn)對(duì)象list排序

    golang?使用sort.slice包實(shí)現(xiàn)對(duì)象list排序

    這篇文章主要介紹了golang?使用sort.slice包實(shí)現(xiàn)對(duì)象list排序,對(duì)比sort跟slice兩種排序的使用方式區(qū)別展開(kāi)內(nèi)容,需要的小伙伴可以參考一下
    2022-03-03
  • 一文帶你掌握Go語(yǔ)言I/O操作中的io.Reader和io.Writer

    一文帶你掌握Go語(yǔ)言I/O操作中的io.Reader和io.Writer

    在?Go?語(yǔ)言中,io.Reader?和?io.Writer?是兩個(gè)非常重要的接口,它們?cè)谠S多標(biāo)準(zhǔn)庫(kù)中都扮演著關(guān)鍵角色,下面就跟隨小編一起學(xué)習(xí)一下它們的使用吧
    2025-01-01
  • golang websocket 服務(wù)端的實(shí)現(xiàn)

    golang websocket 服務(wù)端的實(shí)現(xiàn)

    這篇文章主要介紹了golang websocket 服務(wù)端的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2019-09-09

最新評(píng)論