![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbw5SZzUmN0QzM4UmMlRzN4gjYmVzYkBTN5YWN2IWOiBjZi9CX0JXZ252bj91Ztl2Lc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
上一章中對于golang的元程式設計說明如下:
- 1 插件系統
- 2 代碼生成
接下來我們來對golang的标準庫進行說明,主要内容有:
- 1 JSON
- 2 HTTP
- 3 資料庫
— — — — — — — — — — — — — — — — — — — — — — — — — — — —
資料庫幾乎是所有 Web 服務不可或缺的一部分,在所有類型的資料庫中,關系型資料庫是我們在想要持久存儲資料時的首要選擇,不過因為關系型資料庫的種類繁多,是以 Go 語言的标準庫
database/sql
就為通路關系型資料提供了通用的接口,這樣不同資料庫隻要實作标準庫中的接口,應用程式就可以通過标準庫中的方法通路。
3.1 設計原理
結構化查詢語言(Structured Query Language、SQL)是在關系型資料庫系統中使用的領域特定語言(Domain-Specific Language、DSL),它主要用于處理結構化的資料1。作為一門領域特定語言,它由更加強大的表達能力,與傳統的指令式 API 相比,它能夠提供兩個優點:
- 可以使用單個指令在資料庫中通路多條資料;
- 不需要在查詢中指定擷取資料的方法;
所有的關系型資料庫都會提供 SQL 作為查詢語言,應用程式可以使用相同的 SQL 查詢在不同資料庫中查詢資料,當然不同的資料庫在實作細節和接口上還略有一些不同,這些不相容的特性在不同資料庫中仍然無法通用,例如:PostgreSQL 中的幾何類型,不過它們基本都會相容标準的 SQL 查詢以友善應用程式接入:
圖 - SQL 和資料庫
如上圖所示,SQL 是應用程式和資料庫之間的中間層,應用程式在多數情況下都不需要關心底層資料庫的實作,它們隻關心 SQL 查詢傳回的資料。
Go 語言的
database/sql
就建立在上述前提下,我們可以使用相同的 SQL 語言查詢關系型資料庫,所有關系型資料庫的用戶端都需要實作如下所示的驅動接口:
type Driver interface {
Open(name string) (Conn, error)
}
type Conn interface {
Prepare(query string) (Stmt, error)
Close() error
Begin() (Tx, error)
}
database/sql/driver.Driver
接口中隻包含一個
Open
方法,該方法接收一個資料庫連接配接串作為輸入參數并傳回一個特定資料庫的連接配接,作為參數的資料庫連接配接串是資料庫特定的格式,這個傳回的連接配接仍然是一個接口,整個标準庫中的全部接口可以構成如下所示的樹形結構:
圖 - 資料庫驅動樹形結構
MySQL 的驅動 go-sql-driver/mysql 就實作了上圖中的樹形結構,我們就可以使用語言原生的接口在 MySQL 中查詢或者管理資料。
3.2 驅動接口
我們在這裡從
database/sql
标準庫提供的幾個方法為入口分析這個中間層的實作原理,其中包括資料庫驅動的注冊、擷取資料庫連接配接和查詢資料,這些方法都是我們在與資料庫打交道時的最常用接口。
database/sql
中提供的
database/sql.Register
方法可以注冊自定義的資料庫驅動,這個 package 的内部包含兩個變量,分别是
drivers
哈希以及
driversMu
互斥鎖,所有的資料庫驅動都會存儲在這個哈希中:
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
}
MySQL 驅動會在
init
中調用上述方法将實作
database/sql/driver.Driver
接口的結構體注冊到全局的驅動清單中:
func init() {
sql.Register("mysql", &MySQLDriver{})
}
當我們在全局變量中注冊了驅動之後,就可以使用
database/sql.Open
方法擷取特定資料庫的連接配接。在如下所示的方法中,我們通過傳入的驅動名擷取
database/sql/driver.Driver
組成
database/sql.dsnConnector
結構體後調用
database/sql.OpenDB
:
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)
}
...
return OpenDB(dsnConnector{dsn: dataSourceName, driver: driveri}), nil
}
database/sql.OpenDB
函數會傳回一個
database/sql.DB
結構體,這是标準庫包為我們提供的關鍵結構體,無論是我們直接使用标準庫查詢資料庫,還是使用 GORM 等 ORM 架構都會用到它:
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
}
這個結構體
database/sql.DB
在剛剛初始化時不會包含任何的資料庫連接配接,它持有的資料庫連接配接池會在真正應用程式申請連接配接時在單獨的 Goroutine 中擷取。
database/sql.DB.connectionOpener
方法中包含一個不會退出的循環,每當該 Goroutine 收到了請求時都會調用
database/sql.DB.openNewConnection
:
func (db *DB) openNewConnection(ctx context.Context) {
ci, _ := db.connector.Connect(ctx)
...
dc := &driverConn{
db: db,
createdAt: nowFunc(),
returnedAt: nowFunc(),
ci: ci,
}
if db.putConnDBLocked(dc, err) {
db.addDepLocked(dc, dc)
} else {
db.numOpen--
ci.Close()
}
}
資料庫結構體
database/sql.DB
中的連結器是實作了
database/sql/driver.Connector
類型的接口,我們可以使用該接口建立任意數量完全等價的連接配接,建立的所有連接配接都會被加入連接配接池中,MySQL 的驅動在
connector.Connect
方法實作了連接配接資料庫的邏輯。
無論是使用 ORM 架構還是直接使用标準庫,當我們在查詢資料庫時都會調用
database/sql.DB.Query
方法,該方法的入參就是 SQL 語句和 SQL 語句中的參數,它會初始化新的上下文并調用
database/sql.DB.QueryContext
:
func (db *DB) QueryContext(ctx context.Context, query string, args ...interface{}) (*Rows, error) {
var rows *Rows
var err error
for i := 0; i < maxBadConnRetries; i++ {
rows, err = db.query(ctx, query, args, cachedOrNewConn)
if err != driver.ErrBadConn {
break
}
}
if err == driver.ErrBadConn {
return db.query(ctx, query, args, alwaysNewConn)
}
return rows, err
}
database/sql.DB.query
函數的執行過程可以分成兩個部分,首先調用私有方法
database/sql.DB.conn
擷取底層資料庫的連接配接,資料庫連接配接既可能是剛剛通過連接配接器建立的,也可能是之前緩存的連接配接;擷取連接配接之後調用
database/sql.DB.queryDC
在特定的資料庫連接配接上執行查詢:
func (db *DB) queryDC(ctx, txctx context.Context, dc *driverConn, releaseConn func(error), query string, args []interface{}) (*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)
})
if err != driver.ErrSkip {
if err != nil {
releaseConn(err)
return nil, err
}
rows := &Rows{
dc: dc,
releaseConn: releaseConn,
rowsi: rowsi,
}
rows.initContextClose(ctx, txctx)
return rows, nil
}
}
...
}
上述方法在準備了 SQL 查詢所需的參數之後,會調用
database/sql.ctxDriverQuery
方法完成 SQL 查詢,我們會判斷目前的查詢上下文究竟實作了哪個接口,然後調用對應接口的
Query
或者
QueryContext
:
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
}
...
return queryer.Query(query, dargs)
}
對應的資料庫驅動會真正負責執行調用方輸入的 SQL 查詢,作為中間層的标準庫可以不在乎具體的實作,抹平不同關系型資料庫的差異,為使用者程式提供統一的接口。
3.3 總結
Go 語言的标準庫
database/sql
是一個抽象層的經典例子,雖然關系型資料庫的功能相對比較複雜,但是我們仍然可以通過定義一系列構成樹形結構的接口提供合理的抽象,這也是我們在編寫架構和中間層時應該注意的,即面向接口程式設計 —— 隻依賴抽象的接口,不要依賴具體的實作。
全套教程點選下方連結直達:IT實戰:Go語言設計與實作自學教程zhuanlan.zhihu.com