# 5.1 database/sql介面 Go與PHP不同的地方是Go官方沒有提供資料庫驅動,而是為開發資料庫驅動定義了一些標準介面,開發者可以根據定義的介面來開發相應的資料庫驅動,這樣做有一個好處,只要是按照標準介面開發的程式碼, 以後需要遷移資料庫時,不需要任何修改。那麼Go都定義了哪些標準介面呢?讓我們來詳細的分析一下 ## sql.Register 這個存在於database/sql的函式是用來註冊資料庫驅動的,當第三方開發者開發資料庫驅動時,都會實現init函式,在init裡面會呼叫這個`Register(name string, driver driver.Driver)`完成本驅動的註冊。 我們來看一下mymysql、sqlite3的驅動裡面都是怎麼呼叫的: ```Go //https://github.com/mattn/go-sqlite3驅動 func init() { sql.Register("sqlite3", &SQLiteDriver{}) } //https://github.com/mikespook/mymysql驅動 // Driver automatically registered in database/sql var d = Driver{proto: "tcp", raddr: "127.0.0.1:3306"} func init() { Register("SET NAMES utf8") sql.Register("mymysql", &d) } ``` 我們看到第三方資料庫驅動都是透過呼叫這個函式來註冊自己的資料庫驅動名稱以及相應的driver實現。在database/sql內部透過一個map來儲存使用者定義的相應驅動。 ```Go var drivers = make(map[string]driver.Driver) drivers[name] = driver ``` 因此透過database/sql的註冊函式可以同時註冊多個數據函式庫驅動,只要不重複。 >在我們使用database/sql介面和第三方函式庫的時候經常看到如下: > import ( > "database/sql" > _ "github.com/mattn/go-sqlite3" > ) >新手都會被這個`_`所迷惑,其實這個就是Go設計的巧妙之處,我們在變數賦值的時候經常看到這個符號,它是用來忽略變數賦值的佔位符,那麼套件引入用到這個符號也是相似的作用,這兒使用`_`的意思是引入後面的套件名而不直接使用這個套件中定義的函式,變數等資源。 >我們在2.3節流程和函式一節中介紹過init函式的初始化過程,套件在引入的時候會自動呼叫套件的init函式以完成對套件的初始化。因此,我們引入上面的資料庫驅動套件之後會自動去呼叫init函式,然後在init函式裡面註冊這個資料庫驅動,這樣我們就可以在接下來的程式碼中直接使用這個資料庫驅動了。 ## driver.Driver Driver是一個數據函式庫驅動的介面,他定義了一個method: Open(name string),這個方法返回一個數據函式庫的Conn介面。 ```Go type Driver interface { Open(name string) (Conn, error) } ``` 返回的Conn只能用來進行一次goroutine的操作,也就是說不能把這個Conn應用於Go的多個goroutine裡面。如下程式碼會出現錯誤 ```Go ... go goroutineA (Conn) //執行查詢操作 go goroutineB (Conn) //執行插入操作 ... ``` 上面這樣的程式碼可能會使Go不知道某個操作究竟是由哪個goroutine發起的,從而導致資料混亂,比如可能會把goroutineA裡面執行的查詢操作的結果返回給goroutineB從而使B錯誤地把此結果當成自己執行的插入資料。 第三方驅動都會定義這個函式,它會解析name引數來取得相關資料庫的連線資訊,解析完成後,它將使用此資訊來初始化一個Conn並返回它。 ## driver.Conn Conn是一個數據函式庫連線的介面定義,他定義了一系列方法,這個Conn只能應用在一個goroutine裡面,不能使用在多個goroutine裡面,詳情請參考上面的說明。 ```Go type Conn interface { Prepare(query string) (Stmt, error) Close() error Begin() (Tx, error) } ``` Prepare函式返回與當前連線相關的執行Sql語句的準備狀態,可以進行查詢、刪除等操作。 Close函式關閉當前的連線,執行釋放連線擁有的資源等清理工作。因為驅動實現了database/sql裡面建議的conn pool,所以你不用再去實現快取conn之類別的,這樣會容易引起問題。 Begin函式返回一個代表事務處理的Tx,透過它你可以進行查詢,更新等操作,或者對事務進行回滾、遞交。 ## driver.Stmt Stmt是一種準備好的狀態,和Conn相關聯,而且只能應用於一個goroutine中,不能應用於多個goroutine。 ```Go type Stmt interface { Close() error NumInput() int Exec(args []Value) (Result, error) Query(args []Value) (Rows, error) } ``` Close函式關閉當前的連結狀態,但是如果當前正在執行query,query還是有效返回rows資料。 NumInput函式返回當前預留引數的個數,當返回>=0時資料庫驅動就會智慧檢查呼叫者的引數。當資料庫驅動套件不知道預留引數的時候,返回-1。 Exec函式執行Prepare準備好的sql,傳入引數執行update/insert等操作,返回Result資料 Query函式執行Prepare準備好的sql,傳入需要的引數執行select操作,返回Rows結果集 ## driver.Tx 事務處理一般就兩個過程,遞交或者回滾。資料庫驅動裡面也只需要實現這兩個函式就可以 ```Go type Tx interface { Commit() error Rollback() error } ``` 這兩個函式一個用來遞交一個事務,一個用來回滾事務。 ## driver.Execer 這是一個Conn可選擇實現的介面 ```Go type Execer interface { Exec(query string, args []Value) (Result, error) } ``` 如果這個介面沒有定義,那麼在呼叫DB.Exec,就會首先呼叫Prepare返回Stmt,然後執行Stmt的Exec,然後關閉Stmt。 ## driver.Result 這個是執行Update/Insert等操作返回的結果介面定義 ```Go type Result interface { LastInsertId() (int64, error) RowsAffected() (int64, error) } ``` LastInsertId函式返回由資料庫執行插入操作得到的自增ID號。 RowsAffected函式返回query操作影響的資料條目數。 ## driver.Rows Rows是執行查詢返回的結果集介面定義 ```Go type Rows interface { Columns() []string Close() error Next(dest []Value) error } ``` Columns函式返回查詢資料庫表的欄位資訊,這個返回的slice和sql查詢的欄位一一對應,而不是返回整個表的所有欄位。 Close函式用來關閉Rows迭代器。 Next函式用來返回下一條資料,把資料賦值給dest。dest裡面的元素必須是driver.Value的值除了string,返回的資料裡面所有的string都必須要轉換成[]byte。如果最後沒資料了,Next函式最後返回io.EOF。 ## driver.RowsAffected RowsAffected其實就是一個int64的別名,但是他實現了Result介面,用來底層實現Result的表示方式 ```Go type RowsAffected int64 func (RowsAffected) LastInsertId() (int64, error) func (v RowsAffected) RowsAffected() (int64, error) ``` ## driver.Value Value其實就是一個空介面,他可以容納任何的資料 ```Go type Value interface{} ``` drive的Value是驅動必須能夠操作的Value,Value要麼是nil,要麼是下面的任意一種 ```Go int64 float64 bool []byte string [*]除了Rows.Next返回的不能是string. time.Time ``` ## driver.ValueConverter ValueConverter介面定義瞭如何把一個普通的值轉化成driver.Value的介面 ```Go type ValueConverter interface { ConvertValue(v interface{}) (Value, error) } ``` 在開發的資料庫驅動套件裡面實現這個介面的函式在很多地方會使用到,這個ValueConverter有很多好處: - 轉化driver.value到資料庫表相應的欄位,例如int64的資料如何轉化成資料庫表uint16欄位 - 把資料庫查詢結果轉化成driver.Value值 - 在scan函式裡面如何把driver.Value值轉化成使用者定義的值 ## driver.Valuer Valuer介面定義了返回一個driver.Value的方式 ```Go type Valuer interface { Value() (Value, error) } ``` 很多型別都實現了這個Value方法,用來自身與driver.Value的轉化。 透過上面的講解,你應該對於驅動的開發有了一個基本的瞭解,一個驅動只要實現了這些介面就能完成增刪查改等基本操作了,剩下的就是與相應的資料庫進行資料互動等細節問題了,在此不再贅述。 ## database/sql database/sql在database/sql/driver提供的介面基礎上定義了一些更高階的方法,用以簡化資料庫操作,同時內部還建議性地實現一個conn pool。 ```Go type DB struct { driver driver.Driver dsn string mu sync.Mutex // protects freeConn and closed freeConn []driver.Conn closed bool } ``` 我們可以看到Open函式返回的是DB物件,裡面有一個freeConn,它就是那個簡易的連線池。它的實現相當簡單或者說簡陋,就是當執行`db.prepare` -> `db.prepareDC`的時候會`defer dc.releaseConn`,然後呼叫`db.putConn`,也就是把這個連線放入連線池,每次呼叫`db.conn`的時候會先判斷freeConn的長度是否大於0,大於0說明有可以複用的conn,直接拿出來用就是了,如果不大於0,則建立一個conn,然後再返回之。 ## links * [目錄]() * 上一節: [訪問資料庫](<05.0.md>) * 下一節: [使用MySQL資料庫](<05.2.md>)