Go语言内置了一个通用的数据库操作接口,位于database/sql
包,但标准库没有具体的实现和数据库驱动,这和Java有JDBC但需要额外加载数据库驱动类似。JDBC接口的设计其实滥用了受检查异常,导致代码异常难写,而且因为历史原因,也就只能那样了,Go语言则稍微好用一点。
我们这里以MySQL为例进行介绍,网上大多数使用Go语言连接MySQL,都是使用这个第三方Go语言驱动,项目地址:https://github.com/go-sql-driver/mysql/
该项目开源协议是MPL2.0
,使用时要注意一下,另外重要的上线项目要对这些看起来不是太靠谱的库进行充分的代码审计,不要留下安全隐患。
我们直接使用go get
下载下来使用:
go get github.com/go-sql-driver/mysql
在代码中引入:import _ "github.com/go-sql-driver/mysql"
下面代码段演示了Go语言中如何连接到MySQL数据库。
db, err := sql.Open("mysql", "root:root@tcp(127.0.0.1:3306)/netstore")
if err != nil {
fmt.Println(err)
return
}
代码中我们调用了Open()
方法,它返回了一个*DB
指针代表数据库连接。注意Go语言的连接字符串和JDBC不同,格式为用户名:密码@tcp(主机:端口)/数据库名
。
关闭连接也非常简单,我们在*DB
指针上调用Close()
方法即可。
defer func(db *sql.DB) {
err := db.Close()
if err != nil {
fmt.Println(err)
return
}
}(db)
这里为了确保连接关闭,我们可以使用defer
语句。
这里要注意的一点是DB
是一个长生命周期的对象,database/sql
内部已经实现了连接池,DB
代表的不是一个数据库连接而是一个池化的对象,具体创建连接是在调用Exec()
、Query()
等方法时。我们工程中在启动时创建1个DB
,之后的操作都在其上执行就可以了,频繁的创建和销毁DB
对象的开销是极大的,更不要误写成每次执行SQL语句都调用Open()
方法!这和Java的JDBC设计思路有些区别,Java中的数据库连接池都是在JDBC之上实现的。
对于DB对象,我们可以配置连接池的连接数,下面是一些例子。
// 设置最大空闲连接数
db.SetMaxIdleConns(3)
// 设置最大连接数
db.SetMaxOpenConns(20)
// 设置最大空闲超时时间
db.SetConnMaxIdleTime(5 * time.Minute)
查询一条数据可以使用QueryRow()
方法实现,下面代码中,我们对应数据库表结构定义了一个结构体,查询结果会被放入结构体中。
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
type Cust struct {
CustId uint64
CustName string
AreaCode string
Tel string
Email string
Address string
}
func main() {
// 获取数据库连接池对象
db, err := sql.Open("mysql", "root:root@tcp(127.0.0.1:3306)/netstore")
if err != nil {
fmt.Println(err)
return
}
// 查询一条数据
id := 1
cust := Cust{}
err = db.QueryRow("select * from t_cust where cust_id=?", id).Scan(
&cust.CustId,
&cust.CustName,
&cust.AreaCode,
&cust.Tel,
&cust.Email,
&cust.Address,
)
if err != nil {
fmt.Println(err)
return
}
// 打印数据
fmt.Printf("%v\n", cust)
// 关闭DB对象
defer func(db *sql.DB) {
err := db.Close()
if err != nil {
fmt.Println(err)
return
}
}(db)
}
代码中,我们调用了QueryRow()
方法传入了SQL语句和占位符参数,它返回一个代表结果集的*ROW
指针;Scan()
方法用于将结果集映射到结构体字段上,我们按照结果集中的字段顺序指定了结构体的字段。这里大家不要理解错了,由于Go语言的点号.
优先级高于取地址符&
,我们实际传入的参数是&(cust.CustId)
。
注:以上代码只是个例子,千万不要在正式场合写select *
,一定要把字段写全,因为这个问题不知被坑了多少次了。
此外如果数据表中字段值为NULL,而需要赋值的结构体字段类型为string
等时,上面的写法其实会出现类似如下的报错:
sql: Scan error on column index 5, name "address": converting NULL to string is unsupported
这是因为Go语言中的一个怪异设计,布尔类型、数值类型、字符串都不能被赋值为nil
,其空值分别是false
、0
和空字符串。对于这个问题我们有两个办法,一个是规范数据表的约束,不允许数据值为NULL(可以用空字符串、0
值等代替);另一种方法就需要修改代码了:
type Cust struct {
CustId uint64
CustName string
AreaCode string
Tel string
Email string
Address sql.NullString
}
这里假设我们的Address
字段可能出现NULL
值,那么我们需要将其定义为sql.NullString
类型,此时即使数据值为NULL
也不会报错了,其内部值会被设置为空字符串。sql.NullString
类型结构体包含2个字段,String
表示真实的数据记录值,不存在会被设置为空字符串,Valid
会在值存在时返回true
,对于数值类型也是类似的。
查询多条数据时,可以使用Query()
方法,返回值类型为*Rows
指针,我们可以通过其Next()
方法遍历查询结果集,取出每条记录并映射到结构体上。下面是一个例子。
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
type Cust struct {
CustId uint64
CustName string
AreaCode string
Tel string
Email string
Address string
}
func main() {
// 获取数据库连接池对象
db, err := sql.Open("mysql", "root:root@tcp(127.0.0.1:3306)/netstore")
if err != nil {
fmt.Println(err)
return
}
// 查询数据
rows, err := db.Query("select * from t_cust")
if err != nil {
fmt.Println(err)
return
}
// 遍历结果集
for rows.Next() {
cust := Cust{}
err = rows.Scan(
&cust.CustId,
&cust.CustName,
&cust.AreaCode,
&cust.Tel,
&cust.Email,
&cust.Address,
)
if err != nil {
fmt.Println(err)
return
}
// 打印数据
fmt.Printf("%v\n", cust)
}
// 关闭结果集释放连接
defer func(rows *sql.Rows) {
err := rows.Close()
if err != nil {
fmt.Println(err)
return
}
}(rows)
// 关闭DB对象
defer func(db *sql.DB) {
err := db.Close()
if err != nil {
fmt.Println(err)
return
}
}(db)
}
我们在循环使用用到了rows.Next()
方法,读取第1条结果前我们也必须先调用该方法,每次调用它时,它会在结果集没有遍历完成前返回true
并将内部的偏移量加1,当结果集全部全部遍历完成后会返回false
,这类似于一种迭代器模式。在循环内部,我们调用了rows.Scan()
方法,它用于将当前的结果集映射到结构体。
增删改写法都是一样的,都是执行一条SQL语句即可,这里以增加一条数据为例。
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
type Cust struct {
CustId uint64
CustName string
AreaCode string
Tel string
Email string
Address string
}
func main() {
// 获取数据库连接池对象
db, err := sql.Open("mysql", "root:root@tcp(127.0.0.1:3306)/netstore")
if err != nil {
fmt.Println(err)
return
}
// 执行SQL语句
result, err := db.Exec("insert into t_cust (cust_name,area_code) values ('Tom','10')")
// 打印执行结果信息
rowsAffected, _ := result.RowsAffected()
lastInsertId, _ := result.LastInsertId()
fmt.Printf("Rows affected: %v\nLast insert id: %v\n", rowsAffected, lastInsertId)
// 关闭DB对象
defer func(db *sql.DB) {
err := db.Close()
if err != nil {
fmt.Println(err)
return
}
}(db)
}
代码中我们调用了Exec()
方法执行了一条SQL语句,这里我们的参数是固定在SQL语句中的,如果有动态的参数Exec()
方法也支持使用?
占位符的形式,该方法返回了一个Result
对象,其中包含了2个很有用的信息,SQL执行受影响的行数和最后插入数据的主键。
前面我们插入数据时没有开启事务,database/sql
也支持事务方式对数据进行更新,下面是一个例子。
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
type Cust struct {
CustId uint64
CustName string
AreaCode string
Tel string
Email string
Address string
}
func main() {
// 获取数据库连接池对象
db, err := sql.Open("mysql", "root:root@tcp(127.0.0.1:3306)/netstore")
if err != nil {
fmt.Println(err)
return
}
// 开启事务
tx, err := db.Begin()
if err != nil {
fmt.Println(err)
return
}
// 执行SQL语句
result, err := tx.Exec("insert into t_cust (cust_name,area_code) values ('Tom','10')")
// 打印执行结果信息
rowsAffected, _ := result.RowsAffected()
lastInsertId, _ := result.LastInsertId()
fmt.Printf("Rows affected: %v\nLast insert id: %v\n", rowsAffected, lastInsertId)
// 提交事务
err = tx.Commit()
if err != nil {
fmt.Println(err)
return
}
// 关闭DB对象
defer func(db *sql.DB) {
err := db.Close()
if err != nil {
fmt.Println(err)
return
}
}(db)
}
代码和之前不同的是,我们在*DB
上调用了Begin()
和Commit()
开启和关闭事务,Begin()
方法会返回*Tx
指针代表一个事务上下文,我们调用Exec()
等方法需要在*Tx
指针上调用。
类似于Java的JDBC,Go语言也提供了PreparedStatement预编译SQL语句,PreparedStatement适用于重复执行相同或是类似的SQL语句,例如批量插入、批量更新等场景,GoLang底层对其进行了优化,具有更好的执行效率。
// 获取PreparedStatement
stmt, err := db.Prepare("insert into t_cust (cust_name,area_code) values ('Tom','10')")
if err != nil {
fmt.Println(err)
return
}
// 执行预编译SQL
result, err := stmt.Exec()
// 关闭预编译SQL
defer func(stmt *sql.Stmt) {
err := stmt.Close()
if err != nil {
fmt.Println(err)
return
}
}(stmt)
代码中,我们调用了Prepare()
方法获取了一个*Stmt
指针,它代表一个预编译SQL对象,随后我们调用Exec()
方法,如果有?
占位符参数,也可以在这个方法中传入,此时SQL才真正被发送到数据库服务器并执行。