这一期讲一讲如何使用 Go 操作 MySQL 数据库,这里就不讲 MySQL 的安装以及配置了,但要记得开启 MySQL 服务,我这里使用的是 MySQL 8.0.20 版本。
加载数据库驱动
想要连接到数据库,首先需要加载目标数据库的驱动,驱动里面包含着与该数据库交互的逻辑。在 Go 中我们使用 sql 包下的 Open()
方法设置连接数据库的参数:
func Open(driverName, dataSourceName string) (*DB, error)
第一个参数是数据库驱动的名称,第二个参数是数据源名称,该函数会返回一个指向 sql.DB
的指针,结构体 sql.DB
如下:
type DB struct {
waitDuration int64
connector driver.Connector
numClosed uint64
mu sync.Mutex
freeConn []*driverConn
connRequests map[uint64]chan connRequest
nextRequest uint64
numOpen int
openerCh chan struct{}
closed bool
dep map[finalCloser]depSet
lastPut map[*driverConn]string
maxIdleCount int
maxOpen int
maxLifetime time.Duration
maxIdleTime time.Duration
cleanerCh chan struct{}
waitCount int64
maxIdleClosed int64
maxIdleTimeClosed int64
maxLifetimeClosed int64
stop func()
}
sql.DB
是用来操作数据库的,它代表了 0 个或者多个底层连接池,这些连接都由 sql 包维护,该包会自动地创建和释放这些连接,它是线程安全的。
Open()
函数并不会连接数据库,甚至不会验证其参数。它只是把后续连接到数据库所必需的 structs 给设置好了,而真正的连接是在被需要的时候才进行懒设置的。
sql.DB
不需要进行关闭(当然你想关闭也是可以的),它就是用来处理数据库的,而不是实际的连接。这个抽象包含了数据库连接的池,而且会对此进行维护。在使用 sql.DB
的时候,可以定义它的全局变量进行使用,也可以将它传递函数/方法里。
正常获得驱动的做法是使用 sql.Register()
函数:
func Register(name string, driver driver.Driver)
该函数需传入数据库驱动的名称和一个实现了 driver.Driver
接口的结构体,来注册数据库的驱动。
第三方驱动自动注册
当第三方数据库包被引入到时候,它的 init
函数将会运行并进行自我注册。
并且在引入包的时候,把该包的名设置为下划线 _
,这是因为我们不直接使用数据库驱动,我们只使用 database/sql ,如果未来升级驱动,也无需改变代码。
Go 没有提供官方的数据库驱动,所有的数据库驱动都是第三方驱动,但是它们都遵循 sql.driver
包里面定义的接口,我们需要安装第三方函数库。当然,安装第三方库之前,因为你所知道的某些原因,可能会出现下载安装失败的问题,所以先要配置代理。打开命令行,在项目目录下执行下面两条命令:
go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.io,direct
使用下面的命令初始化项目:
go mod init go-db(项目名称)
然后我们再使用 go get
命令安装第三方库。
go get -u github.com/go-sql-driver/mysql
接着,我们先在 MySQL 中创建一个名为 godb_test 的数据库:
CREATE DATABASE godb_test;
进入该数据库:
use godb_test;
执行以下命令创建一张用于测试的用户数据表:
CREATE TABLE `acl_user` (
`id` bigint unsigned NOT NULL COMMENT '会员ID',
`phone` char(11) NOT NULL COMMENT '手机号码',
`nick_name` varchar(50) DEFAULT NULL COMMENT '昵称',
`age` int DEFAULT '0' COMMENT '年龄',
`is_deleted` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '逻辑删除1(true)已删除,0(false)未删除',
PRIMARY KEY (`id`)
) COMMENT='用户表';
测试数据库连接
接着我们用 Go 编写连接数据库代码,测试数据库能否连接成功:
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
func main() {
// 连接数据库
db, _ := sql.Open("mysql", "root:root@(127.0.0.1:3306)/godb_test?charset=utf8mb4")
// Ping() 验证与数据库的连接是否仍处于活动状态,并在必要时建立连接
err := db.Ping()
if err != nil {
fmt.Println("Database connection failed")
return
}
fmt.Println("Database connection succeeded")
// 延迟调用关闭数据库 阻止新的查询
defer db.Close()
}
上面的代码中,连接参数可以有以下几种格式:
user@unix(/path/to/socket)/dbname?charset=utf8
user:password@tcp(localhost:5555)/dbname?charset=utf8
user:password@/dbname
user:password@tcp([de:ad:be:ef::ca:fe]:80)/dbname
通常我们使用第二种。运行该程序,输出如下:
Database connection succeeded
证明测试数据库连接成功。
插入操作
插入操作我们使用的是 Exec()
方法:
func (db *DB) Exec(query string, args ...interface{}) (Result, error) {
return db.ExecContext(context.Background(), query, args...)
}
func (tx *Tx) Exec(query string, args ...interface{}) (Result, error) {
return tx.ExecContext(context.Background(), query, args...)
}
func (s *Stmt) Exec(args ...interface{}) (Result, error) {
return s.ExecContext(context.Background(), args...)
}
Exec()
执行一次命令(包括查询、删除、更新、插入等),返回的 Result
是对已执行的 SQL 命令的总结。参数 args
表示 query 中的占位参数。下面是插入数据的例子:
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
func main() {
// 连接数据库
db, _ := sql.Open("mysql", "root:root@(127.0.0.1:3306)/godb_test?charset=utf8mb4")
// Ping() 验证与数据库的连接是否仍处于活动状态,并在必要时建立连接
err := db.Ping()
if err != nil {
fmt.Println("Database connection failed")
return
}
fmt.Println("Database connection succeeded")
// 延迟调用关闭数据库 阻止新的查询
defer db.Close()
// 准备 SQL 语句
stmt, err := db.Prepare(`insert into acl_user(id, phone, nick_name, age) values (?,?,?,?)`)
if err != nil {
fmt.Println("Prepare fail")
return
}
fmt.Println("Prepare succeeded")
// 将参数传递到 SQL 语句中并执行
_, err = stmt.Exec("101", "13112345678", "caizi", 20)
if err != nil {
fmt.Println("Exec fail")
return
}
fmt.Println("Exec succeeded")
}
运行该程序后,输出如下证明已经成功地插入了数据:
Database connection succeeded
Prepare succeeded
Exec succeeded
修改操作
修改操作与上面的插入操作基本一致,下面是修改数据的例子:
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
func main() {
// 连接数据库
db, _ := sql.Open("mysql", "root:root@(127.0.0.1:3306)/godb_test?charset=utf8mb4")
// Ping() 验证与数据库的连接是否仍处于活动状态,并在必要时建立连接
err := db.Ping()
if err != nil {
fmt.Println("Database connection failed")
return
}
fmt.Println("Database connection succeeded")
// 延迟调用关闭数据库 阻止新的查询
defer db.Close()
// 准备 SQL 语句
stmt, err := db.Prepare(`update acl_user set phone = ?, age = ? where id = ? and is_deleted = 0`)
if err != nil {
fmt.Println("Prepare fail")
return
}
fmt.Println("Prepare succeeded")
// 将参数传递到 SQL 语句中并执行
_, err = stmt.Exec("13122222223", 18, "101")
if err != nil {
fmt.Println("Exec fail")
return
}
fmt.Println("Update succeeded")
}
运行该程序后,输出如下证明已经成功地更新了数据:
Database connection succeeded
Prepare succeeded
Update succeeded
查询操作
查询操作分为单行查询和多行查询,单行查询使用的是 QueryRow()
方法:
func (db *DB) QueryRow(query string, args ...interface{}) *Row {
return db.QueryRowContext(context.Background(), query, args...)
}
QueryRow()
执行一次查询,并期望返回最多一行结果。QueryRow()
总是返回非 nil
的值,直到返回值的 Scan()
方法被调用时,才会返回被延迟的错误。Scan()
将查询结果赋值到对应的变量中。下面是单行查询的例子:
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
func main() {
// 连接数据库
db, _ := sql.Open("mysql", "root:root@(127.0.0.1:3306)/godb_test?charset=utf8mb4")
// Ping() 验证与数据库的连接是否仍处于活动状态,并在必要时建立连接
err := db.Ping()
if err != nil {
fmt.Println("Database connection failed")
return
}
fmt.Println("Database connection succeeded")
// 延迟调用关闭数据库 阻止新的查询
defer db.Close()
var nickName string
// QueryRow 后调用 Scan 方法,否则持有的数据库链接不会被释放
err = db.QueryRow(`select nick_name from acl_user where id = ?`, 101).Scan(&nickName)
if err != nil {
fmt.Println("Query fail")
return
}
fmt.Println("nickName: ", nickName)
}
运行上面的程序,我们得到的结果如下:
Database connection succeeded
nickName: caizi
多行查询使用的是 Query()
方法:
func (db *DB) Query(query string, args ...interface{}) (*Rows, error) {
return db.QueryContext(context.Background(), query, args...)
}
下面是多行查询的例子,在查询前我先向数据表添加了一些数据:
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
type User struct {
id int
phone string
nickName string
age int
}
func main() {
// 连接数据库
db, _ := sql.Open("mysql", "root:root@(127.0.0.1:3306)/godb_test?charset=utf8mb4")
// Ping() 验证与数据库的连接是否仍处于活动状态,并在必要时建立连接
err := db.Ping()
if err != nil {
fmt.Println("Database connection failed")
return
}
fmt.Println("Database connection succeeded")
// 延迟调用关闭数据库 阻止新的查询
defer db.Close()
rows, err := db.Query(`select id, nick_name, phone, age from acl_user where id > 0 and is_deleted = 0`)
if err != nil {
fmt.Println("Query fail")
return
}
var users []User
for rows.Next() {
var user User
err := rows.Scan(&user.id, &user.nickName, &user.phone, &user.age)
if err != nil {
fmt.Println("Rows fail")
}
users = append(users, user)
}
for _, v := range users {
fmt.Println(v)
}
}
运行该程序输出如下:
Database connection succeeded
{101 13122222223 caizi 18}
{102 13612345678 John 25}
{103 13712345689 Mary 20}
删除操作
删除操作使用的还是 Exec()
方法,下面是删除数据的例子:
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
type User struct {
id int
phone string
nickName string
age int
}
func main() {
// 连接数据库
db, _ := sql.Open("mysql", "root:root@(127.0.0.1:3306)/godb_test?charset=utf8mb4")
// Ping() 验证与数据库的连接是否仍处于活动状态,并在必要时建立连接
err := db.Ping()
if err != nil {
fmt.Println("Database connection failed")
return
}
fmt.Println("Database connection succeeded")
// 延迟调用关闭数据库 阻止新的查询
defer db.Close()
// 准备 SQL 语句
stmt, err := db.Prepare(`delete from acl_user where id = ?`)
if err != nil {
fmt.Println("Prepare fail")
return
}
fmt.Println("Prepare succeeded")
// 将参数传递到 SQL 语句中并执行
_, err = stmt.Exec("101")
if err != nil {
fmt.Println("Exec fail")
return
}
fmt.Println("Exec succeeded")
}
运行该程序输出如下,可以看到已经删除成功:
Database connection succeeded
Prepare succeeded
Exec succeeded