Go操作MySQL
MySQL是業界常用的關系型數據庫,本文介紹了Go語言如何操作MySQL數據庫。
Go語言中的database/sql
包提供了保證SQL或類SQL數據庫的泛用接口,並不提供具體的數據庫驅動。使用database/sql
包時必須注入(至少)一個數據庫驅動。
我們常用的數據庫基本上都有完整的第三方實現。例如:MySQL驅動
一、創建表和庫
create database go_mysql_testmysql;
CREATE TABLE `user` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(20) DEFAULT '',
`age` INT(11) DEFAULT '0',
PRIMARY KEY(`id`)
)ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
mysql> desc user;
+-------+-------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+----------------+
| id | bigint(20) | NO | PRI | NULL | auto_increment |
| name | varchar(20) | YES | | | |
| age | int(11) | YES | | 0 | |
+-------+-------------+------+-----+---------+----------------+
3 rows in set (0.01 sec)
二、下載依賴
go get -u github.com/go-sql-driver/mysql
三、使用MySQL驅動
func Open(driverName, dataSourceName string) (*DB, error)
Open打開一個dirverName指定的數據庫,dataSourceName指定數據源,一般至少包括數據庫文件名和其它連接必要的信息。
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
)
func main() {
// DSN:Data Source Name
dsn := "user:password@tcp(127.0.0.1:3306)/dbname"
db, err := sql.Open("mysql", dsn)
if err != nil {
panic(err)
}
defer db.Close() // 注意這行代碼要寫在上面err判斷的下面
}
四、初始化連接
Open函數可能只是驗證其參數格式是否正確,實際上並不創建與數據庫的連接。如果要檢查數據源的名稱是否真實有效,應該調用Ping方法。
返回的DB對象可以安全地被多個goroutine並發使用,並且維護其自己的空閑連接池。因此,Open函數應該僅被調用一次,很少需要關閉這個DB對象。
package main
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
)
// 定義一個全局對象db
var Db *sql.DB
// 定義一個初始化數據庫的函數
func InitDb() (err error) {
// DSN:Data Source Name
dsn := "root:@tcp(127.0.0.1:3306)/go_mysql_test?charset=utf8mb4&parseTime=True"
// 不會校驗賬號密碼是否正確
// 注意!!!,這里不要使用:=,我是是給全局變量賦值,然后在main中使用全局變量db
Db, err = sql.Open("mysql", dsn)
if err != nil {
return err
}
// 嘗試與數據庫建立連接(校驗dsn是否正確)
err = Db.Ping()
if err != nil {
return err
}
// 根據實際場景設置
Db.SetConnMaxLifetime(time.Second * 10) // 連接存活時間
Db.SetMaxOpenConns(200) // 最大連接數
Db.SetMaxIdleConns(10) // 最大空閑連接數
return nil
}
func main() {
// database/sql
err := InitDb() // 調用輸出數據庫的函數
if err != nil{
fmt.Printf("init db failed, err: %v\n", err)
return
}
// 做完檢查之后確保db部位nil,在關閉資源
defer Db.Close() // 函數直接完就會被調用,要放在外面
fmt.Printf("init db success db%v\n", Db)
}
其中sql.DB
是表示連接的數據庫對象(結構體實例),它保存了連接數據庫相關的所有信息。它內部維護着一個具有零到多個底層連接的連接池,它可以安全地被多個goroutine同時使用。
4.1 SetMaxOpenConns
func (db *DB) SetMaxOpenConns(n int)
SetMaxOpenConns
設置與數據庫建立連接的最大數目。 如果n大於0且小於最大閑置連接數,會將最大閑置連接數減小到匹配最大開啟連接數的限制。 如果n<=0,不會限制最大開啟連接數,默認為0(無限制)。
4.2 SetMaxIdleConns
func (db *DB) SetMaxIdleConns(n int)
SetMaxIdleConns設置連接池中的最大閑置連接數。 如果n大於最大開啟連接數,則新的最大閑置連接數會減小到匹配最大開啟連接數的限制。 如果n<=0,不會保留閑置連接。
五、driver
v 表示value
T 表示類型
m 表示一個方法
f 表示函數
init
import (
"context"
"database/sql"
"database/sql/driver"
"net"
"sync"
)
func init() {
sql.Register("mysql", &MySQLDriver{})
}
// MySQLDriver is exported to make the driver directly accessible.
// In general the driver is used via the database/sql package.
type MySQLDriver struct{}
var (
driversMu sync.RWMutex
drivers = make(map[string]driver.Driver)
)
func Register(name string, driver driver.Driver) {
driversMu.Lock() // 讀寫鎖 map並發不安全
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
}
六、查詢
為了方便查詢,我們事先定義好一個結構體來存儲user表的數據。
type user struct {
id int
age int
name string
}
6.1 單行查詢
單行查詢db.QueryRow()
執行一次查詢,並期望返回最多一行結果(即Row)。QueryRow總是返回非nil的值,直到返回值的Scan方法被調用時,才會返回被延遲的錯誤。(如:未找到結果)
func (db *DB) QueryRow(query string, args ...interface{}) *Row
具體示例代碼:
package main
import "fmt"
/*
@author RandySun
@create 2021-08-30-8:32
*/
func QueryRowDemo(id int) {
// 獲取單條數據
sqlStr := "select id, name, age from user where id=?"
var u User
// 非常重要:確保QueryRow之后調用Scan方法,否則持有數據的連接不會被釋放
err := Db.QueryRow(sqlStr, id).Scan(&u.Id, &u.Name, &u.Age)
if err != nil {
fmt.Printf("scan failed, err: %v\n", err)
return
}
fmt.Printf("id: %d, name:%s, age:%d\n", u.Id, u.Name, u.Age)
}
func main() {
//database/sql
err := InitDb() // 調用輸出數據庫的函數
if err != nil{
fmt.Printf("init db failed, err: %v\n", err)
return
}
fmt.Printf("init db success db%v\n", Db)
//查詢單挑數據
QueryRowDemo(1)
}
id: 1, name:Randy, age:18
6.2 多行查詢
多行查詢db.Query()
執行一次查詢,返回多行結果(即Rows),一般用於執行select命令。參數args表示query中的占位參數。
func (db *DB) Query(query string, args ...interface{}) (*Rows, error)
具體示例代碼:
func QueryMultiRowDemo(id int) {
sqlStr := "select id, name, age from user where id > ?"
userList := make([]User, 0, 120)
//userList := []User{1,"s", 3}
rows, err := Db.Query(sqlStr, id)
if err != nil {
return
}
// 非常重要,關閉rows釋放持有的數據庫連接
defer rows.Close()
fmt.Println(rows, 5555555555555)
// 循環讀取結果集中的數據
for rows.Next() {
var u User
err := rows.Scan(&u.Id, &u.Name, &u.Age)
if err != nil {
fmt.Printf("scan failed, err:%v\n", err)
return
}
//fmt.Println(u)
userList = append(userList, u)
fmt.Printf("id: %d, name: %s, age: %d\n", u.Id, u.Name, u.Age)
}
fmt.Printf("%#v", userList)
}
func main() {
//database/sql
err := InitDb() // 調用輸出數據庫的函數
if err != nil{
fmt.Printf("init db failed, err: %v\n", err)
return
}
fmt.Printf("init db success db%v\n", Db)
//查詢單挑數據
QueryMultiRowDemo(1)
}
id: 2, name: Jack, age: 30
id: 3, name: Barry, age: 200
id: 4, name: RandySun, age: 18
id: 6, name: RandySun, age: 18
id: 8, name: RandySun2, age: 18
七、插入數據
插入、更新和刪除操作都使用Exec
方法。
func (db *DB) Exec(query string, args ...interface{}) (Result, error)
Exec執行一次命令(包括查詢、刪除、更新、插入等),返回的Result是對已執行的SQL命令的總結。參數args表示query中的占位參數。
具體插入數據示例代碼如下:
package main
import "fmt"
/*
@author RandySun
@create 2021-08-30-22:29
*/
func insertRowDemo(name string, age int) {
sqlStr := "insert into user(name, age) values(?,?)"
ret, err := Db.Exec(sqlStr, name, age)
if err != nil {
fmt.Printf("insert failed , err:%v\n", err)
return
}
theId, err := ret.LastInsertId() // 新插入數據的id
if err != nil {
fmt.Printf("get lastinsert Id failed, err: %v\n,", err)
}
fmt.Println("insert success, the id is:", theId)
}
func main() {
//database/sql
err := InitDb() // 調用輸出數據庫的函數
if err != nil{
fmt.Printf("init db failed, err: %v\n", err)
return
}
fmt.Printf("init db success db%v\n", Db)
// 插入數據
insertRowDemo("RandySun", 18)
}
八、更新數據
具體更新數據示例代碼如下:
package main
import "fmt"
/*
@author RandySun
@create 2021-08-30-22:36
*/
func updateRowDemo(age int, id int) {
sqlStr := "update user set age=? where id=?"
ret, err := Db.Exec(sqlStr, age, id)
if err != nil {
fmt.Printf("update failed, err: %v\n", err)
}
n, err := ret.RowsAffected() // 操作收影響的行
if err != nil {
fmt.Printf("get RowsAffected failed: %v\n", err)
return
}
fmt.Printf("update success, affected rows: %d\n", n)
}
func main() {
//database/sql
err := InitDb() // 調用輸出數據庫的函數
if err != nil{
fmt.Printf("init db failed, err: %v\n", err)
return
}
fmt.Printf("init db success db%v\n", Db)
// 插入數據
updateRowDemo(19, 5)
}
九、刪除數據
具體刪除數據的示例代碼如下:
package main
import "fmt"
/*
@author RandySun
@create 2021-08-30-22:36
*/
func deleteRowDemo(id int) {
sqlStr := "delete from user where id=?"
ret, err := Db.Exec(sqlStr, id)
if err != nil {
fmt.Printf("delete failed, err: %v\n", err)
return
}
n, err := ret.RowsAffected() // 獲取操作影響的行數
if err != nil {
fmt.Printf("get RowsAffected fail, err:%v\n", err)
}
fmt.Printf("delete success, affected rows: %v \n", n)
}
func main() {
//database/sql
err := InitDb() // 調用輸出數據庫的函數
if err != nil{
fmt.Printf("init db failed, err: %v\n", err)
return
}
fmt.Printf("init db success db%v\n", Db)
// 插入數據
deleteRowDemo(5)
}
十、MySQL預處理
10.1 什么是預處理?
普通SQL語句執行過程:
- 客戶端對SQL語句進行占位符替換得到完整的SQL語句。
- 客戶端發送完整SQL語句到MySQL服務端
- MySQL服務端執行完整的SQL語句並將結果返回給客戶端。
預處理執行過程:
- 把SQL語句分成兩部分,命令部分與數據部分。
- 先把命令部分發送給MySQL服務端,MySQL服務端進行SQL預處理。
- 然后把數據部分發送給MySQL服務端,MySQL服務端對SQL語句進行占位符替換。
- MySQL服務端執行完整的SQL語句並將結果返回給客戶端。
10.2 為什么要預處理?
- 優化MySQL服務器重復執行SQL的方法,可以提升服務器性能,提前讓服務器編譯,一次編譯多次執行,節省后續編譯的成本。
- 避免SQL注入問題。
10.3 Go實現MySQL預處理
database/sql
中使用下面的Prepare
方法來實現預處理操作。
func (db *DB) Prepare(query string) (*Stmt, error)
Prepare
方法會先將sql語句發送給MySQL服務端,返回一個准備好的狀態用於之后的查詢和命令。返回值可以同時執行多個查詢和命令。
查詢操作的預處理示例代碼如下:
package main
import "fmt"
/*
@author RandySun
@create 2021-08-30-22:56
*/
// 預處理查詢示例
func prepareQueryDemo(id int) {
sqlStr := "select id, name, age from user where id > ?"
stmt, err := Db.Prepare(sqlStr)
if err != nil {
fmt.Printf("preare failed, err: %v\n", err)
return
}
defer stmt.Close()
rows, err := stmt.Query(id)
if err != nil {
fmt.Printf("query failed, err:%v\n", err)
return
}
defer rows.Close()
// 循環讀取結果集中的數據
for rows.Next() {
var u User
err := rows.Scan(&u.Id, &u.Name, &u.Age)
if err != nil {
fmt.Printf("scan failed, err:%v\n", err)
return
}
fmt.Printf("id:%d, name: %s, age:%d", u.Id, u.Name, u.Age)
}
}
func main() {
//database/sql
err := InitDb() // 調用輸出數據庫的函數
if err != nil{
fmt.Printf("init db failed, err: %v\n", err)
return
}
fmt.Printf("init db success db%v\n", Db)
///預處理查詢示例
prepareQueryDemo(3)
}
插入、更新和刪除操作的預處理十分類似,這里以插入操作的預處理為例:
// 預處理插入示例
func prepareInserDemo(name string, age int) {
sqlStr := "insert into user(name, age) values(?, ?)"
stmt, err := Db.Prepare(sqlStr)
if err != err {
fmt.Printf("prepare failed, err:%v\n", err)
}
defer stmt.Close()
ret, err := stmt.Exec(name, age)
if err != nil {
fmt.Printf("insert failed, err: %d\n", err)
return
}
n, err := ret.RowsAffected()
if err != nil {
fmt.Printf("get RowsAffected failed err:%d", err)
}
fmt.Printf("insert sucesss n:%d", n)
}
func main() {
//database/sql
err := InitDb() // 調用輸出數據庫的函數
if err != nil{
fmt.Printf("init db failed, err: %v\n", err)
return
}
fmt.Printf("init db success db%v\n", Db)
///預處理插入
prepareInserDemo("RandySun", 18)
}
10.4 SQL注入問題
我們任何時候都不應該自己拼接SQL語句!
這里我們演示一個自行拼接SQL語句的示例,編寫一個根據name字段查詢user表的函數如下:
package main
import "fmt"
/*
@author RandySun
@create 2021-08-30-23:12
*/
func sqlInjectDemo(name string) {
sqlStr := fmt.Sprintf("select id, name, age from user where name='%s'", name)
fmt.Println("SQL:", sqlStr)
var u User
err := Db.QueryRow(sqlStr).Scan(&u.Id, &u.Name, &u.Age)
if err != nil {
fmt.Println("exec failed, err:", err)
return
}
fmt.Println("user: %#v", u)
}
此時以下輸入字符串都可以引發SQL注入問題:
func main() {
//database/sql
err := InitDb() // 調用輸出數據庫的函數
if err != nil{
fmt.Printf("init db failed, err: %v\n", err)
return
}
fmt.Printf("init db success db%v\n", Db)
// sql注入
sqlInjectDemo("xxx' or 1=1#")
sqlInjectDemo("xxx' union select * from user #")
sqlInjectDemo("xxx' and (select count(*) from user) <4 #")
}
補充:不同的數據庫中,SQL語句使用的占位符語法不盡相同。
數據庫 | 占位符語法 |
---|---|
MySQL | ? |
PostgreSQL | $1 , $2 等 |
SQLite | ? 和$1 |
Oracle | :name |
十一、Go實現MySQL事務
什么是事務?
事務:一個最小的不可再分的工作單元;通常一個事務對應一個完整的業務(例如銀行賬戶轉賬業務,該業務就是一個最小的工作單元),同時這個完整的業務需要執行多次的DML(insert、update、delete)語句共同聯合完成。A轉賬給B,這里面就需要執行兩次update操作。
在MySQL中只有使用了Innodb
數據庫引擎的數據庫或表才支持事務。事務處理可以用來維護數據庫的完整性,保證成批的SQL語句要么全部執行,要么全部不執行。
事務的ACID
通常事務必須滿足4個條件(ACID):原子性(Atomicity,或稱不可分割性)、一致性(Consistency)、隔離性(Isolation,又稱獨立性)、持久性(Durability)。
條件 | 解釋 |
---|---|
原子性 | 一個事務(transaction)中的所有操作,要么全部完成,要么全部不完成,不會結束在中間某個環節。事務在執行過程中發生錯誤,會被回滾(Rollback)到事務開始前的狀態,就像這個事務從來沒有執行過一樣。 |
一致性 | 在事務開始之前和事務結束以后,數據庫的完整性沒有被破壞。這表示寫入的資料必須完全符合所有的預設規則,這包含資料的精確度、串聯性以及后續數據庫可以自發性地完成預定的工作。 |
隔離性 | 數據庫允許多個並發事務同時對其數據進行讀寫和修改的能力,隔離性可以防止多個事務並發執行時由於交叉執行而導致數據的不一致。事務隔離分為不同級別,包括讀未提交(Read uncommitted)、讀提交(read committed)、可重復讀(repeatable read)和串行化(Serializable)。 |
持久性 | 事務處理結束后,對數據的修改就是永久的,即便系統故障也不會丟失。 |
事務相關方法
Go語言中使用以下三個方法實現MySQL中的事務操作。 開始事務
func (db *DB) Begin() (*Tx, error)
提交事務
func (tx *Tx) Commit() error
回滾事務
func (tx *Tx) Rollback() error
事務示例
下面的代碼演示了一個簡單的事務操作,該事物操作能夠確保兩次更新操作要么同時成功要么同時失敗,不會存在中間狀態。
package main
import "fmt"
/*
@author RandySun
@create 2021-08-30-23:25
*/
// 事務操作
func transactionDemo() {
tx, err := Db.Begin() //開啟事務
if err != nil {
if tx != nil {
tx.Rollback() // 回滾
}
fmt.Printf("begin trans failed, err:%v\n", err)
return
}
sqlStr1 := "Update user set age=30 where id=?"
ret1, err := tx.Exec(sqlStr1, 2)
if err != nil {
tx.Rollback() // 回滾
fmt.Printf("exec sql1 failed, err:%v\n", err)
return
}
affRow1, err := ret1.RowsAffected()
if err != nil {
tx.Rollback() // 回滾
fmt.Printf("exec ret1.RowsAffected() failed, err:%v\n", err)
return
}
sqlStr2 := "Update user set age=4 where id=?"
ret2, err := tx.Exec(sqlStr2, 3)
if err != nil {
tx.Rollback() // 回滾
fmt.Printf("exec sql2 failed, err:%v\n", err)
return
}
affRow2, err := ret2.RowsAffected()
if err != nil {
tx.Rollback() // 回滾
fmt.Printf("exec ret1.RowsAffected() failed, err:%v\n", err)
return
}
fmt.Println(affRow1, affRow2)
if affRow1 == 1 && affRow2 == 1 {
fmt.Println("事務提交啦...")
tx.Commit() // 提交事務
} else {
tx.Rollback()
fmt.Println("事務回滾啦...")
}
fmt.Println("exec trans success!")
}
func main() {
//database/sql
err := InitDb() // 調用輸出數據庫的函數
if err != nil{
fmt.Printf("init db failed, err: %v\n", err)
return
}
fmt.Printf("init db success db%v\n", Db)
// 事務操作
transactionDemo()
}
0 1
事務回滾啦...
exec trans success!