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函数创建数据库对象,第一个参数是数据库驱动名,第二个参数是一个连接字符串

关于数据库的增删查改

增加数据

关于增加数据几个小知识点:

关于插入数据的时候占位符是通过问号:?

插入数据的后可以通过LastInsertId可以获取插入数据的id

通过RowsAffected可以获取受影响的行数

执行sql语句是通过exec

一个简单的使用例子:

48304ba5e6f9fe08f3fa1abda7d326ab.png

package main

import (

"github.com/jmoiron/sqlx"

_ "github.com/go-sql-driver/mysql"

"fmt"

)

func main() {

Db,err:=sqlx.Open("mysql","root:123456@tcp(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","男","8989@qq.com")

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)

}

48304ba5e6f9fe08f3fa1abda7d326ab.png

通过Exec方法插入数据,返回的结果是一个sql.Result类型

查询数据

下面是一个查询的例子代码:

48304ba5e6f9fe08f3fa1abda7d326ab.png

//执行查询操作

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

48304ba5e6f9fe08f3fa1abda7d326ab.png

使用了Query方法执行select查询语句,返回的是一个sql.Rows类型的结果集

迭代后者的Next方法,然后使用Scan方法给变量s赋值,以便取出结果。最后再把结果集关闭(释放连接)。

同样的我们还可以通过Exec方式执行查询语句

但是因为Exec返回的是一个sql.Result类型,从官网这里:

https://golang.google.cn/pkg/database/sql/#typeResult

我们可以直接这个接口里只有两个方法:LastInsertId(),RowsAffected()

我们还可以通过Db.Get()方法获取查询的数据,将查询的数据保存到一个结构体中

48304ba5e6f9fe08f3fa1abda7d326ab.png

//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)

48304ba5e6f9fe08f3fa1abda7d326ab.png

这样获取的一个数据,如果我们需要获取多行数据信息还可以通过Db.Select方法获取数据,代码例子为:

48304ba5e6f9fe08f3fa1abda7d326ab.png

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)

}

48304ba5e6f9fe08f3fa1abda7d326ab.png

通过Db.Select方法将查询的多行数据保存在一个切片中,然后就可以通过循环的方式获取每行数据

更新数据

下面是一个更新的例子,这里是通过Exec的方式执行的

48304ba5e6f9fe08f3fa1abda7d326ab.png

//更新数据

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

48304ba5e6f9fe08f3fa1abda7d326ab.png

删除数据

下面是一个删除的例子,同样是通过Exec的方式执行的

48304ba5e6f9fe08f3fa1abda7d326ab.png

//删除数据

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

48304ba5e6f9fe08f3fa1abda7d326ab.png

通过上面的简单例子,对golang操作mysql的增删查改,有了一个基本的了解,下面整理一下重点内容

sql.DB

当我们调用sqlx.Open()可以获取一个sql.DB对象,sql.DB是数据库的抽象,切记它不是数据库连接,sqlx.Open()只是验证数据库参数,并没不创建数据库连接。sql.DB提供了和数据库交互的函数,同时也管理维护一个数据库连接池,并且对于多gegoroutines也是安全的

sql.DB表示是数据库抽象,因此你有几个数据库就需要为每一个数据库创建一个sql.DB对象。因为它维护了一个连接池,因此不需要频繁的创建和销毁。

连接池

只用sql.Open函数创建连接池,可是此时只是初始化了连接池,并没有创建任何连接。连接创建都是惰性的,只有当真正使用到连接的时候,连接池才会创建连接。连接池很重要,它直接影响着你的程序行为。

连接池的工作原来却相当简单。当你的函数(例如Exec,Query)调用需要访问底层数据库的时候,函数首先会向连接池请求一个连接。如果连接池有空闲的连接,则返回给函数。否则连接池将会创建一个新的连接给函数。一旦连接给了函数,连接则归属于函数。函数执行完毕后,要不把连接所属权归还给连接池,要么传递给下一个需要连接的(Rows)对象,最后使用完连接的对象也会把连接释放回到连接池。

请求连接的函数有几个,执行完毕处理连接的方式也不同:

db.Ping() 调用完毕后会马上把连接返回给连接池。

db.Exec() 调用完毕后会马上把连接返回给连接池,但是它返回的Result对象还保留这连接的引用,当后面的代码需要处理结果集的时候连接将会被重用。

