【轉】dive into golang database/sql(1)

轉,原文:https://www.jianshu.com/p/3b0b3a4c83da

 

---------------

數據庫操作是一個應用必不可少的部分,但是我們很多時候對golang的sql包僅僅是會用,這是不夠的。每一條語句的執行,它的背後到底發生了什麼。各式各樣對sql包的封裝,是不是有必要的,有沒有做無用功?

這是go to database package系列文章的第一篇。本系列將按照程序中使用sql包的順序來展開

先來看一段簡短的代碼:

package main
import (
    "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/mysqlinit方法。它非常簡單:

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發消息

根據上面的代碼不難猜測,connectionOpeneropennerCh有關。看名字也很容易看出,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



作者:suoga
鏈接:https://www.jianshu.com/p/3b0b3a4c83da
來源:簡書
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章