1. 程式人生 > >Go基礎之--操作Mysql

Go基礎之--操作Mysql

關於標準庫database/sql

database/sql是golang的標準庫之一,它提供了一系列介面方法,用於訪問關係資料庫。它並不會提供資料庫特有的方法,那些特有的方法交給資料庫驅動去實現。

database/sql庫提供了一些type。這些型別對掌握它的用法非常重要。

DB
資料庫物件。 sql.DB型別代表了資料庫。和其他語言不一樣,它並是資料庫連線。golang中的連線來自內部實現的連線池,連線的建立是惰性的,當你需要連線的時候,連線池會自動幫你建立。通常你不需要操作連線池。一切都有go來幫你完成。

Results
結果集。資料庫查詢的時候,都會有結果集。sql.Rows型別表示查詢返回多行資料的結果集。sql.Row則表示單行查詢結果的結果集。當然,對於插入更新和刪除,返回的結果集型別為sql.Result。

Statements
語句。sql.Stmt型別表示sql查詢語句,例如DDL,DML等類似的sql語句。可以把當成prepare語句構造查詢,也可以直接使用sql.DB的函式對其操作。

而通常工作中我們可能更多的是用https://github.com/jmoiron/sqlx包來操作資料庫
sqlx是基於標準庫database/sql的擴充套件,並且我們可以通過sqlx操作各種型別的資料如

和其他語言不通的是,查詢資料庫的時候需要建立一個連線,對於go而言則是需要建立一個數據庫物件,連線將會在查詢需要的時候,由連線池建立並維護,使用sql.Open函式建立資料庫物件,第一個引數是資料庫驅動名,第二個引數是一個連線字串

關於資料庫的增刪查改

增加資料

關於增加資料幾個小知識點:

  1. 關於插入資料的時候佔位符是通過問號:?
  2. 插入資料的後可以通過LastInsertId可以獲取插入資料的id
  3. 通過RowsAffected可以獲取受影響的行數
  4. 執行sql語句是通過exec

一個簡單的使用例子:需要安裝第三方 包

go get github.com/jmoiron/sqlx
go get github.com/go-sql-driver/mysql

package main

import (
    "github.com/jmoiron/sqlx"
    _ "github.com/go-sql-driver/mysql"
    "fmt"
)