db.Query() 调用完毕后会将连接传递给sql.Rows类型,当然后者迭代完毕或者显示的调用.Clonse()方法后,连接将会被释放回到连接池。

db.QueryRow()调用完毕后会将连接传递给sql.Row类型,当.Scan()方法调用之后把连接释放回到连接池。

db.Begin() 调用完毕后将连接传递给sql.Tx类型对象,当.Commit()或.Rollback()方法调用后释放连接。

每个连接都是惰性的,如何验证sql.Open调用之后,sql.DB对象可用,通过db.Ping()初始化

代码例子:

48304ba5e6f9fe08f3fa1abda7d326ab.png

package main

import (

"github.com/jmoiron/sqlx"

_ "github.com/go-sql-driver/mysql"

"fmt"

)

func main() {

Db, err := sqlx.Open("mysql", "root:123456@tcp(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")

}

48304ba5e6f9fe08f3fa1abda7d326ab.png

需要知道:当调用了ping之后,连接池一定会初始化一个数据连接

连接失败

database/sql 其实帮我们做了很多事情,我们不用见擦汗连接失败的情况,当我们进行数据库操作的时候,如果连接失败,database/sql 会帮我们处理,它会自动连接2次,这个如果查看源码中我们可以看到如下的代码:

48304ba5e6f9fe08f3fa1abda7d326ab.png

// 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

}

48304ba5e6f9fe08f3fa1abda7d326ab.png

上述代码中变量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查询数据的时候主要分为三个步骤:

从连接池中请求一个连接

执行查询的sql语句

将数据库连接的所属权传递给Result结果集

Query返回的结果集是sql.Rows类型。它有一个Next方法,可以迭代数据库的游标,进而获取每一行的数据,使用方法如下:

48304ba5e6f9fe08f3fa1abda7d326ab.png

//执行查询操作

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

48304ba5e6f9fe08f3fa1abda7d326ab.png

其实当我们通过for循环迭代数据库的时候,当迭代到最后一样数据的时候,会出发一个io.EOF的信号,引发一个错误,同时go会自动调用rows.Close方法释放连接,然后返回false,此时循环将会结束退出。

通常你会正常迭代完数据然后退出循环。可是如果并没有正常的循环而因其他错误导致退出了循环。此时rows.Next处理结果集的过程并没有完成,归属于rows的连接不会被释放回到连接池。因此十分有必要正确的处理rows.Close事件。如果没有关闭rows连接,将导致大量的连接并且不会被其他函数重用,就像溢出了一样。最终将导致数据库无法使用。

所以为了避免这种情况的发生,最好的办法就是显示的调用rows.Close方法,确保连接释放,又或者使用defer指令在函数退出的时候释放连接,即使连接已经释放了,rows.Close仍然可以调用多次,是无害的。

rows.Next循环迭代的时候,因为触发了io.EOF而退出循环。为了检查是否是迭代正常退出还是异常退出,需要检查rows.Err。例如上面的代码应该改成:

48304ba5e6f9fe08f3fa1abda7d326ab.png

//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

}

48304ba5e6f9fe08f3fa1abda7d326ab.png

读取单条数据

Query方法是读取多行结果集,实际开发中,很多查询只需要单条记录,不需要再通过Next迭代。golang提供了QueryRow方法用于查询单条记录的结果集。

QueryRow方法的使用很简单,它要么返回sql.Row类型,要么返回一个error,如果是发送了错误,则会延迟到Scan调用结束后返回,如果没有错误,则Scan正常执行。只有当查询的结果为空的时候,会触发一个sql.ErrNoRows错误。你可以选择先检查错误再调用Scan方法,或者先调用Scan再检查错误。

在之前的代码中我们都用到了Scan方法,下面说说关于这个方法

结果集方法Scan可以把数据库取出的字段值赋值给指定的数据结构。它的参数是一个空接口的切片,这就意味着可以传入任何值。通常把需要赋值的目标变量的指针当成参数传入,它能将数据库取出的值赋值到指针值对象上。

代码例子如:

48304ba5e6f9fe08f3fa1abda7d326ab.png

// 查询数据

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)

48304ba5e6f9fe08f3fa1abda7d326ab.png

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这个时候,如果直接这样查询,会提示错误:

48304ba5e6f9fe08f3fa1abda7d326ab.png

// 查询数据

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)

48304ba5e6f9fe08f3fa1abda7d326ab.png

错误内容如下:

scan err: sql: Scan error on column index 2: unsupported Scan, storing driver.Value type into type *string

