這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
資料庫操作是一個應用必不可少的部分,但是我們很多時候對golang的sql包僅僅是會用,這是不夠的。每一條語句的執行,它的背後到底發生了什麼。各式各樣對sql包的封裝,是不是有必要的,有沒有做無用功?
這是go to database package系列文章的第一篇。本系列將按照程式中使用sql包的順序來展開
先來看一段簡短的代碼:
package mainimport ( "database/sql" _ "github.com/go-sql-driver/mysql" "fmt")func main() { db, err := sql.Open("mysql", "user:password@/dbname") if nil != err { panic(err) } age := 18 rows,err := db.Query(`SELECT name,age FROM person where age > ?`, age) if nil != err { panic(err) } defer rows.Close() for rows.Next() { var name string var age int err := rows.Scan(&name, &age) if nil != err { panic(err) } fmt.Println(name, age) }}
這應該是最簡單的使用情境了。本文也會按照以上代碼,逐句展開。
import _ "somedriver"是在幹什麼
先來看一下golang官方文檔的說法:
To import a package solely for its side-effects (initialization), use the blank identifier as explicit package name:
import _ "lib/math"
也就是說,import _ "somedriver"僅僅是想調用somedriver包的init方法。那麼我們可以一起來看看go-sql-driver/mysql的init方法。它非常簡單:
func init() { sql.Register("mysql", &MySQLDriver{})}
只有1行,確實非常的簡單。調用sql的Register方法註冊了一個名為mysql的資料庫驅動,而驅動本身就是&MySQLDriver{}。
那我們再看看sql包中的Register方法:
// Register makes a database driver available by the provided name.// If Register is called twice with the same name or if driver is nil,// it panics.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}
Register的第二個參數接收一個driver.Driver的interface,因此go-sql-driver/mysql包中的&MySQLDriver必須實現driver.Driver規定的一系列方法(當然它肯定實現了)。
Register函數如果發現名為name的driver已經註冊了,就會觸發panic,否則就進行註冊。註冊其實很簡單,drivers[name] = driver。
drivers是一個map
drivers = make(map[string]driver.Driver)
所以簡單來說,import _ "somedriver"其實就是調用sql.Register註冊一個實現了driver.Driver介面的執行個體。
驅動給sql包提供了最基本的支援,sql包最終與資料庫打交道的操作都是通過driver完成的。其實不應該說sql包,而應該說是DB執行個體。
在上面程式main函數的一開始,執行sql.Open拿到了一個DB執行個體,那麼什麼是DB執行個體,sql.Open又幹了什嗎?
sql.Open是在幹什麼
看一下官方文檔的介紹:
func Open(driverName, dataSourceName string) (*DB, error)//Open opens a database specified by its database driver name and a driver-specific data source name, usually consisting of at least a database name and connection information.//Most users will open a database via a driver-specific connection helper function that returns a *DB. No database drivers are included in the Go standard library. See https://golang.org/s/sqldrivers for a list of third-party drivers.//Open may just validate its arguments without creating a connection to the database. To verify that the data source name is valid, call Ping.//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.
簡單來說,Open返回一個DB執行個體,DB執行個體引用了由driverName指定的資料庫驅動程式。DB本身維護了資料庫連接池,是安全執行緒的。
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) } db := &DB{ driver: driveri, dsn: dataSourceName, openerCh: make(chan struct{}, connectionRequestQueueSize), lastPut: make(map[*driverConn]string), } go db.connectionOpener() return db, nil}
Open方法:
- 根據
driverName拿到對應的driver
- 根據driver和dataSourceName產生一個DB執行個體
- 另起一個goroutine來執行某種任務A
如果對goroutine比較敏感的同學可能會猜到go db.connectionOpener()是在幹嘛。在go中大多數情況下新開一個goroutine都是在:
- 監聽某個channel
- 往某個channel發訊息
根據上面的代碼不難猜測,connectionOpener和opennerCh有關。看名字也很容易看出,connectionOpener翻譯過來就是串連建立者,負責建立串連。看看代碼吧:
// Runs in a separate goroutine, opens new connections when requested.func (db *DB) connectionOpener() { for range db.openerCh { db.openNewConnection() }}
每當從openerCh取到一條訊息,connectionOpener就建立一個串連。
如何建立串連其實很簡單,就是調用Driver提供的Open方法,具體先暫時不展開了。(不展開的這個決定,和golang的sql包是很吻合的,因為sql包對Open一個串連的處理,僅僅是定義了一個介面,讓驅動去實現。也就是說,在邏輯上這裡需要Open一個新串連,具體怎麼做我不管,Driver你提供Open介面,返回給我我要的就行。)
整個DB可以畫一張圖來理解。
211460-b738074548aa0ba8.png
當然DB執行個體還有很多其它細節,但是對於sql.Open方法來說,以上就夠了。總結一下,sql.Open會根據driverName和dataSourceName產生一個DB執行個體,並且另起一個goroutine來負責建立串連(監聽openerCh的“建立串連請求”)。
在這裡可以看出,執行sql.Open,僅僅返回了DB執行個體,但無法得知是否真的和資料庫成功串連。按照文檔的說法,如果要確認是否和資料庫真的串連上了,需要執行Ping方法:
Open may just validate its arguments without creating a connection to the database. To verify that the data source name is valid, call Ping.
成功拿到DB對象之後,我們就可以操作資料庫了。
下一篇,我們的主題將是串連池的維護和what's behind the db.Query command