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 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()
}

初始化連接

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"
    // 不會校驗賬號密碼是否正確
    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包會自動創建和釋放連接;它也會維護一個閑置連接的連接池。

// 完整的連接MySQL數據庫並查詢表數據demo
package main import (
"database/sql" "fmt" _ "github.com/go-sql-driver/mysql" ) func main() { db, err := sql.Open("mysql", "root:123456@tcp(127.0.0.1:3306)/mysql?charset=utf8") if err != nil { // 連接失敗 fmt.Printf("connect mysql fail ! [%s]", err) } else { // 連接成功 fmt.Println("connect to mysql success") } rows, err := db.Query("select id,username from userinfo") // 執行sql if err != nil { // 查詢失敗 fmt.Printf("select fail [%s]", err) } var mapUser map[string]int mapUser = make(map[string]int) for rows.Next() { var id int var username string rows.Columns() err := rows.Scan(&id, &username) if err != nil { fmt.Printf("get user info error [%s]", err) } mapUser[username] = id } for k, v := range mapUser { fmt.Println(k, v) } }

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語句執行過程:

  1. 客戶端對SQL語句進行占位符替換得到完整的SQL語句。
  2. 客戶端發送完整SQL語句到MySQL服務端
  3. MySQL服務端執行完整的SQL語句並將結果返回給客戶端。

預處理執行過程:

  1. 把SQL語句分成兩部分,命令部分與數據部分。
  2. 先把命令部分發送給MySQL服務端,MySQL服務端進行SQL預處理。
  3. 然后把數據部分發送給MySQL服務端,MySQL服務端對SQL語句進行占位符替換。
  4. MySQL服務端執行完整的SQL語句並將結果返回給客戶端。

為什么要預處理?

  1. 優化MySQL服務器重復執行SQL的方法,可以提升服務器性能,提前讓服務器編譯,一次編譯多次執行,節省后續編譯的成本。
  2. 避免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 #")