数据库用来存储数据。只要不是玩具项目,每一个项目都须要用到数据库。如今用的最多的仍是 MySQL,PostgreSQL的使用也在快速增加中。 在 Web 开发中,数据库也是必须的。本文将介绍如何在 Go 语言中操做数据库,基于 MySQL。本文假定你们已经掌握了数据库和 MySQL 的基础知识。 关于 MySQL 有一个很是详细的免费教程我放在参考中了,须要的自取。mysql
Go 语言标准库database/sql
只是提供了一组查询和操做数据库的接口,没有提供任何实现。在 Go 中操做数据库只能使用第三方库。 各类类型的数据库都有对应的第三方库。Go 中支持 MySQL 的驱动中最多见的是go-sql-driver/mysql。 该库支持database/sql
,所有采用 go 实现。git
建立一个数据库department
,表示公司中的某个部门。 在该库中建立两张表employees
和teams
。employees
记录员工信息,teams
记录小组信息。 每一个员工都属于一个小组,每一个小组都有若干名员工。github
SET NAMES utf8mb4;
CREATE DATABASE IF NOT EXISTS `department`
CHARACTER SET utf8mb4
COLLATE utf8mb4_general_ci;
USE `department`;
CREATE TABLE IF NOT EXISTS `employees` (
`id` INT(11) AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(255) NOT NULL DEFAULT '',
`age` INT(11) NOT NULL DEFAULT 0,
`salary` INT(11) NOT NULL DEFAULT 0,
`team_id` INT(11) NOT NULL DEFAULT 0
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS `teams` (
`id` INT(11) AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(255) NOT NULL DEFAULT ''
) ENGINE=InnoDB;
INSERT INTO `teams`(`name`)
VALUES
('策划'),
('开发'),
('运营'),
('运维');
INSERT INTO `employees`(`name`, `age`, `salary`, `team_id`)
VALUES
('张三', 28, 1200, 1),
('李四', 38, 4000, 1),
('王五', 36, 3500, 1),
('赵六', 31, 3100, 2),
('田七', 29, 2900, 2),
('吴八', 27, 1500, 3),
('朱九', 26, 1600, 3),
('钱十', 27, 1800, 3),
('陶十一', 28, 1900, 4),
('汪十二', 25, 2000, 4),
('剑十三', 24, 30000, 4);
复制代码
插入一些测试数据。将这个department.sql
文件保存到某个目录,而后在该目录打开命令行:golang
$ mysql -u root -p
复制代码
输入密码链接到数据库,而后输入如下命令:web
mysql> source department.sql
Query OK, 0 rows affected (0.00 sec)
Query OK, 2 rows affected (0.02 sec)
Query OK, 1 row affected (0.00 sec)
Database changed
Query OK, 0 rows affected, 4 warnings (0.02 sec)
Query OK, 0 rows affected, 1 warning (0.02 sec)
Query OK, 4 rows affected (0.01 sec)
Records: 4 Duplicates: 0 Warnings: 0
Query OK, 11 rows affected (0.00 sec)
Records: 11 Duplicates: 0 Warnings: 0
mysql>
复制代码
这样数据库和表就建立好了。sql
go-sql-driver/mysql
是第三方库,须要安装:数据库
$ go get github.com/go-sql-driver/mysql
复制代码
使用:bash
package main
import (
"database/sql"
"log"
_ "github.com/go-sql-driver/mysql"
)
func main() {
db, err := sql.Open("mysql", "root:12345@tcp(127.0.0.1:3306)/department")
if err != nil {
log.Fatal("connect database failed: ", err)
}
defer db.Close()
}
复制代码
咱们操做数据库并非直接使用mysql
库,而是经过database/sql
的接口。微信
import _ "github.com/go-sql-driver/mysql"
复制代码
上面代码导入mysql
,但并不直接使用,而是利用导入的反作用执行mysql
库的init
函数,将mysql
驱动注册到database/sql
中:网络
// go-sql-driver/mysql/driver.go
func init() {
sql.Register("mysql", &MySQLDriver{})
}
复制代码
而后在程序中使用sql.Open
建立一个sql.DB
结构,参数一即为mysql
库注册的名字,参数二实际上就是指定数据库链接信息的。 每一个数据库接受的链接信息是不一样的。对于 MySQL 来讲,链接信息其实是一个 DSN (Data Source Name)。DSN 的通常格式为:
[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]
复制代码
示例中使用的就是一个 DSN,指定用户名为root
,密码为12345
, 经过 tcp 协议链接到 ip 为127.0.0.1
,端口为 3306 的 MySQL 的department
数据库上。
在使用完成后,须要调用db.Close
关闭sql.DB
。
**须要特别注意的是,sql.Open
并不会创建到数据库的链接,它也不会检测驱动的链接参数。它仅仅建立了一个数据库抽象层给后面使用。 到数据库的链接实际上会在须要的时候惰性地建立。**因此,咱们使用一个非法的用户名或密码,链接一个主机上不存在的库,sql.Open
也不会报错。 将上面的 DSN 改成user:password@tcp(127.0.0.1:6666)/not_exist_department
,运行程序,没有报错。
若是想要检测数据库是否可访问,可使用db.Ping()
函数:
err = db.Ping()
if err != nil {
log.Fatal("ping failed: ", err)
}
复制代码
这时链接not_exist_department
会报错:
2020/01/20 22:16:12 ping failed: Error 1049: Unknown database 'not_exist_department'
exit status 1
复制代码
sql.DB
对象通常做为某种形式的全局变量长期存活。不要频繁打开、关闭该对象。这对性能会有很是大的影响。
先看一个简单示例:
package main
import (
"database/sql"
"log"
_ "github.com/go-sql-driver/mysql"
)
func main() {
db, err := sql.Open("mysql", "root:12345@tcp(127.0.0.1:3306)/department")
if err != nil {
log.Fatal("open database failed: ", err)
}
defer db.Close()
var id int
var name string
var age int
var salary int
var teamId int
rows, err := db.Query("select id, name, age, salary, team_id from employees where id = ?", 1)
if err != nil {
log.Fatal("query failed: ", err)
}
defer rows.Close()
for rows.Next() {
err := rows.Scan(&id, &name, &age, &salary, &teamId)
if err != nil {
log.Fatal("scan failed: ", err)
}
log.Printf("id: %d name:%s age:%d salary:%d teamId:%d\n", id, name, age, salary, teamId)
}
err = rows.Err()
if err != nil {
log.Fatal(err)
}
}
复制代码
运行程序,输出:
2020/01/20 22:27:21 id: 1 name:张三 age:28 salary:1200 teamId:1
复制代码
从上面程序中,咱们看到一个查询操做的基本流程:
db.Query()
查询数据库;rows.Scan()
读取各列的值,rows.Next()
将“指针”移动到下一行;rows.Next()
将返回 false,循环退出。数据库操做可能会遇到各类各样的错误,因此错误处理很重要。例如,在循环中调用rows.Scan
可能产生错误。
遍历结束后,必定要关闭rows
。由于它持有链接的指针,不关闭会形成资源泄露。rows.Next()
遇到最后一行时会返回一个 EOF 错误,并关闭链接。 另外,若是rows.Next()
因为产生错误返回 false,rows
也会自动关闭。其它状况下,若是提早退出循环,可能会忘记关闭rows
。 因此通常使用defer rows.Close()
确保正常关闭。
Tips:
调用Scan
方法时,其内部会根据传入的参数类型执行相应的数据类型转换。利用这个特性能够简化代码。 例如,MySQL 中某一列是VARCHAR/CHAR
或相似的文本类型,可是咱们知道它保存的是一个整数。 那么就能够传入一个int
类型的变量,Scan
内部会帮助咱们将字符串转为int
。免除了咱们手动调用strconv
相关方法的麻烦。
database/sql
中函数的命名特别讲究:
Query*
这种以Query
开头的函数,确定返回若干行(可能为 0)数据;Query*
函数,应该使用Exec
。当咱们须要屡次执行同一条语句时,最好的作法是先建立一个PreparedStatement
。这个PreparedStatement
能够包含参数占位符,后续执行时再提供参数。
每种数据库都有本身参数占位符,MySQL 使用的是?
。使用参数占位符有一个明显的好处:能避免SQL 注入攻击。
须要执行 SQL 时,传入参数调用PreparedStatement
的Query
方法便可:
func main() {
db, err := sql.Open("mysql", "root:12345@tcp(127.0.0.1:3306)/department")
if err != nil {
log.Fatal("open failed: ", err)
}
defer db.Close()
stmt, err := db.Prepare("select id, name, age, salary from employees where id = ?")
if err != nil {
log.Fatal("prepare failed: ", err)
}
defer stmt.Close()
rows, err := stmt.Query(2)
if err != nil {
log.Fatal("query failed: ", err)
}
defer rows.Close()
var (
id int
name string
age int
salary int
)
for rows.Next() {
err := rows.Scan(&id, &name, &age, &salary)
if err != nil {
log.Fatal("scan failed: ", err)
}
log.Printf("id:%d name:%s age:%d salary:%d\n", id, name, age, salary)
}
err = rows.Err()
if err != nil {
log.Fatal(err)
}
}
复制代码
实际上,在db.Query()
函数内部,会先建立一个PreparedStatement
,执行它,而后关闭。这会与数据库产生 3 次通讯。因此尽可能先建立PreparedStatement
,再使用。
若是查询最多只返回一行数据,咱们不用写循环处理,使用QueryRow
能够简化代码编写。
直接调用db.QueryRow
:
var name string
err = db.QueryRow("select name from employees where id = ?", 1).Scan(&name)
if err != nil {
log.Fatal(err)
}
fmt.Println(name)
复制代码
也能够在PreparedStatement
上调用QueryRow
:
stmt, err := db.Prepare("select name from employees where id = ?").Scan(&name)
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
var name string
err = stmt.QueryRow(1).Scan(&name)
if err != nil {
log.Fatal(err)
}
fmt.Println(name)
复制代码
注意,QueryRow
遇到的错误会延迟到调用Scan
时才返回。
INSERT/UPDATE/DELETE
这些操做,因为都不返回行,应该使用Exec
函数。建议先建立PreparedStatement
再执行。
如今“策划组”新加入了一名员工:
func main() {
db, err := sql.Open("mysql", "root:12345@tcp(127.0.0.1:3306)/department")
if err != nil {
log.Fatal("open failed: ", err)
}
defer db.Close()
stmt, err := db.Prepare("INSERT INTO employees(name, age, salary, team_id) VALUES(?,?,?,?)")
if err != nil {
log.Fatal("prepare failed: ", err)
}
defer stmt.Close()
res, err := stmt.Exec("柳十四", 32, 5000, 1)
if err != nil {
log.Fatal("exec failed: ", err)
}
lastId, err := res.LastInsertId()
if err != nil {
log.Fatal("fetch last insert id failed: ", err)
}
rowCnt, err := res.RowsAffected()
if err != nil {
log.Fatal("fetch rows affected failed: ", err)
}
log.Printf("ID = %d, affected = %d\n", lastId, rowCnt)
}
复制代码
Exec
方法返回一个sql.Result
接口类型的值:
// src/database/sql/sql.go
type Result interface {
LastInsertId() (int64, error)
RowsAffected() (int64, error)
}
复制代码
有些表设置了自增的 id,插入时不须要设置 id,数据库会自动生成一个返回。LastInsertId()
返回插入时生成的 id。 RowsAffected()
返回受影响的行数。
运行程序,输出:
2020/01/21 07:20:26 ID = 12, affected = 1
复制代码
在 Go 中,事务本质上是一个对象,它持有一个到数据库的链接。经过该对象执行咱们上面介绍的方法时, 都会使用这个相同的链接。调用db.Begin()
建立一个事务对象,而后在该对象上执行上面的方法, 最后成功调用Commit()
,失败调用Rollback()
关闭事务。
func main() {
db, err := sql.Open("mysql", "root:12345@tcp(127.0.0.1:3306)/department")
if err != nil {
log.Fatal("open failed: ", err)
}
defer db.Close()
tx, err := db.Begin()
if err != nil {
log.Fatal("begin failed: ", err)
}
defer tx.Rollback()
stmt, err := tx.Prepare("UPDATE employees SET team_id=? WHERE id=?")
if err != nil {
log.Fatal("prepare failed: ", err)
}
defer stmt.Close()
_, err = stmt.Exec(2, 1)
if err != nil {
log.Fatal("exec failed: ", err)
}
tx.Commit()
}
复制代码
注意,在事务内部不能再直接调用db
的方法了,由于db
使用的是与事务不一样的链接,可能会致使执行结果的不一致。
database/sql
中几乎全部的操做最后一个返回值都是一个error
类型。数据库会出现各类各样的错误,咱们应该时刻检查是否出现了错误。下面介绍几种特殊状况产生的错误。
for rows.Next() {
// ...
}
if err = rows.Err(); err != nil {
}
复制代码
``rows.Err()返回的错误多是
rows.Next()循环中的多种错误。循环可能因为某些缘由提早退出了。咱们应该检测循环是否正常退出。 异常退出时,
database/sql会自动调用
rows.Close()。提早退出时,咱们须要手动调用
rows.Close()。**能够屡次调用
rows.Close()`**。
实际上,rows.Close()
也返回一个错误。可是,对于这个错误,咱们能作的事情比较有限。一般就是记录日志。 若是不须要记录日志,一般会忽略这个错误。
考虑下面的代码:
var name string
err = db.QueryRow("SELECT name FROM employees WHERE id = ?", 1).Scan(&name)
if err != nil {
log.Fatal(err)
}
fmt.Println(name)
复制代码
若是没有id = 1
的员工,Scan()
要如何处理?
Go 定义了一个特殊的错误常量,sql.ErrNoRows
。若是没有符合要求的行,QueryRow
将返回这个错误。 这个错误在大多数状况下须要特殊处理,由于没有结果在应用层一般不认为是错误。
var name string
err = db.QueryRow("SELECT name FROM employees WHERE id = ?", 1).Scan(&name)
if err != nil {
if err == sql.ErrNoRows {
} else {
log.Fatal(err)
}
}
fmt.Println(name)
复制代码
那为何QueryRow
在没有符合要求的行时返回一个错误?
由于要区分是否返回了行,若是返回空结果集,因为Scan()
不会作任什么时候间,咱们就不能区分name
读取到了空字符串,仍是初始值。
为了辨别发生了何种错误,有一种作法是检查错误描述中是否有特定的文本:
rows, err := db.Query("SELECT someval FROM sometable")
if err != nil {
if strings.Contains(err.Error(), "Access denied") {
}
}
复制代码
可是不推荐这种作法,由于不一样的数据库版本,这些描述不必定能保持一致。
比较好的作法是将错误转成特定数据库驱动的错误,而后比较错误码:
if driverErr, ok := err.(*mysql.MySQLError); ok {
if driverErr.Number == 1045 {
}
}
复制代码
不一样驱动间判断方法可能不一样。另外,直接写数字1045
也不太好,VividCortex 整理了 MySQL 错误码,GitHub 仓库为mysqlerr。使用库后续便于修改:
if driverErr, ok := err.(*mysql.MySQLError); ok {
if driverErr.Number == mysqlerr.ER_ACCESS_DENIED_ERROR {
}
}
复制代码
有时候,可能咱们不能肯定查询返回多少列。可是Scan()
要求传入正确数量的参数。为此,咱们能够先使用rows.Columns()
返回全部列名,而后建立一样大小的字符串指针切片传给Scan()
函数:
func main() {
db, err := sql.Open("mysql", "root:12345@tcp(127.0.0.1:3306)/department")
if err != nil {
log.Fatal("open failed: ", err)
}
defer db.Close()
stmt, err := db.Prepare("SELECT * FROM employees")
if err != nil {
log.Fatal("prepare failed: ", err)
}
defer stmt.Close()
rows, err := stmt.Query()
if err != nil {
log.Fatal("exec failed: ", err)
}
defer rows.Close()
cols, err := rows.Columns()
if err != nil {
log.Fatal("columns failed: ", err)
}
data := make([]interface{}, len(cols), len(cols))
for i := range data {
data[i] = new(string)
}
for rows.Next() {
err = rows.Scan(data...)
if err != nil {
log.Fatal("scan failed: ", err)
}
for i := 0; i < len(cols); i++ {
fmt.Printf("%s: %s ", cols[i], *(data[i].(*string)))
}
fmt.Println()
}
if err = rows.Err(); err != nil {
log.Fatal(err)
}
}
复制代码
运行程序:
id: 1 name: 张三 age: 28 salary: 1200 team_id: 2
id: 2 name: 李四 age: 38 salary: 4000 team_id: 1
id: 3 name: 王五 age: 36 salary: 3500 team_id: 1
id: 4 name: 赵六 age: 31 salary: 3100 team_id: 2
id: 5 name: 田七 age: 29 salary: 2900 team_id: 2
id: 6 name: 吴八 age: 27 salary: 1500 team_id: 3
id: 7 name: 朱九 age: 26 salary: 1600 team_id: 3
id: 8 name: 钱十 age: 27 salary: 1800 team_id: 3
id: 9 name: 陶十一 age: 28 salary: 1900 team_id: 4
id: 10 name: 汪十二 age: 25 salary: 2000 team_id: 4
id: 11 name: 剑十三 age: 24 salary: 30000 team_id: 4
id: 12 name: 柳十四 age: 32 salary: 5000 team_id: 1
复制代码
database/sql
实现了一个基本的链接池。链接池有一些有趣的特性,了解一下,避免踩坑:
LOCK TABLES
,而后执行INSERT
可能会阻塞;too many connections
错误;db.SetMaxIdleConns(N)
限制池中最大空闲链接数;db.SetMaxOpenConns(N)
限制全部打开的链接数;db.SetConnMaxLifeTime(duration)
设置链接最大存活时间。本文介绍了如何在 Go 中查询和修改数据库,主要是database/sql
和go-sql-driver/mysql
库的用法。database/sql
的接口并不复杂,可是不少细节须要注意。一不留神可能就有资源泄露。
欢迎关注个人微信公众号【GoUpUp】,共同窗习,一块儿进步~
本文由博客一文多发平台 OpenWrite 发布!