Go中提供了database包,database包下有sql.driver。該包用來定義操作數據庫的接口,這保證了無論使用哪種數據庫,操作都是相同的。但Go並沒有提供連接數據庫的driver,如果需要操作數據庫,需要使用第三方的driver包。
因此以mysql為例:
go get github.com/Go-SQL-Driver/MySQL
安裝成功之后導入方式如下:
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
"os"
)
Go雖然提供一些方法,但是並不會提供一些特有的方法,但是眾所周知會有些特別的方法需要交給數據庫的驅動去實現。
選擇使用匿名導入包,會讓被導入的包編譯到可執行文件中,通常來講,導入包就可以使用包中的數據和方法。但是對於數據操作來講,我們不應該直接使用導入的驅動包所提供的方法,而應該使用sql.DB對象所提供的統一的方法。因此在導入MySQL的區動時,使用匿名導入的方式。在導入一個數據庫驅動后,該驅動會自行初始化並注冊到Golang的database/sql上下文中,這樣就能使用database/sql包所提供的方法來訪問數據庫了。
一, 連接數據庫
sql中的Open()函數,原型如下所示。
func Open(driverName, dataSourceName string) (*DB, error)
driverName: 使用的驅動名,就是這個名字其實就是數據驅動注冊到database/sql時所需使用的名字。
dataSourceName:數據庫連接信息。它包含數據庫的用戶名、密碼、數據庫注漿機及其需要連接的數據名等信息
db, err := sql.Open("mysql", "用戶名:密碼@tcp(ip:端口)/數據庫?charset=utf8")
var db *sql.DB // 創建連接對象
func init() {
db, _ := sql.Open("mysql", "disk:disk@tcp(127.0.0.1:3306)/fileserver?charset=utf8")
db.SetMaxOpenConns(1000) // 同時連接數
err := db.Ping() // 測試連接是否成功
if err != nil {
fmt.Println("error to link sql:" + err.Error())
os.Exit(1)
}
}
如上面代碼所示:一般將數據連接的方法寫在init函數中,保證到爆時候立即被執行
二,數據的增刪改查
直接調用DB對象的Exec()方法如下所示:
func (db *DB) Exec(query string, args ...interface{}) (Result, error)
通過db.Exec()插入數據,通過返回的err可知插入失敗的原因,通過返回的結果可以進一步查詢本次插入數據庫所影響的行數(RowsAffected)和最后插入的ID(如果數據庫支持查詢最后插入ID)。
Exec()方法使用方式如下:
result, err := db.Exec("INSERT INTO userinfo (username, departname, created) VALUES (?,?,?)","Steven", "北京", "2020-09-16")
預編譯語句(PreparedStatement)提供了諸多好處。PreparedStatement可以實現自定義參數的查詢,通常會比手動拼接字符SQL串高效;還可以防止SQL注入攻擊。因此一般情況下用PreparedStatement和Exec()完成對INSERT、UPDATE、DELETE操作。使用DB對象的Prepare()方法獲得預編譯對象stmt,然后調用Exec()方法,語句如下。
func (db *DB) Prepare(query string) (*Stmt, error)
使用如下:
stmt, err := db.Prepare("INSERT userinfo SET username=?,departname=?,created=?")
result,err : stmt.Exec("zhouli", "開發部", "2020-05-16")
獲取影響數據庫的行數,可以根據該數值判斷是否操作(插入、刪除或修改)成功。
count, err := result.RowAffected()
// 注意SQL的注入攻擊
stm, err := mydb.DBConn().Prepare("insert ignore into tab_file(`file_sha1`, `file_name`, `file_size`)" +
"`file_addr`,`status`values(?,?,?,?,1)")
if err != nil {
fmt.Println("Failed to Prepare statement, err:" + err.Error())
return false
}
defer stm.Close()
ret, err := stm.Exec(filehash, filename, filesize,fileaddr)
if err != nil {
fmt.Println(err.Error())
return false
}
// 檢測受到更新影響的行數
if rf,err := ret.RowsAffected();nil == err {
if rf <= 0 {
fmt.Printf("插入失敗")
}
return true
}
三, 查詢數據
數據庫查詢步驟如下:
1, 調用db.Query()方法執行SQL語句,此方法返回一個Rows作為查詢結果。語法如下:
func (db *DB) Query(query string,args ...interface{}) (*Rows, error)
2, 將rows.Next()方法的返回值作為for循環的條件,迭代查詢數據,語法如下所示。
func (rs *Rows) Next() bool
3, 再循環中,通過rows.Scan()方法讀取每一行數據,語法如下所示:
func (rs *Rows) Scan(dest ...interface{}) error
4, 調用db.Close()關閉查詢
通過QueryRow()方法查詢單挑數據,語法如下所示:
func (db *DB) QueryRow(query string,args ...interface{}) *Row
整體步驟如下所示:
var username,departname, created string
err := db.QueryRow("SELECT username,departname, created FORM user_info WHERE uid=?",3).Scan(&username, &departname, &created)
查詢多行數據:
stmt, err := db.Prepare("SELECT * FORM user_info WHERE uid<?")
rows, err := stmt.Query(10)
user := new(UserTable)
for rows.Next() {
err := row.Scan(&user.Uid, &user.Username, &user.Department, &user.Created)
if err != nil {
panic(err)
continue
}
fmt.Println(*user)
}
rows.Scan()方法參數的順序很重要,必須和查詢結果的column相對應(數量和順序都要相對應),不然會造成數據讀取的錯位
四, 注意點
每次db.Query()操作后,都建議調用rows.Close()
因為db.Query()會從數據庫連接池中獲取一個連接,這個底層連接在結果集rows未關閉前會被標記為繁忙狀態。當遍歷到最后一條記錄,會發生一個內部錯誤EOF錯誤,自動調用row.Close()。
但如果出現異常,提前退出循環,rows不會被關閉,連接不會回到連接池中,連接不會關閉,則詞里阿傑會一直被占用。因此通常使用defer rows.Close()來確保數據庫連接可以被正確的放回到連接池中。
但是我們從源碼中可以找到rows.Close()操作是冪等操作,而一個冪等操作最大的特點就是:其任意多次指定所產生的的影響與一次執行的影響相同。所以即便對已關閉的row再執行close也是沒事的。