func main() {
    Db,err:=sqlx.Open("mysql","root:
[email protected]
(192.168.14.7:3306)/godb") if err != nil{ fmt.Println("connect to mysql failed,",err) return } defer Db.Close() fmt.Println("connect to mysql success") //執行sql語句,切記這裡的佔位符是? result,err := Db.Exec("INSERT INTO user_info(username,sex,email)VALUES (?,?,?)","user01","男","[email protected]") if err != nil{ fmt.Println("insert failed,",err) } // 通過LastInsertId可以獲取插入資料的id userId,err:= result.LastInsertId() // 通過RowsAffected可以獲取受影響的行數 rowCount,err:=result.RowsAffected() fmt.Println("user_id:",userId) fmt.Println("rowCount:",rowCount) }

通過Exec方法插入資料,返回的結果是一個sql.Result型別

查詢資料

下面是一個查詢的例子程式碼:

//執行查詢操作
rows,err := Db.Query("SELECT email FROM user_info WHERE user_id>=5")
if err != nil{
    fmt.Println("select db failed,err:",err)
    return
}
// 這裡獲取的rows是從資料庫查的滿足user_id>=5的所有行的email資訊,rows.Next(),用於迴圈獲取所有
for rows.Next(){
    var s string
    err = rows.Scan(&s)
    if err != nil{
        fmt.Println(err)
        return
    }
    fmt.Println(s)
}
rows.Close()

使用了Query方法執行select查詢語句,返回的是一個sql.Rows型別的結果集
迭代後者的Next方法,然後使用Scan方法給變數s賦值,以便取出結果。最後再把結果集關閉(釋放連線)。
同樣的我們還可以通過Exec方式執行查詢語句
但是因為Exec返回的是一個sql.Result型別,從官網這裡:
https://golang.google.cn/pkg/database/sql/#type Result
我們可以直接這個接口裡只有兩個方法:LastInsertId(),RowsAffected()

我們還可以通過Db.Get()方法獲取查詢的資料,將查詢的資料儲存到一個結構體中

//Get執行查詢操作
    type user_info struct {
        Username string `db:"username"`
        Email string `db:"email"`
    }
    var userInfo user_info
    err = Db.Get(&userInfo,"SELECT username,email FROM user_info WHERE user_id=5")
    if err != nil{
        fmt.Println(err)
        return 
    }
    fmt.Println(userInfo)

這樣獲取的一個數據,如果我們需要獲取多行資料資訊還可以通過Db.Select方法獲取資料,程式碼例子為:

var userList []*user_info
err = Db.Select(&userList,"SELECT username,email FROM user_info WHERE user_id>5")
if err != nil{
    fmt.Println(err)
    return
}
fmt.Println(userList)
for _,v:= range userList{
    fmt.Println(v)
}

通過Db.Select方法將查詢的多行資料儲存在一個切片中,然後就可以通過迴圈的方式獲取每行資料

更新資料

下面是一個更新的例子,這裡是通過Exec的方式執行的

//更新資料
results,err := Db.Exec("UPDATE user_info SET username=? where user_id=?","golang",5)
if err != nil{
    fmt.Println("update data fail,err:",err)
    return
}
fmt.Println(results.RowsAffected())

刪除資料

下面是一個刪除的例子,同樣是通過Exec的方式執行的

//刪除資料
results,err := Db.Exec("DELETE from user_info where user_id=?",5)
if err != nil{
    fmt.Println("delete data fail,err:",err)
    return
}
fmt.Println(results.RowsAffected())

通過上面的簡單例子,對golang操作mysql的增刪查改,有了一個基本的瞭解,下面整理一下重點內容

sql.DB

當我們呼叫sqlx.Open()可以獲取一個sql.DB物件,sql.DB是資料庫的抽象,切記它不是資料庫連線,sqlx.Open()只是驗證資料庫引數,並沒不建立資料庫連線。sql.DB提供了和資料庫互動的函式,同時也管理維護一個數據庫連線池,並且對於多gegoroutines也是安全的

sql.DB表示是資料庫抽象,因此你有幾個資料庫就需要為每一個數據庫建立一個sql.DB物件。因為它維護了一個連線池,因此不需要頻繁的建立和銷燬。

連線池

只用sql.Open函式建立連線池,可是此時只是初始化了連線池,並沒有建立任何連線。連線建立都是惰性的,只有當真正使用到連線的時候,連線池才會建立連線。連線池很重要,它直接影響著你的程式行為。

連線池的工作原來卻相當簡單。當你的函式(例如Exec,Query)呼叫需要訪問底層資料庫的時候,函式首先會向連線池請求一個連線。如果連線池有空閒的連線,則返回給函式。否則連線池將會建立一個新的連線給函式。一旦連線給了函式,連線則歸屬於函式。函式執行完畢後,要不把連線所屬權歸還給連線池,要麼傳遞給下一個需要連線的(Rows)物件,最後使用完連線的物件也會把連線釋放回到連線池。

請求連線的函式有幾個,執行完畢處理連線的方式也不同:

  1. db.Ping() 呼叫完畢後會馬上把連線返回給連線池。
  2. db.Exec() 呼叫完畢後會馬上把連線返回給連線池,但是它返回的Result物件還保留這連線的引用,當後面的程式碼需要處理結果集的時候連線將會被重用。
  3. db.Query() 呼叫完畢後會將連線傳遞給sql.Rows型別,當然後者迭代完畢或者顯示的呼叫.Clonse()方法後,連線將會被釋放回到連線池。
  4. db.QueryRow()呼叫完畢後會將連線傳遞給sql.Row型別,當.Scan()方法呼叫之後把連線釋放回到連線池。
  5. db.Begin() 呼叫完畢後將連線傳遞給sql.Tx型別物件,當.Commit()或.Rollback()方法呼叫後釋放連線。

每個連線都是惰性的,如何驗證sql.Open呼叫之後,sql.DB物件可用,通過db.Ping()初始化

程式碼例子:

package main

import (
    "github.com/jmoiron/sqlx"
    _ "github.com/go-sql-driver/mysql"
    "fmt"
)

func main() {
    Db, err := sqlx.Open("mysql", "root:[email protected](192.168.50.166:3306)/godb")
    if err != nil {
        fmt.Println("connect to mysql failed,", err)
        return
    }

    defer Db.Close()
    fmt.Println("connect to mysql success")

    err = Db.Ping()
    if err != nil{
        fmt.Println(err)
        return
    }
    fmt.Println("ping success")
}

需要知道:當呼叫了ping之後,連線池一定會初始化一個數據連線

連線失敗

database/sql 其實幫我們做了很多事情,我們不用見擦汗連線失敗的情況,當我們進行資料庫操作的時候,如果連線失敗,database/sql 會幫我們處理,它會自動連線2次,這個如果檢視原始碼中我們可以看到如下的程式碼:

// ExecContext executes a query without returning any rows.
// The args are for any placeholder parameters in the query.
func (db *DB) ExecContext(ctx context.Context, query string, args ...interface{}) (Result, error) {
    var res Result
    var err error
    for i := 0; i < maxBadConnRetries; i++ {
        res, err = db.exec(ctx, query, args, cachedOrNewConn)
        if err != driver.ErrBadConn {
            break
        }
    }
    if err == driver.ErrBadConn {
        return db.exec(ctx, query, args, alwaysNewConn)
    }
    return res, err
}

上述程式碼中變數maxBadConnRetries小時如果連線失敗嘗試的次數,預設是2

關於連線池配置

db.SetMaxIdleConns(n int) 設定連線池中的保持連線的最大連線數。預設也是0,表示連線池不會保持釋放會連線池中的連線的連線狀態:即當連線釋放回到連線池的時候,連線將會被關閉。這會導致連線再連線池中頻繁的關閉和建立。

db.SetMaxOpenConns(n int) 設定開啟資料庫的最大連線數。包含正在使用的連線和連線池的連線。如果你的函式呼叫需要申請一個連線,並且連線池已經沒有了連線或者連線數達到了最大連線數。此時的函式呼叫將會被block,直到有可用的連線才會返回。設定這個值可以避免併發太高導致連線mysql出現too many connections的錯誤。該函式的預設設定是0,表示無限制。

db.SetConnMaxLifetime(d time.Duration) 設定連線可以被使用的最長有效時間,如果過期,連線將被拒絕

讀取資料

在上一篇文章中整理查詢資料的時候,使用了Query的方法查詢,其實database/sql還提供了QueryRow方法查詢資料,就像之前說的database/sql連線建立都是惰性的,所以當我們通過Query查詢資料的時候主要分為三個步驟:

  1. 從連線池中請求一個連線
  2. 執行查詢的sql語句
  3. 將資料庫連線的所屬權傳遞給Result結果集

Query返回的結果集是sql.Rows型別。它有一個Next方法,可以迭代資料庫的遊標,進而獲取每一行的資料,使用方法如下:

//執行查詢操作
rows,err := Db.Query("SELECT email FROM user_info WHERE user_id>=5")
if err != nil{
    fmt.Println("select db failed,err:",err)
    return
}
// 這裡獲取的rows是從資料庫查的滿足user_id>=5的所有行的email資訊,rows.Next(),用於迴圈獲取所有
for rows.Next(){
    var s string
    err = rows.Scan(&s)
    if err != nil{
        fmt.Println(err)
        return
    }
    fmt.Println(s)
}
rows.Close()

其實當我們通過for迴圈迭代資料庫的時候,當迭代到最後一樣資料的時候,會出發一個io.EOF的訊號,引發一個錯誤,同時go會自動呼叫rows.Close方法釋放連線,然後返回false,此時迴圈將會結束退出。

通常你會正常迭代完資料然後退出迴圈。可是如果並沒有正常的迴圈而因其他錯誤導致退出了迴圈。此時rows.Next處理結果集的過程並沒有完成,歸屬於rows的連線不會被釋放回到連線池。因此十分有必要正確的處理rows.Close事件。如果沒有關閉rows連線,將導致大量的連線並且不會被其他函式重用,就像溢位了一樣。最終將導致資料庫無法使用。

所以為了避免這種情況的發生,最好的辦法就是顯示的呼叫rows.Close方法,確保連線釋放,又或者使用defer指令在函式退出的時候釋放連線,即使連線已經釋放了,rows.Close仍然可以呼叫多次,是無害的。

rows.Next迴圈迭代的時候,因為觸發了io.EOF而退出迴圈。為了檢查是否是迭代正常退出還是異常退出,需要檢查rows.Err。例如上面的程式碼應該改成:

//Query執行查詢操作
rows,err := Db.Query("SELECT email FROM user_info WHERE user_id>=5")
if err != nil{
    fmt.Println("select db failed,err:",err)
    return
}
// 這裡獲取的rows是從資料庫查的滿足user_id>=5的所有行的email資訊,rows.Next(),用於迴圈獲取所有
for rows.Next(){
    var s string
    err = rows.Scan(&s)
    if err != nil{
        fmt.Println(err)
        return
    }
    fmt.Println(s)
}
rows.Close()
if err = rows.Err();err != nil{
    fmt.Println(err)
    return 
}

讀取單條資料

Query方法是讀取多行結果集,實際開發中,很多查詢只需要單條記錄,不需要再通過Next迭代。golang提供了QueryRow方法用於查詢單條記錄的結果集。

QueryRow方法的使用很簡單,它要麼返回sql.Row型別,要麼返回一個error,如果是傳送了錯誤,則會延遲到Scan呼叫結束後返回,如果沒有錯誤,則Scan正常執行。只有當查詢的結果為空的時候,會觸發一個sql.ErrNoRows錯誤。你可以選擇先檢查錯誤再呼叫Scan方法,或者先呼叫Scan再檢查錯誤。

在之前的程式碼中我們都用到了Scan方法,下面說說關於這個方法

結果集方法Scan可以把資料庫取出的欄位值賦值給指定的資料結構。它的引數是一個空介面的切片,這就意味著可以傳入任何值。通常把需要賦值的目標變數的指標當成引數傳入,它能將資料庫取出的值賦值到指標值物件上。
程式碼例子如:

// 查詢資料
var username string
var email string
rows  := Db.QueryRow("SELECT username,email FROM user_info WHERE user_id=6")
err = rows.Scan(&username,&email)
if err != nil{
    fmt.Println("scan err:",err)
    return
}
fmt.Println(username,email)

Scan還會幫我們自動推斷除資料欄位匹配目標變數。比如有個資料庫欄位的型別是VARCHAR,而他的值是一個數字串,例如"1"。如果我們定義目標變數是string,則scan賦值後目標變數是數字string。如果宣告的目標變數是一個數字型別,那麼scan會自動呼叫strconv.ParseInt()或者strconv.ParseInt()方法將欄位轉換成和宣告的目標變數一致的型別。當然如果有些欄位無法轉換成功,則會返回錯誤。因此在呼叫scan後都需要檢查錯誤。

空值處理

資料庫有一個特殊的型別,NULL空值。可是NULL不能通過scan直接跟普遍變數賦值,甚至也不能將null賦值給nil。對於null必須指定特殊的型別,這些型別定義在database/sql庫中。例如sql.NullFloat64,sql.NullString,sql.NullBool,sql.NullInt64。如果在標準庫中找不到匹配的型別,可以嘗試在驅動中尋找。下面是一個簡單的例子:

下面程式碼,資料庫中create_time為Null這個時候,如果直接這樣查詢,會提示錯誤:

// 查詢資料
var username string
var email string
var createTime string
rows  := Db.QueryRow("SELECT username,email,create_time FROM user_info WHERE user_id=6")
err = rows.Scan(&username,&email,&createTime)
if err != nil{
    fmt.Println("scan err:",err)
    return
}
fmt.Println(username,email,createTime)

錯誤內容如下:

scan err: sql: Scan error on column index 2: unsupported Scan, storing driver.Value type <nil> into type *string所以需要將程式碼更改為:

// 查詢資料
var username string
var email string
var createTime sql.NullString
rows  := Db.QueryRow("SELECT username,email,create_time FROM user_info WHERE user_id=6")
err = rows.Scan(&username,&email,&createTime)
if err != nil{
    fmt.Println("scan err:",err)
    return
}
fmt.Println(username,email,createTime)

執行結果為:

user01 [email protected] { false}
我將資料庫中添加了一列,是int型別,同樣的預設值是Null,程式碼為:

// 查詢資料
var username string
var email string
var createTime string
var score int
rows  := Db.QueryRow("SELECT username,email,create_time,socre FROM user_info WHERE user_id=6")
rows.Scan(&username,&email,&createTime,&score)
fmt.Println(username,email,createTime,score)

其實但我們忽略錯誤直接輸出的時候,也可以輸出,當然Null的欄位都被轉換為了零值
而當我們按照上面的方式處理後,程式碼為:

// 查詢資料
var username string
var email string
var createTime sql.NullString
var score sql.NullInt64
rows  := Db.QueryRow("SELECT username,email,create_time,socre FROM user_info WHERE user_id=6")
err = rows.Scan(&username,&email,&createTime,&score)
if err != nil{
    fmt.Println("scan fail,err:",err)
    return
}
fmt.Println(username,email,createTime,score)

輸出的結果為:

user01 [email protected] { false} {0 false}
對Null的操作,一般還是需要驗證的,程式碼如下:

// 查詢資料
var score sql.NullInt64
rows  := Db.QueryRow("SELECT socre FROM user_info WHERE user_id=6")
err = rows.Scan(&score)
if err != nil{
    fmt.Println("scan fail,err:",err)
    return
}
if score.Valid{
    fmt.Println("res:",score.Int64)
}else{
    fmt.Println("err",score.Int64)
}

這裡我已經在資料庫給欄位新增內容了,所以這裡預設輸出10,但是當還是Null的時候輸出的則是零值
但是有時候我們如果不關心是不是Null的時候,只是想把它當做空字串處理就行,我們也可以使用[]byte,程式碼如下:

// 查詢資料
var score []byte
var modifyTime []byte
rows  := Db.QueryRow("SELECT modify_time,socre FROM user_info WHERE user_id=6")
err = rows.Scan(&modifyTime,&score)
if err != nil{
    fmt.Println("scan fail,err:",err)
    return
}
fmt.Println(string(modifyTime),string(score))

這樣處理後,如果有值則可以獲取值,如果沒有則獲取的為空字串

自動匹配欄位

上面查詢的例子中,我們都自己定義了變數,同時查詢的時候也寫明瞭欄位,如果不指名欄位,或者欄位的順序和查詢的不一樣,都有可能出錯。因此如果能夠自動匹配查詢的欄位值,將會十分節省程式碼,同時也易於維護。
go提供了Columns方法用獲取欄位名,與大多數函式一樣,讀取失敗將會返回一個err,因此需要檢查錯誤。
程式碼例子如下:

// 查詢資料

rows,err:= Db.Query("SELECT * FROM user_info WHERE user_id>6")
if err != nil{
    fmt.Println("select fail,err:",err)
    return
}
cols,err := rows.Columns()
if err != nil{
    fmt.Println("get columns fail,err:",err)
    return
}
fmt.Println(cols)
vals := make([][]byte, len(cols))
scans := make([]interface{},len(cols))


for i := range vals{
    scans[i] = &vals[i]
}
fmt.Println(scans)
var results []map[string]string

for rows.Next(){
    err = rows.Scan(scans...)
    if err != nil{
        fmt.Println("scan fail,err:",err)
        return
    }
    row := make(map[string]string)
    for k,v:=range vals{
        key := cols[k]
        row[key] =string(v)
    }
    results = append(results,row)
}

for k,v:=range results{
    fmt.Println(k,v)
}

因為查詢的時候是語句是:
SELECT * FROM user_info WHERE user_id>6
這樣就會獲取每行資料的所有的欄位
使用rows.Columns()獲取欄位名,是一個string的陣列
然後建立一個切片vals,用來存放所取出來的資料結果,類似是byte的切片。接下來還需要定義一個切片,這個切片用來scan,將資料庫的值複製到給它
vals則得到了scan複製給他的值,因為是byte的切片,因此在迴圈一次,將其轉換成string即可。
轉換後的row即我們取出的資料行值,最後組裝到result切片中。

上面程式碼的執行結果為:

[user_id username sex email create_time modify_time socre]
[0xc4200c6000 0xc4200c6018 0xc4200c6030 0xc4200c6048 0xc4200c6060 0xc4200c6078 0xc4200c6090]
0 map[user_id:7 username:user01 sex:男 email:[email protected] create_time:2018-03-05 14:10:08 modify_time: socre:]
1 map[username:user11 sex:男 email:[email protected] create_time:2018-03-05 14:10:11 modify_time: socre: user_id:8]
2 map[sex:男 email:[email protected] create_time:2018-03-05 14:10:15 modify_time: socre: user_id:9 username:user12]

通過上面例子的整理以及上面文章的整理,我們基本可以知道:
Exec的時候通常用於執行插入和更新操作
Query以及QueryRow通常用於執行查詢操作

Exec執行完畢之後,連線會立即釋放回到連線池中,因此不需要像query那樣再手動呼叫row的close方法。

事務是資料庫的一個非常重要的特性,尤其對於銀行,支付系統,等等。
database/sql提供了事務處理的功能。通過Tx物件實現。db.Begin會建立tx物件,後者的Exec和Query執行事務的資料庫操作,最後在tx的Commit和Rollback中完成資料庫事務的提交和回滾,同時釋放連線。

tx物件

我們在之前查詢以及操作資料庫都是用的db物件,而事務則是使用另外一個物件.
使用db.Begin 方法可以建立tx物件,tx物件也可以對資料庫互動的Query,Exec方法
用法和我們之前操作基本一樣,但是需要在查詢或者操作完畢之後執行tx物件的Commit提交或者Rollback方法回滾。

一旦建立了tx物件,事務處理都依賴於tx物件,這個物件會從連線池中取出一個空閒的連線,接下來的sql執行都基於這個連線,知道commit或者Roolback呼叫之後,才會把這個連線釋放到連線池。

在事務處理的時候,不能使用db的查詢方法,當然你如果使用也能執行語句成功,但是這和你事務裡執行的操作將不是一個事務,將不會接受commit和rollback的改變,如下面操作時:

tx,err := Db.Begin()
Db.Exec()
tx.Exec()
tx.Commit()

上面這個虛擬碼中,呼叫Db.Exec方法的時候,和tx執行Exec方法時候是不同的,只有tx的會繫結到事務中,db則是額外的一個連線,兩者不是同一個事務。

事務與連線

建立Tx物件的時候,會從連線池中取出連線,然後呼叫相關的Exec方法的時候,連線仍然會繫結在該事務處理中。
事務的連線生命週期從Beigin函式呼叫起,直到Commit和Rollback函式的呼叫結束。

事務併發

對於sql.Tx物件,因為事務過程只有一個連線,事務內的操作都是順序執行的,在開始下一個資料庫互動之前,必須先完成上一個資料庫互動。

rows, _ := db.Query("SELECT id FROM user") 
for rows.Next() {
    var mid, did int
    rows.Scan(&mid)
    db.QueryRow("SELECT id FROM detail_user WHERE master = ?", mid).Scan(&did)

}

呼叫了Query方法之後,在Next方法中取結果的時候,rows是維護了一個連線,再次呼叫QueryRow的時候,db會再從連線池取出一個新的連線。rows和db的連線兩者可以並存,並且相互不影響。

但是如果邏輯在事務處理中會失效,如下程式碼:

rows, _ := tx.Query("SELECT id FROM user")
for rows.Next() {
   var mid, did int
   rows.Scan(&mid)
   tx.QueryRow("SELECT id FROM detail_user WHERE master = ?", mid).Scan(&did)
}

tx執行了Query方法後,連線轉移到rows上,在Next方法中,tx.QueryRow將嘗試獲取該連線進行資料庫操作。因為還沒有呼叫rows.Close,因此底層的連線屬於busy狀態,tx是無法再進行查詢的。

完整的小結

通過下面一個完整的例子就行更好的理解:

func doSomething(){
    panic("A Panic Running Error")
}

func clearTransaction(tx *sql.Tx){
    err := tx.Rollback()
    if err != sql.ErrTxDone && err != nil{
        log.Fatalln(err)
    }
}


func main() {
    db, err := sql.Open("mysql", "root:@tcp(127.0.0.1:3306)/test?parseTime=true")
    if err != nil {
        log.Fatalln(err)
    }

    defer db.Close()

    tx, err := db.Begin()
    if err != nil {
        log.Fatalln(err)
    }
    defer clearTransaction(tx)

    rs, err := tx.Exec("UPDATE user SET gold=50 WHERE real_name='vanyarpy'")
    if err != nil {
        log.Fatalln(err)
    }
    rowAffected, err := rs.RowsAffected()
    if err != nil {
        log.Fatalln(err)
    }
    fmt.Println(rowAffected)

    rs, err = tx.Exec("UPDATE user SET gold=150 WHERE real_name='noldorpy'")
    if err != nil {
        log.Fatalln(err)
    }
    rowAffected, err = rs.RowsAffected()
    if err != nil {
        log.Fatalln(err)
    }
    fmt.Println(rowAffected)

    doSomething()

    if err := tx.Commit(); err != nil {
        // tx.Rollback() 此時處理錯誤,會忽略doSomthing的異常
        log.Fatalln(err)
    }

}

這裡定義了一個clearTransaction(tx)函式,該函式會執行rollback操作。因為我們事務處理過程中,任何一個錯誤都會導致main函式退出,因此在main函式退出執行defer的rollback操作,回滾事務和釋放連線。

如果不新增defer,只在最後Commit後check錯誤err後再rollback,那麼當doSomething發生異常的時候,函式就退出了,此時還沒有執行到tx.Commit。這樣就導致事務的連線沒有關閉,事務也沒有回滾。

tx事務環境中,只有一個數據庫連線,事務內的Eexc都是依次執行的,事務中也可以使用db進行查詢,但是db查詢的過程會新建連線,這個連線的操作不屬於該事務。