Go 官方提供了database 包,database 包下有 sql/driver。该包用来定义操作数据库的接口,这保证了无论使用哪种数据库,操作方式都是相同的。但 Go 官方并没有提供连接数据库的 driver,如果要操作数据库,还需要第三方的 driver 包。这里介绍 go-mysql-driver 的使用。
1. 安装
在执行了 go mod 的项目目录下执行如下安装命令
| |
Win10 下,go-sql-driver 包将被安装到 %GOPATH%\pkg\mod\github.com\go-sql-driver\mysql@v1.5.0 目录下,其它项目使用时不必重复下载,执行上述命令即可直接引入。
2. 导入
示例代码如下
| |
Golang 提供了database/sql 包,用于对 SQL 数据库的访问。它提供了一系列接口方法,用于访问关系数据库但并不会提供数据库特有的方法,那些特有的方法交给数据库驱动去实现。
对于数据库操作来说,开发者不应该直接使用导入的驱动包所提供的方法,而应该使用 sql.DB 对象所提供的统一的方法。因此在导入 MySQL 驱动时,使用了匿名导入包的方式,即将 go-sql-driver 包重命名为特殊符号 _。采用这种方式只会执行其中的 init 函数和初始化其全局变量,无法调用函数。
3. 连接数据库
连接数据库使用 sql 包中的 Open() 函数,原型如下
| |
- driverName:使用的驱动名。这个名字其实就是数据库驱动注册到 database/sql 时所使用的名字
- dataSourceName:数据库连接信息。它包含了数据库的用户名、密码、数据库主机以及需要连接的数据库名等信息。
使用示例如下
| |
sql.Open() 返回的 sql.DB 对象是 Goroutine 并发安全的。sql.DB 通过数据库驱动为开发者提供管理底层数据库连接的打开和关闭操作。sql.DB 帮助开发者管理数据库连接池。正在使用的连接被标记为繁忙,用完后回到连接池等待下次使用。所以,如果开发者没有把连接释放回连接池,会导致过多连接使系统资源耗尽。
sql.DB 的设计目标就是作为长连接(一次连接多次数据交互)使用,不宜频繁开关。比较好的做法是,为每个不同的 datastore 建一个DB 对象,保持这些对象打开。如果需要短连接(一次连接一次数据交互),就把 DB 作为参数传入函数,而不要在函数中开关。
4. 增删改数据
直接调用DB对象的 Exec() 方法如下所示
| |
通过db.Exec()插入数据,通过返回的 err 可知插入失败的原因,通过返回的结果可以进一步查询本次插入数据影响的行数(RowsAffected)和最后插入的ID(如果数据库支持查询最后插入ID)。事实上,Result 是对已执行的 SQL 命令的总结,类型定义如下
| |
Exec() 方法的使用方式如下所示
| |
预编译语句(PreparedStatement)提供了诸多好处。PreparedStatement 可以实现自定义参数的查询,通常来说比手动拼接字符串SQL语句高效;PreparedStatement 还可以防止SQL注入攻击。因此,开发中应尽量使用它。
通常使用 PreparedStatement 和 Exec() 完成 INSERT、UPDATE、DELETE 操作。使用DB对象的Prepare() 方法获得预编译对象 stmt,然后调用 Exec() 方法,语法如下所示。
| |
具体用法如下
| |
获取影响数据库的行数,可以根据该数值判断是否操作(插入、删除或修改)成功。语法如下所示。
| |
预编译对象 stmt 属于 Stmt 类型,是一个准备好的状态,可以安全地被多个 Goroutine 同时使用,类型定义与方法集如下,定义在 DB 对象上的 Exec 和 Stmt 对象上的 Exec 传入参数有区别,后者不需要 SQL 语句,但作用应该是相同的。
| |
5. 查询数据
数据查询的一般步骤如下
调用 db.Query() 方法执行 SQL 语句,此方法返回一个 Rows 作为查询结果,语法如下所示
1func (db *DB) Query(query string, args ...interface{}) (*Rows, error)注意,Rows 作为查询结果,其游标指向结果集的第零行
将 rows.Next() 方法的返回值作为 for 循环的条件,迭代查询数据,语法如下所示。
1func (rs *Rows) Next() boolNext 的返回值不是简单的一个对下一个结果是否存在的判断,而是准备下一行结果用于 Scan 方法进行扫描,如果准备好,返回 true,如果没有下一行或准备时出现错误,返回 false
在循环中,通过 rows.Scan()方法读取每一行数据,语法如下所示。
1func (rs *Rows) Scan(dest ...interface{}) errorrows.Scan() 方法的参数顺序很重要,必须和查询结果的列相对应(数量和顺序都需要一致)。假设
SELECT * From user_info where age >=20 AND age < 30查询的列顺序是id, name, age,和插入操作顺序相同,rows.Scan() 的参数传入也需要按照此顺序rows.Scan(&id, &name, &age),不然会造成数据读取的错位。调用db.Close()关闭查询,Close 关闭 DB对象,释放任何打开的资源,但实际上因为 DB 句柄通常被多个 Go 协程共享,不会被关闭。
查询多行数据的一个完整示例如下所示
| |
因为 Golang 是强类型语言,所以查询数据时先定义数据类型。数据库中的数据有3种可能状态:存在值、存在零值、未赋值,因此可以将待查询的数据类型定义为 sql.NullString、sql.NullInt64 类型等。可以通过 Valid 值来判断查询到的值是赋值状态还是未赋值状态。
每次 db.Query() 操作后,都建议调用 rows.Close()。因为 db.Query() 会从数据库连接池中获取一个连接,这个底层连接在结果集(rows)未关闭前会被标记为处于繁忙状态。当遍历读到最后一条记录时,会发生一个内部 EOF 错误,自动调用 rows.Close()。但如果出现异常,提前退出循环,rows 不会关闭,连接不会回到连接池中,连接也不会关闭,则此连接会一直被占用。因此通常使用 defer rows.Close() 来确保数据库连接可以正确放回到连接池中。rows.Close() 操作是幂等操作,而一个幂等操作的特点是:其任意多次执行所产生的影响与一次执行的影响相同。所以即便对已关闭的 rows 再执行 close() 也没关系。
谈到这里,我们可以注意到 Close 方法一共有三种,分别定义在 DB、Stmt、Rows 三个方法上,用于关闭数据库连接、预准备状态和查询结果,不过本质上都是释放某个连接池中的连接。
单条数据通过 QueryRow() 方法查询,语法如下所示。
| |
使用示例如下
| |
6. 示例代码
定义一个表结构如下
| |
编写一个完整的测试代码,使用上述提到的所有数据库操作
| |
运行结果为
| |
https://weread.qq.com/web/reader/df83279071b1ee24df86404k07c32570311607cdfd23f04