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 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() // 注意
}
思考題: 為什么上面代碼中的defer db.Close()陳述句不應該寫在if err != nil的前面呢?
初始化連接
Open函式可能只是驗證其引數,而不創建與資料庫的連接,如果要檢查資料源的名稱是否合法,應呼叫回傳值的Ping方法,
回傳的DB可以安全的被多個goroutine同時使用,并會維護自身的閑置連接池,這樣一來,Open函式只需呼叫一次,很少需要關閉DB,
// 定義一個全域物件db
var db *sql.DB
// 定義一個初始化資料庫的函式
func initDB() (err error) {
// DSN:Data Source Name
dsn := "user:password@tcp(127.0.0.1:3306)/test"
// 不會校驗賬號密碼是否正確
// 注意!!!這里不要使用:=,我們是給全域變數賦值,然后在main函式中使用全域變數db
db, 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是一個資料庫(操作)句柄,代表一個具有零到多個底層連接的連接池,它可以安全的被多個go程同時使用,database/sql包會自動創建和釋放連接;它也會維護一個閑置連接的連接池,
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;
查詢
單行查詢
單行查詢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 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\n", u.id, u.name, u.age)
}
}
插入資料
插入、更新和洗掉操作都使用
方法,
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() // 新插入資料的id
if 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陳述句執行程序:
- 客戶端對SQL陳述句進行占位符替換得到完整的SQL陳述句,
- 客戶端發送完整SQL陳述句到MySQL服務端
- MySQL服務端執行完整的SQL陳述句并將結果回傳給客戶端,
預處理執行程序:
- 把SQL陳述句分成兩部分,命令部分與資料部分,
- 先把命令部分發送給MySQL服務端,MySQL服務端進行SQL預處理,
- 然后把資料部分發送給MySQL服務端,MySQL服務端對SQL陳述句進行占位符替換,
- MySQL服務端執行完整的SQL陳述句并將結果回傳給客戶端,
為什么要預處理?
- 優化MySQL服務器重復執行SQL的方法,可以提升服務器性能,提前讓服務器編譯,一次編譯多次執行,節省后續編譯的成本,
- 避免SQL注入問題,
Go實作MySQL預處理
Go中的
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 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\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.")
}
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=?"
_, err = tx.Exec(sqlStr1, 2)
if err != nil {
tx.Rollback() // 回滾
fmt.Printf("exec sql1 failed, err:%v\n", err)
return
}
sqlStr2 := "Update user set age=40 where id=?"
_, err = tx.Exec(sqlStr2, 4)
if err != nil {
tx.Rollback() // 回滾
fmt.Printf("exec sql2 failed, err:%v\n", err)
return
}
err = tx.Commit() // 提交事務
if err != nil {
tx.Rollback() // 回滾
fmt.Printf("commit failed, err:%v\n", err)
return
}
fmt.Println("exec trans success!")
}
sqlx使用
第三方庫sqlx能夠簡化操作,提高開發效率,
安裝
go get github.com/jmoiron/sqlx
基本使用
連接資料庫
var db *sqlx.DB
func initDB() (err error) {
dsn := "user:password@tcp(127.0.0.1:3306)/test"
// 也可以使用MustConnect連接不成功就panic
db, err = sqlx.Connect("mysql", dsn)
if err != nil {
fmt.Printf("connect DB failed, err:%v\n", err)
return
}
db.SetMaxOpenConns(20)
db.SetMaxIdleConns(10)
return
}
查詢
查詢單行資料示例代碼如下:
// 查詢單條資料示例
func queryRowDemo() {
sqlStr := "select id, name, age from user where id=?"
var u user
err := db.Get(&u, sqlStr, 1)
if err != nil {
fmt.Printf("get failed, err:%v\n", err)
return
}
fmt.Printf("id:%d name:%s age:%d\n", u.ID, u.Name, u.Age)
}
查詢多行資料示例代碼如下:
// 查詢多條資料示例
func queryMultiRowDemo() {
sqlStr := "select id, name, age from user where id > ?"
var users []user
err := db.Select(&users, sqlStr, 0)
if err != nil {
fmt.Printf("query failed, err:%v\n", err)
return
}
fmt.Printf("users:%#v\n", users)
}
插入、更新和洗掉
sqlx中的exec方法與原生sql中的exec使用基本一致:
// 插入資料
func insertRowDemo() {
sqlStr := "insert into user(name, age) values (?,?)"
ret, err := db.Exec(sqlStr, "沙河小王子", 19)
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)
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, 6)
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, 6)
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)
}
事務操作
對于事務操作,我們可以使用sqlx中提供的db.Beginx()和tx.MustExec()方法來簡化錯誤處理程序,示例代碼如下:
func transactionDemo() {
tx, err := db.Beginx() // 開啟事務
if err != nil {
if tx != nil {
tx.Rollback()
}
fmt.Printf("begin trans failed, err:%v\n", err)
return
}
sqlStr1 := "Update user set age=40 where id=?"
tx.MustExec(sqlStr1, 2)
sqlStr2 := "Update user set age=50 where id=?"
tx.MustExec(sqlStr2, 4)
err = tx.Commit() // 提交事務
if err != nil {
tx.Rollback() // 回滾
fmt.Printf("commit failed, err:%v\n", err)
return
}
fmt.Println("exec trans success!")
}
注意事項
SQL中的占位符
不同的資料庫中,SQL陳述句使用的占位符語法不盡相同,
| 資料庫 | 占位符語法 |
|---|---|
| MySQL | ? |
| PostgreSQL | $1, $2等 |
| SQLite | ? 和$1 |
| Oracle | :name |
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 users []user
err := db.Select(&users, sqlStr)
if err != nil {
fmt.Printf("exec failed, err:%v\n", err)
return
}
for _, u := range users {
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 #")
練習題
- 結合
net/http和sqlx包實作一個注冊及登陸的web程式,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/45068.html
標籤:Go
上一篇:Go語言操作Redis
下一篇:gRPC初識
