Go语言操作MySQL
MySQL是業界常用的關系型數據庫,本文介紹了Go語言如何操作MySQL數據庫。
Go操作MySQL
連接
Go語言中的database/sql包提供了保證SQL或類SQL數據庫的泛用接口,并不提供具體的數據庫驅動。使用database/sql包時必須注入(至少)一個數據庫驅動。
我們常用的數據庫基本上都有完整的第三方實現。例如:MySQL驅動
下載依賴
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 Namedsn := "user:password@tcp(127.0.0.1:3306)/dbname"db, err := sql.Open("mysql", dsn)if err != nil {panic(err)}defer db.Close() // 注意這行代碼要寫在上面err判斷的下面 }思考題: 為什么上面代碼中的defer db.Close()語句不應該寫在if err != nil的前面呢?
初始化連接
Open函數可能只是驗證其參數格式是否正確,實際上并不創建與數據庫的連接。如果要檢查數據源的名稱是否真實有效,應該調用Ping方法。
返回的DB對象可以安全地被多個goroutine并發使用,并且維護其自己的空閑連接池。因此,Open函數應該僅被調用一次,很少需要關閉這個DB對象。
接下來,我們定義一個全局變量db,用來保存數據庫連接對象。將上面的示例代碼拆分出一個獨立的initDB函數,只需要在程序啟動時調用一次該函數完成全局變量db的初始化,其他函數中就可以直接使用全局變量db了。(注意下方的注意)
// 定義一個全局對象db var db *sql.DB// 定義一個初始化數據庫的函數 func initDB() (err error) {// DSN:Data Source Namedsn := "user:password@tcp(127.0.0.1:3306)/sql_test?charset=utf8mb4&parseTime=True"// 不會校驗賬號密碼是否正確// 注意!!!這里不要使用:=,我們是給全局變量賦值,然后在main函數中使用全局變量dbdb, err = sql.Open("mysql", dsn)if err != nil {return err}// 嘗試與數據庫建立連接(校驗dsn是否正確)err = db.Ping()if err != nil {return err}return nil }func main() {err := initDB() // 調用輸出化數據庫的函數if err != nil {fmt.Printf("init db failed,err:%v\n", err)return} }其中sql.DB是表示連接的數據庫對象(結構體實例),它保存了連接數據庫相關的所有信息。它內部維護著一個具有零到多個底層連接的連接池,它可以安全地被多個goroutine同時使用。
SetMaxOpenConns
func (db *DB) SetMaxOpenConns(n int)SetMaxOpenConns設置與數據庫建立連接的最大數目。 如果n大于0且小于最大閑置連接數,會將最大閑置連接數減小到匹配最大開啟連接數的限制。 如果n<=0,不會限制最大開啟連接數,默認為0(無限制)。
SetMaxIdleConns
func (db *DB) SetMaxIdleConns(n int)SetMaxIdleConns設置連接池中的最大閑置連接數。 如果n大于最大開啟連接數,則新的最大閑置連接數會減小到匹配最大開啟連接數的限制。 如果n<=0,不會保留閑置連接。
CRUD
建庫建表
我們先在MySQL中創建一個名為sql_test的數據庫
CREATE DATABASE sql_test;進入該數據庫:
use sql_test;執行以下命令創建一張用于測試的數據表:
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;查詢
為了方便查詢,我們事先定義好一個結構體來存儲user表的數據。
type user struct {id intage intname string }單行查詢
單行查詢db.QueryRow()執行一次查詢,并期望返回最多一行結果(即Row)。QueryRow總是返回非nil的值,直到返回值的Scan方法被調用時,才會返回被延遲的錯誤。(如:未找到結果)
func (db *DB) QueryRow(query string, args ...interface{}) *Row具體示例代碼:
// 查詢單條數據示例 func queryRowDemo() {sqlStr := "select id, name, age from user where id=?"var u user// 非常重要:確保QueryRow之后調用Scan方法,否則持有的數據庫鏈接不會被釋放err := db.QueryRow(sqlStr, 1).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) }多行查詢
多行查詢db.Query()執行一次查詢,返回多行結果(即Rows),一般用于執行select命令。參數args表示query中的占位參數。
func (db *DB) Query(query string, args ...interface{}) (*Rows, error)具體示例代碼:
// 查詢多條數據示例 func queryMultiRowDemo() {sqlStr := "select id, name, age from user where id > ?"rows, err := db.Query(sqlStr, 0)if err != nil {fmt.Printf("query failed, err:%v\n", err)return}// 非常重要:關閉rows釋放持有的數據庫鏈接defer rows.Close()// 循環讀取結果集中的數據for rows.Next() {var u usererr := 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\n", u.id, u.name, u.age)} }插入數據
插入、更新和刪除操作都使用Exec方法。
func (db *DB) Exec(query string, args ...interface{}) (Result, error)Exec執行一次命令(包括查詢、刪除、更新、插入等),返回的Result是對已執行的SQL命令的總結。參數args表示query中的占位參數。
具體插入數據示例代碼如下:
// 插入數據 func insertRowDemo() {sqlStr := "insert into user(name, age) values (?,?)"ret, err := db.Exec(sqlStr, "王五", 38)if err != nil {fmt.Printf("insert failed, err:%v\n", err)return}theID, err := ret.LastInsertId() // 新插入數據的idif err != nil {fmt.Printf("get lastinsert ID failed, err:%v\n", err)return}fmt.Printf("insert success, the id is %d.\n", theID) }更新數據
具體更新數據示例代碼如下:
// 更新數據 func updateRowDemo() {sqlStr := "update user set age=? where id = ?"ret, err := db.Exec(sqlStr, 39, 3)if err != nil {fmt.Printf("update failed, err:%v\n", err)return}n, err := ret.RowsAffected() // 操作影響的行數if err != nil {fmt.Printf("get RowsAffected failed, err:%v\n", err)return}fmt.Printf("update success, affected rows:%d\n", n) }刪除數據
具體刪除數據的示例代碼如下:
// 刪除數據 func deleteRowDemo() {sqlStr := "delete from user where id = ?"ret, err := db.Exec(sqlStr, 3)if err != nil {fmt.Printf("delete failed, err:%v\n", err)return}n, err := ret.RowsAffected() // 操作影響的行數if err != nil {fmt.Printf("get RowsAffected failed, err:%v\n", err)return}fmt.Printf("delete success, affected rows:%d\n", n) }MySQL預處理
什么是預處理?
普通SQL語句執行過程:
預處理執行過程:
為什么要預處理?
Go實現MySQL預處理
database/sql中使用下面的Prepare方法來實現預處理操作。
func (db *DB) Prepare(query string) (*Stmt, error)Prepare方法會先將sql語句發送給MySQL服務端,返回一個準備好的狀態用于之后的查詢和命令。返回值可以同時執行多個查詢和命令。
查詢操作的預處理示例代碼如下:
// 預處理查詢示例 func prepareQueryDemo() {sqlStr := "select id, name, age from user where id > ?"stmt, err := db.Prepare(sqlStr)if err != nil {fmt.Printf("prepare failed, err:%v\n", err)return}defer stmt.Close()rows, err := stmt.Query(0)if err != nil {fmt.Printf("query failed, err:%v\n", err)return}defer rows.Close()// 循環讀取結果集中的數據for rows.Next() {var u usererr := 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\n", u.id, u.name, u.age)} }插入、更新和刪除操作的預處理十分類似,這里以插入操作的預處理為例:
// 預處理插入示例 func prepareInsertDemo() {sqlStr := "insert into user(name, age) values (?,?)"stmt, err := db.Prepare(sqlStr)if err != nil {fmt.Printf("prepare failed, err:%v\n", err)return}defer stmt.Close()_, err = stmt.Exec("小王子", 18)if err != nil {fmt.Printf("insert failed, err:%v\n", err)return}_, err = stmt.Exec("沙河娜扎", 18)if err != nil {fmt.Printf("insert failed, err:%v\n", err)return}fmt.Println("insert success.") }SQL注入問題
我們任何時候都不應該自己拼接SQL語句!
這里我們演示一個自行拼接SQL語句的示例,編寫一個根據name字段查詢user表的函數如下:
// sql注入示例 func sqlInjectDemo(name string) {sqlStr := fmt.Sprintf("select id, name, age from user where name='%s'", name)fmt.Printf("SQL:%s\n", sqlStr)var u usererr := db.QueryRow(sqlStr).Scan(&u.id, &u.name, &u.age)if err != nil {fmt.Printf("exec failed, err:%v\n", err)return}fmt.Printf("user:%#v\n", u) }此時以下輸入字符串都可以引發SQL注入問題:
sqlInjectDemo("xxx' or 1=1#") sqlInjectDemo("xxx' union select * from user #") sqlInjectDemo("xxx' and (select count(*) from user) <10 #")補充:不同的數據庫中,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事務示例
下面的代碼演示了一個簡單的事務操作,該事物操作能夠確保兩次更新操作要么同時成功要么同時失敗,不會存在中間狀態。
// 事務操作示例 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=40 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!") }更強大、更好用的sqlx庫
練習題
總結
以上是生活随笔為你收集整理的Go语言操作MySQL的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: go protobuf tcp 粘包处理
- 下一篇: golang bloom filter实