關於 Go 的標准庫 database/sql 和 sqlx
database/sql 是 Go 操作數據庫的標准庫之一,它提供了一系列接口方法,用於訪問數據庫(mysql,sqllite,oralce,postgresql),它並不會提供數據庫特有的方法,那些特有的方法交給數據庫驅動去實現
而通常在工作中,我們更多的是用 https://github.com/jmoiron/sqlx 包來操作數據庫,sqlx 是基於標准庫 sql 的擴展,並且我們可以通過 sqlx 操作各種類型的數據,如將查詢的數據轉為結構體等
github 地址:
- https://github.com/go-sql-driver/mysql
- https://github.com/jmoiron/sqlx
安裝:
go get "github.com/go-sql-driver/mysql" go get "github.com/jmoiron/sqlx"
sqlx 庫提供了一些類型,掌握這些類型的用法非常的重要
1)DB(數據庫對象)
sql.DB 類型代表了數據庫,其它語言操作數據庫的時候,需要創建一個連接,對於 Go 而言則是需要創建一個數據庫類型,它不是數據庫連接,Go 中的連接來自內部實現的連接池,連接的建立是惰性的,連接將會在操作的時候,由連接池創建並維護
使用 sql.Open 函數創建數據庫類型,第一個是數據庫驅動名,第二個是連接信息的字符串
var Db *sqlx.DB db, err := sqlx.Open("mysql","username:password@tcp(ip:port)/database?charset=utf8") Db = db
2)Results 和 Result(結果集)
新增、更新、刪除;和查詢所用的方法不一樣,所有返回的類型也不同
- Result 是 新增、更新、刪除時返回的結果集
- Results 是查詢數據庫時的結果集,sql.Rows 類型表示查詢返回多行數據的結果集,sql.Row 則表示單行查詢的結果集
3)Statements(語句)
sql.Stmt 類型表示 sql 語句,例如 DDL,DML 等類似的 sql 語句,可以當成 prepare 語句構造查詢,也可以直接使用 sql.DB 的函數對其操作
實踐部分(數據庫CURD)
數據庫建表
以下所有 demo 都以下表結構作為基礎
CREATE TABLE `userinfo` ( `uid` INT(10) NOT NULL AUTO_INCREMENT, `create_time` datetime DEFAULT NULL, `username` VARCHAR(64) DEFAULT NULL, `password` VARCHAR(32) DEFAULT NULL, `department` VARCHAR(64) DEFAULT NULL, `email` varchar(64) DEFAULT NULL, PRIMARY KEY (`uid`) )ENGINE=InnoDB DEFAULT CHARSET=utf8;
Exec() 方法使用(新增、修改、刪除)
func (db *DB) Exec(query string, args ...interface{}) (Result, error)
Exec 和 MustExec 從連接池中獲取一個連接然后指向對應的 query 操作,對於不支持 ad-hoc query execution 的驅動,在操作執行的背后會創建一個 prepared statement,在結果返回前,這個 connection 會返回到連接池中
需要注意的是,不同的數據庫,使用的占位符不同,mysql 采用 ? 作為占位符
- Mysql 使用 ?
- PostgreSQL 使用 1,1,2 等等
- SQLLite 使用 ? 或 $1
- Oracle 使用 :name (注意有冒號)
demo:定義了 4 個函數,分別是 連接數據庫,插入數據,更新數據,刪除數據
關於 下面數據庫操作的幾個小知識點
- 插入數據后可以通過 LastInsertId() 方法獲取插入數據的主鍵 id
- 通過 RowsAffected 可以獲取受影響的行數
- 通過 Exec() 方法插入數據,返回的結果是 sql.Result 類型
package main import ( "fmt" _ "github.com/go-sql-driver/mysql" "github.com/jmoiron/sqlx" ) var ( userName string = "chenkai" password string = "chenkai" ipAddrees string = "192.168.0.115" port int = 3306 dbName string = "test" charset string = "utf8" ) func connectMysql() (*sqlx.DB) { dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s", userName, password, ipAddrees, port, dbName, charset) Db, err := sqlx.Open("mysql", dsn) if err != nil { fmt.Printf("mysql connect failed, detail is [%v]", err.Error()) } return Db } func addRecord(Db *sqlx.DB) { for i:=0; i<2; i++ { result, err := Db.Exec("insert into userinfo values(?,?,?,?,?,?)",0, "2019-07-06 11:45:20", "johny", "123456", "技術部", "123456@163.com") if err != nil { fmt.Printf("data insert faied, error:[%v]", err.Error()) return } id, _ := result.LastInsertId() fmt.Printf("insert success, last id:[%d]\n", id) } } func updateRecord(Db *sqlx.DB){ //更新uid=1的username result, err := Db.Exec("update userinfo set username = 'anson' where uid = 1") if err != nil { fmt.Printf("update faied, error:[%v]", err.Error()) return } num, _ := result.RowsAffected() fmt.Printf("update success, affected rows:[%d]\n", num) } func deleteRecord(Db *sqlx.DB){ //刪除uid=2的數據 result, err := Db.Exec("delete from userinfo where uid = 2") if err != nil { fmt.Printf("delete faied, error:[%v]", err.Error()) return } num, _ := result.RowsAffected() fmt.Printf("delete success, affected rows:[%d]\n", num) } func main() { var Db *sqlx.DB = connectMysql() defer Db.Close() addRecord(Db) updateRecord(Db) deleteRecord(Db) } 運行結果: API server listening at: 127.0.0.1:59899 insert success, last id:[1] insert success, last id:[2] update success, affected rows:[1] delete success, affected rows:[1]
Query() 方法使用(查詢單個字段數據)
func (db *DB) Query(query string, args ...interface{}) (*Rows, error)
Query() 方法返回的是一個 sql.Rows 類型的結果集
也可以用來查詢多個字段的數據,不過需要定義多個字段的變量進行接收
迭代后者的 Next() 方法,然后使用 Scan() 方法給對應類型變量賦值,以便取出結果,最后再把結果集關閉(釋放連接)
package main import ( "fmt" _ "github.com/go-sql-driver/mysql" "github.com/jmoiron/sqlx" ) var ( userName string = "chenkai" password string = "chenkai" ipAddrees string = "192.168.0.115" port int = 3306 dbName string = "test" charset string = "utf8" ) func connectMysql() (*sqlx.DB) { dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s", userName, password, ipAddrees, port, dbName, charset) Db, err := sqlx.Open("mysql", dsn) if err != nil { fmt.Printf("mysql connect failed, detail is [%v]", err.Error()) } return Db } func queryData(Db *sqlx.DB) { rows, err := Db.Query("select * from userinfo") if err != nil { fmt.Printf("query faied, error:[%v]", err.Error()) return } for rows.Next() { //定義變量接收查詢數據 var uid int var create_time, username, password, department, email string err := rows.Scan(&uid, &create_time, &username, &password, &department, &email) if err != nil { fmt.Println("get data failed, error:[%v]", err.Error()) } fmt.Println(uid, create_time, username, password, department, email) } //關閉結果集(釋放連接) rows.Close() } func main() { var Db *sqlx.DB = connectMysql() defer Db.Close() queryData(Db) } 運行結果: 1 2019-07-06 11:45:20 anson 123456 技術部 123456@163.com 3 2019-07-06 11:45:20 johny 123456 技術部 123456@163.com 4 2019-07-06 11:45:20 johny 123456 技術部 123456@163.com
Get() 方法使用
func (db *DB) Get(dest interface{}, query string, args ...interface{}) error
是將查詢到的一條記錄,保存到結構體
結構體的字段名首字母必須大寫,不然無法尋址

package main import ( "fmt" _ "github.com/go-sql-driver/mysql" "github.com/jmoiron/sqlx" ) var ( userName string = "chenkai" password string = "chenkai" ipAddrees string = "192.168.0.115" port int = 3306 dbName string = "test" charset string = "utf8" ) func connectMysql() (*sqlx.DB) { dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s", userName, password, ipAddrees, port, dbName, charset) Db, err := sqlx.Open("mysql", dsn) if err != nil { fmt.Printf("mysql connect failed, detail is [%v]", err.Error()) } return Db } func getData(Db *sqlx.DB) { type userInfo struct { Uid int `db:"uid"` UserName string `db:"username"` CreateTime string `db:"create_time"` Password string `db:"password"` Department string `db:"department"` Email string `db:"email"` } //初始化定義結構體,用來存放查詢數據 var userData *userInfo = new(userInfo) err := Db.Get(userData,"select *from userinfo where uid = 1") if err != nil { fmt.Printf("query faied, error:[%v]", err.Error()) return } //打印結構體內容 fmt.Println(userData.Uid, userData.CreateTime, userData.UserName, userData.Password, userData.Department, userData.Email) } func main() { var Db *sqlx.DB = connectMysql() defer Db.Close() getData(Db) } 運行結果: 1 2019-07-06 11:45:20 anson 123456 技術部 123456@163.com
Select() 方法使用
func (db *DB) Select(dest interface{}, query string, args ...interface{}) error
將查詢的多條記錄,保存到結構體的切片中
結構體的字段名首字母必須大寫,不然無法尋址

package main import ( "fmt" _ "github.com/go-sql-driver/mysql" "github.com/jmoiron/sqlx" ) var ( userName string = "chenkai" password string = "chenkai" ipAddrees string = "192.168.0.115" port int = 3306 dbName string = "test" charset string = "utf8" ) func connectMysql() (*sqlx.DB) { dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s", userName, password, ipAddrees, port, dbName, charset) Db, err := sqlx.Open("mysql", dsn) if err != nil { fmt.Printf("mysql connect failed, detail is [%v]", err.Error()) } return Db } func selectData(Db *sqlx.DB) { type userInfo struct { Uid int `db:"uid"` UserName string `db:"username"` CreateTime string `db:"create_time"` Password string `db:"password"` Department string `db:"department"` Email string `db:"email"` } //定義結構體切片,用來存放多條查詢記錄 var userInfoSlice []userInfo err := Db.Select(&userInfoSlice,"select * from userinfo") if err != nil { fmt.Printf("query faied, error:[%v]", err.Error()) return } //遍歷結構體切片 for _, userData := range userInfoSlice { fmt.Println(userData.Uid, userData.CreateTime, userData.UserName, userData.Password, userData.Department, userData.Email) } } func main() { var Db *sqlx.DB = connectMysql() defer Db.Close() selectData(Db) } 運行結果: 1 2019-07-06 11:45:20 anson 123456 技術部 123456@163.com 3 2019-07-06 11:45:20 johny 123456 技術部 123456@163.com 4 2019-07-06 11:45:20 johny 123456 技術部 123456@163.com
重點內容回顧
sql.DB
- 當我們調用 sqlx.Open() 可以獲取一個 sql.DB 類型對象,sqlx.DB 是數據庫的抽象,切記它不是數據庫連接,sqlx.Open() 只是驗證數據庫參數,並沒有創建數據庫連接
- sqlx.DB 擁有一系列與數據庫交互的方法(Exec,Query,Get,Select ...),同時也管理維護着一個數據庫連接池,並且對於多個 goroutine 也是安全的
- sqlx.DB 表示是數據庫的抽象,因此有幾個數據庫就要創建幾個 sqlx.DB 類型對象,因為它要維護一個連接池,因此不需要頻繁的創建和銷毀
連接池
只用 sqlx.Open() 函數創建連接池,此時只是初始化了連接池,並沒有連接數據庫,連接都是惰性的,只有調用 sqlx.DB 的方法時,此時才真正用到了連接,連接池才會去創建連接,連接池很重要,它直接影響着你的程序行為
連接池的工作原理也非常簡單,當調用 sqlx.DB 的方法時,會首先去向連接池請求要一個數據庫連接,如果連接池有空閑的連接,則返回給方法中使用,否則連接池將創建一個新的連接給到方法中使用;一旦將數據庫連接給到了方法中,連接就屬於方法了。方法執行完畢后,要不把連接所屬權還給連接池,要不傳遞給下一個需要數據庫連接的方法中,最后都使用完將連接釋放回到連接池中
請求數據庫連接的方法有幾個,執行完畢處理連接的方式也不同:
- DB.Ping() 使用完畢后會馬上把連接返回給連接池
- DB.Exec() 使用完畢后會馬上把連接返回給連接池,但是它返回的 Result 對象還保留着連接的引用,當后面的代碼需要處理結果集的時候,連接將會被重新啟用
- DB.Query() 調用完畢后將連接傳遞給 sql.Rows 類型,當后者迭代完畢或者顯示的調用 Close() 方法后,連接將會被釋放到連接池
- DB.QueryRow() 調用完畢后將連接傳遞給 sql.Row 類型,當 Scan() 方法調用完成后,連接將會被釋放到連接池
- DB.Begin() 調用完畢后將連接傳遞給 sql.Tx 類型對象,當 Commit() 或 Rollback() 方法調用后釋放連接
每個連接都是惰性的,如果驗證 sqlx.Open() 調用之后,sqlx.DB 類型對象可用呢?通過 DB.Ping() 方法來初始化
func (db *DB) Ping() error
demo:需要知道,當調用了 Ping() 方法后,連接池一定會初始化一個數據庫連接
package main import ( "fmt" _ "github.com/go-sql-driver/mysql" "github.com/jmoiron/sqlx" ) var ( userName string = "chenkai" password string = "chenkai" ipAddrees string = "192.168.0.115" port int = 3306 dbName string = "test" charset string = "utf8" ) func connectMysql() (*sqlx.DB) { dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s", userName, password, ipAddrees, port, dbName, charset) Db, err := sqlx.Open("mysql", dsn) if err != nil { fmt.Printf("mysql connect failed, detail is [%v]", err.Error()) } return Db } func ping(Db *sqlx.DB) { err := Db.Ping() if err != nil { fmt.Println("ping failed") } else { fmt.Println("ping success") } } func main() { var Db *sqlx.DB = connectMysql() defer Db.Close() ping(Db) } 運行結果: ping success
連接池配置
DB.SetMaxIdleConns(n int) 設置連接池中的保持連接的最大連接數。默認也是0,表示連接池不會保持數據庫連接的狀態:即當連接釋放回到連接池的時候,連接將會被關閉。這會導致連接再連接池中頻繁的關閉和創建,我們可以設置一個合理的值。
DB.SetMaxOpenConns(n int) 設置打開數據庫的最大連接數。包含正在使用的連接和連接池的連接。如果你的方法調用 需要用到一個連接,並且連接池已經沒有了連接或者連接數達到了最大連接數。此時的方法調用將會被 block,直到有可用的連接才會返回。設置這個值可以避免並發太高導致連接 mysql 出現 too many connections 的錯誤。該函數的默認設置是0,表示無限制
DB.SetConnMaxLifetime(d time.Duration) 設置連接可以被使用的最長有效時間,如果過期,連接將被拒絕
數據庫連接重試次數
sqlx 中的方法幫我們做了很多事情,我們不用考慮連接失敗的情況,當調用方法進行數據庫操作的時候,如果連接失敗,sqlx 中的方法會幫我們處理,它會自動連接2次,這個如果查看源碼中我們可以看到如下的代碼:
其它的方法中也有這種處理,代碼中變量maxBadConnRetries小時如果連接失敗嘗試的次數,默認是 2
// ExecContext executes a query without returning any rows. // The args are for any placeholder parameters in the query. func (db *DB) ExecContext(ctx context.Context, query string, args ...interface{}) (Result, error) { var res Result var err error for i := 0; i < maxBadConnRetries; i++ { res, err = db.exec(ctx, query, args, cachedOrNewConn) if err != driver.ErrBadConn { break } } if err == driver.ErrBadConn { return db.exec(ctx, query, args, alwaysNewConn) } return res, err }
參考鏈接:https://www.cnblogs.com/zhaof/p/8509164.html
ending ~