所以需要将代码更改为:

48304ba5e6f9fe08f3fa1abda7d326ab.png

// 查询数据

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)

48304ba5e6f9fe08f3fa1abda7d326ab.png

执行结果为:

user01 8989@qq.com { false}

我将数据库中添加了一列,是int类型,同样的默认值是Null,代码为:

48304ba5e6f9fe08f3fa1abda7d326ab.png

// 查询数据

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)

48304ba5e6f9fe08f3fa1abda7d326ab.png

其实但我们忽略错误直接输出的时候,也可以输出,当然Null的字段都被转换为了零值

而当我们按照上面的方式处理后,代码为:

48304ba5e6f9fe08f3fa1abda7d326ab.png

// 查询数据

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)

48304ba5e6f9fe08f3fa1abda7d326ab.png

输出的结果为:

user01 8989@qq.com { false} {0 false}

对Null的操作,一般还是需要验证的,代码如下:

48304ba5e6f9fe08f3fa1abda7d326ab.png

// 查询数据

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)

}

48304ba5e6f9fe08f3fa1abda7d326ab.png

这里我已经在数据库给字段添加内容了,所以这里默认输出10,但是当还是Null的时候输出的则是零值

但是有时候我们如果不关心是不是Null的时候,只是想把它当做空字符串处理就行,我们也可以使用[]byte,代码如下:

48304ba5e6f9fe08f3fa1abda7d326ab.png

// 查询数据

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

48304ba5e6f9fe08f3fa1abda7d326ab.png

这样处理后,如果有值则可以获取值,如果没有则获取的为空字符串

自动匹配字段

上面查询的例子中,我们都自己定义了变量,同时查询的时候也写明了字段,如果不指名字段,或者字段的顺序和查询的不一样,都有可能出错。因此如果能够自动匹配查询的字段值,将会十分节省代码,同时也易于维护。

go提供了Columns方法用获取字段名,与大多数函数一样,读取失败将会返回一个err,因此需要检查错误。

代码例子如下:

48304ba5e6f9fe08f3fa1abda7d326ab.png

// 查询数据

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)

}

48304ba5e6f9fe08f3fa1abda7d326ab.png

因为查询的时候是语句是:

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:23333222@qq.com create_time:2018-03-05 14:10:08 modify_time: socre:]

1 map[username:user11 sex:男 email:1231313@qq.com create_time:2018-03-05 14:10:11 modify_time: socre: user_id:8]

2 map[sex:男 email:65656@qq.com 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调用之后,才会把这个连接释放到连接池。

cef995ac18ce9cf4d497e9fced94e8d6.png

在事务处理的时候,不能使用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对象,因为事务过程只有一个连接,事务内的操作都是顺序执行的,在开始下一个数据库交互之前,必须先完成上一个数据库交互。

48304ba5e6f9fe08f3fa1abda7d326ab.png

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)

}

48304ba5e6f9fe08f3fa1abda7d326ab.png

调用了Query方法之后,在Next方法中取结果的时候,rows是维护了一个连接,再次调用QueryRow的时候,db会再从连接池取出一个新的连接。rows和db的连接两者可以并存,并且相互不影响。

但是如果逻辑在事务处理中会失效,如下代码:

48304ba5e6f9fe08f3fa1abda7d326ab.png

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)

}

48304ba5e6f9fe08f3fa1abda7d326ab.png

tx执行了Query方法后,连接转移到rows上,在Next方法中,tx.QueryRow将尝试获取该连接进行数据库操作。因为还没有调用rows.Close,因此底层的连接属于busy状态,tx是无法再进行查询的。

完整的小结

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

48304ba5e6f9fe08f3fa1abda7d326ab.png

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)

}

}

48304ba5e6f9fe08f3fa1abda7d326ab.png

这里定义了一个clearTransaction(tx)函数,该函数会执行rollback操作。因为我们事务处理过程中,任何一个错误都会导致main函数退出,因此在main函数退出执行defer的rollback操作,回滚事务和释放连接。

如果不添加defer,只在最后Commit后check错误err后再rollback,那么当doSomething发生异常的时候,函数就退出了,此时还没有执行到tx.Commit。这样就导致事务的连接没有关闭,事务也没有回滚。

tx事务环境中,只有一个数据库连接,事务内的Eexc都是依次执行的,事务中也可以使用db进行查询,但是db查询的过程会新建连接,这个连接的操作不属于该事务。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值