介绍
对许多 Web 应用程序而言,数据库都是其核心所在。数据库几乎可以用来存储你想查询和修改的任何信息。
Go 没有内置的驱动支持任何的数据库,但是 Go 定义了 database/sql 接口,用户可以基于驱动接口开发相应数据库的驱动。
database/sql 接口
Go 语言官方没有提供数据库驱动,而是为开发数据库驱动定义了一些标准接口(database/sql),开发者可以根据定义的接口来开发相应的数据库驱动,这样做有一个好处,只要是按照标准接口开发的代码, 以后需要迁移数据库时,不需要任何修改。
以下是 database/sql 定义的常用标准接口:
sql.Registe
sql.Register 函数用来注册数据库驱动,当第三方开发者开发数据库驱动时,都会实现 init 函数,在 init 里调用 Register(name string, driver driver.Driver) 完成本驱动的注册。
sqlite3 注册数据库驱动的实现如下:
// https://github.com/mattn/go-sqlite3驱动
func init() {
sql.Register("sqlite3", &SQLiteDriver{})
}
Go-SQL-Driver/MySQL 注册数据库驱动的实现如下:
func init() {
sql.Register("mysql", &MySQLDriver{})
}
第三方数据库驱动都是通过调用这个函数来注册自己的数据库驱动名称以及相应的 driver 实现。在 database/sql 内部通过一个 map 来存储用户定义的相应驱动。
var drivers = make(map[string]driver.Driver)
drivers[name] = driver
因此通过 database/sql 的注册函数可以同时注册多个数据库驱动,只要不重复。
driver.Driver
Driver 是一个数据库驱动的接口,它定义了一个方法:Open(name string),这个方法返回一个数据库的 Conn 接口。
type Driver interface {
Open(name string) (Conn, error)
}
返回的 Conn 只能用来进行一次 goroutine 的操作,也就是说不能把这个 Conn 应用于 Go 的多个 goroutine 里面。如下代码会出现错误:
go goroutineA (Conn) //执行查询操作
go goroutineB (Conn) //执行插入操作
上面的代码可能会使 Go 不知道某个操作究竟是由哪个 goroutine 发起的,从而导致数据混乱,比如可能会把 goroutineA 里面执行的查询操作的结果返回给 goroutineB 从而使 B 错误地把此结果当成自己执行的插入数据。
第三方驱动都会定义这个函数,它会解析 name 参数来获取相关数据库的连接信息,解析完成后,它将使用此信息来初始化一个 Conn 并返回它。
driver.Conn
Conn 是一个数据库连接的接口定义,它定义了一系列方法,这个 Conn 只能应用在一个 goroutine 里面,不能使用在多个 goroutine 里面,详情请参考上面的说明。
type Conn interface {
Prepare(query string) (Stmt, error)
Close() error
Begin() (Tx, error)
}
-
Prepare 函数返回与当前连接相关的执行 Sql 语句的准备状态,可以进行查询、删除等操作。
-
Close 函数关闭当前的连接,执行释放连接拥有的资源等清理工作。因为驱动实现了 database/sql 里面建议的 conn pool,所以你不用再去实现缓存 conn 之类的,这样会容易引起问题。
-
Begin 函数返回一个代表事务处理的 Tx,通过它你可以进行查询,更新等操作,或者对事务进行回滚、递交。
driver.Stmt
Stmt 是一种准备好的状态,和 Conn 相关联,而且只能应用于一个 goroutine 中,不能应用于多个 goroutine。
type Stmt interface {
Close() error
NumInput() int
Exec(args []Value) (Result, error)
Query(args []Value) (Rows, error)
}
-
Close 函数关闭当前的链接状态,但是如果当前正在执行 query,query还是有效返回 rows 数据。
-
NumInput 函数返回当前预留参数的个数,当返回 >=0 时数据库驱动就会智能检查调用者的参数。当数据库驱动包不知道预留参数的时候,返回-1。
-
Exec 函数执行 Prepare 准备好的 sql,传入参数执行 update/insert 等操作,返回 Result 数据
-
Query 函数执行 Prepare 准备好的 sql,传入需要的参数执行 select 操作,返回 Rows 结果集
driver.Tx
事务处理一般就两个过程,递交或者回滚。数据库驱动里面也只需要实现这两个函数就可以
type Tx interface {
Commit() error
Rollback() error
}
这两个函数一个用来递交一个事务,一个用来回滚事务。
driver.Execer
这是一个 Conn 可选择实现的接口
type Execer interface {
Exec(query string, args []Value) (Result, error)
}
如果这个接口没有定义,那么在调用 DB.Exec,就会首先调用 Prepare 返回 Stmt,然后执行 Stmt 的 Exec,然后关闭 Stmt。
driver.Result
这个是执行 Update/Insert 等操作返回的结果接口定义
type Result interface {
LastInsertId() (int64, error)
RowsAffected() (int64, error)
}
-
LastInsertId 函数返回由数据库执行插入操作得到的自增 ID 号。
-
RowsAffected 函数返回 query 操作影响的数据条目数。
driver.Rows
Rows 是执行查询返回的结果集接口定义
type Rows interface {
Columns() []string
Close() error
Next(dest []Value) error
}
-
Columns 函数返回查询数据库表的字段信息,这个返回的 slice 和 sql 查询的字段一一对应,而不是返回整个表的所有字段。
-
Close 函数用来关闭 Rows 迭代器。
-
Next 函数用来返回下一条数据,把数据赋值给 dest。dest 里面的元素必须是 driver.Value 的值除了 string,返回的数据里面所有的string都必须要转换成 []byte。如果最后没数据了,Next 函数最后返回 io.EOF。
driver.RowsAffected
RowsAffected 其实就是一个 int64 的别名,但是它实现了 Result 接口,用来底层实现 Result 的表示方式
type RowsAffected int64
func (RowsAffected) LastInsertId() (int64, error)
func (v RowsAffected) RowsAffected() (int64, error)
driver.Value
Value 其实就是一个空接口,它可以容纳任何的数据
type Value interface{}
drive 的 Value 是驱动必须能够操作的 Value,Value 要么是 nil,要么是下面的任意一种
int64
float64
bool
[]byte
string [*]除了Rows.Next返回的不能是string.
time.Time
driver.ValueConverter
ValueConverter 接口定义了如何把一个普通的值转化成 driver.Value 的接口
type ValueConverter interface {
ConvertValue(v interface{}) (Value, error)
}
在开发的数据库驱动包里面实现这个接口的函数在很多地方会使用到,这个 ValueConverter 有很多好处:
- 转化 driver.value 到数据库表相应的字段,例如 int64 的数据如何转化成数据库表 uint16 字段
- 把数据库查询结果转化成 driver.Value 值
- 在 scan 函数里面如何把 driver.Value 值转化成用户定义的值
driver.Valuer
Valuer 接口定义了返回一个 driver.Value 的方式
type Valuer interface {
Value() (Value, error)
}
很多类型都实现了这个 Value 方法,用来自身与 driver.Value 的转化。
总结:
一个驱动只要实现了以上这些接口就能完成增删查改等基本操作了,剩下的就是与相应的数据库进行数据交互等细节问题了。
database/sql
database/sql 在 database/sql/driver 提供的接口基础上定义了一些更高阶的方法,用以简化数据库操作,同时内部还建议性地实现一个 conn pool。
type DB struct {
driver driver.Driver
dsn string
mu sync.Mutex // protects freeConn and closed
freeConn []driver.Conn
closed bool
}
Open 函数返回的是 DB 对象,里面有一个 freeConn,它就是那个简易的连接池。它的实现相当简单或者说简陋,就是当执行 Db.prepare 的时候会 defer db.putConn(ci, err),也就是把这个连接放入连接池,每次调用 conn 的时候会先判断 freeConn 的长度是否大于0,大于0说明有可以复用的 conn,直接拿出来用就是了,如果不大于0,则创建一个 conn,然后再返回之。
使用 SQLite 数据库
SQLite 是一个开源的嵌入式关系数据库,实现自包容、零配置、支持事务的 SQL 数据库引擎。其特点是高度便携、使用方便、结构紧凑、高效、可靠。 与其它数据库管理系统不同,SQLite 的安装和运行非常简单,在大多数情况下,只要确保 SQLite 的二进制文件存在即可开始创建、连接和使用数据库。如果您正在寻找一个嵌入式数据库项目或解决方案,SQLite 是绝对值得考虑。SQLite 可以是说开源的 Access。
驱动
https://github.com/mattn/go-sqlite3 是一个支持 database/sql 接口的 SQLite 数据库驱动。
实例代码
数据库表结构如下所示,相应的建表 SQL:
CREATE TABLE `t_products` (
`id` integer not null primary key,
`name` text,
`price` float
);
Go 程序操作数据库表数据(增删改查)示例:
package main
import (
"database/sql"
"fmt"
_ "github.com/mattn/go-sqlite3" // _ 的意思是引入后面的包名而不直接使用这个包中定义的函数,变量等资源。
"os"
)
func main() {
// 移除数据库
os.Remove("./products.db")
// 打开数据库
db, err := sql.Open("sqlite3", "./products.db")
checkErr(err)
// 创建表
sqlStmt := `create table t_products(id integer not null primary key, name text, price float)`
_, err = db.Exec(sqlStmt)
checkErr(err)
defer db.Close()
// 开始事务
tx, err := db.Begin()
checkErr(err)
// 插入数据
stmt, err := tx.Prepare("insert into t_products(id,name,price) values (?,?,?)")
checkErr(err)
defer stmt.Close()
for i:=0; i < 10; i++ {
_, err = stmt.Exec(i+1,fmt.Sprintf("产品%d", i + 1), float64(i + 1) * 54.8)
checkErr(err)
}
// 提交事务
tx.Commit()
// 查询数据
rows, err := db.Query("select id,name,price from t_products")
checkErr(err)
// 延迟关闭
defer rows.Close()
for rows.Next() {
var id int
var name string
var price float64
err = rows.Scan(&id, &name, &price)
checkErr(err)
//格式化价格
priceStr := fmt.Sprintf("%.2f", price)
fmt.Println(id,name,priceStr)
}
// 带参数查询
stmt, _ = db.Prepare("select name,price from t_products where id=?")
defer stmt.Close() // 延迟关闭
var name string
var price float64
stmt.QueryRow("6").Scan(&name, &price)
priceStr := fmt.Sprintf("%.2f", price) // 格式化价格
fmt.Println("-------------------------")
fmt.Println(name,priceStr)
// 删除数据
stmt,_ = db.Prepare("delete from t_products where id=?")
stmt.Exec(9)
// 更新数据
stmt, err = db.Prepare("update t_products set name=? where id=?")
checkErr(err)
stmt.Exec( "产品10(改)", 10)
// 关闭数据库
db.Close()
}
func checkErr(err error) {
if err != nil {
panic(err)
}
}
执行以上程序后,服务器控制台输出:
1 产品1 54.80
2 产品2 109.60
3 产品3 164.40
4 产品4 219.20
5 产品5 274.00
6 产品6 328.80
7 产品7 383.60
8 产品8 438.40
9 产品9 493.20
10 产品10 548.00
-------------------------
产品6 328.80
数据库 t_products 表数据:
使用 MySQL 数据库
MySQL 以免费、开源、使用方便为优势成为了很多 Web 开发的后端数据库存储引擎。
驱动
https://github.com/go-sql-driver/mysql 是一个支持 database/sql 接口的 MySQL 数据库驱动。
实例代码
数据库表结构如下所示,相应的建表 SQL:
CREATE TABLE `t_products` (
`id` integer not null primary key,
`name` text,
`price` float
);
Go 程序操作数据库表数据(增删改查)示例:
package main
import (
"database/sql"
"fmt"
_ "github.com/mattn/mysql-master"
)
/*
MySQL 数据库
https://github.com/go-sql-driver/mysql
安装 MySQL 驱动
go get -u github.com/go-sql-driver/mysql
-u: 强制升级
*/
func main() {
// 打开数据库
db, err := sql.Open("mysql", "root:123456@tcp(localhost:3306)/play_db?charset=utf8")
checkErr2(err)
db.Exec("drop table t_products")
// 创建表
sqlStmt := `create table t_products(id integer not null primary key, name text, price float)`
_, err = db.Exec(sqlStmt)
checkErr2(err)
defer db.Close()
// 开始事务
tx, err := db.Begin()
checkErr2(err)
// 插入数据
stmt, err := tx.Prepare("insert into t_products(id,name,price) values (?,?,?)")
checkErr2(err)
defer stmt.Close()
for i:=0; i < 10; i++ {
_, err = stmt.Exec(i+1,fmt.Sprintf("产品%d", i + 1), float64(i + 1) * 54.8)
checkErr2(err)
}
// 提交事务
tx.Commit()
// 查询数据
rows, err := db.Query("select id,name,price from t_products")
checkErr2(err)
// 延迟关闭
defer rows.Close()
for rows.Next() {
var id int
var name string
var price float64
err = rows.Scan(&id, &name, &price)
checkErr2(err)
//格式化价格
priceStr := fmt.Sprintf("%.2f", price)
fmt.Println(id,name,priceStr)
}
// 带参数查询
stmt, _ = db.Prepare("select name,price from t_products where id=?")
defer stmt.Close() // 延迟关闭
var name string
var price float64
stmt.QueryRow("6").Scan(&name, &price)
priceStr := fmt.Sprintf("%.2f", price) // 格式化价格
fmt.Println("-------------------------")
fmt.Println(name,priceStr)
// 删除数据
stmt,_ = db.Prepare("delete from t_products where id=?")
stmt.Exec(9)
// 更新数据
stmt, err = db.Prepare("update t_products set name=? where id=?")
checkErr2(err)
stmt.Exec( "产品10(改)", 10)
// 关闭数据库
db.Close()
}
func checkErr2(err error) {
if err != nil {
panic(err)
}
}
执行以上程序后,服务器控制台输出:
1 产品1 54.80
2 产品2 109.60
3 产品3 164.40
4 产品4 219.20
5 产品5 274.00
6 产品6 328.80
7 产品7 383.60
8 产品8 438.40
9 产品9 493.20
10 产品10 548.00
-------------------------
产品6 328.80
数据库 t_products 表数据:
上面的代码,sql.Open() 函数用来打开一个注册过的数据库驱动,go-sql-driver 中注册了 mysql 这个数据库驱动,第二个参数是 DSN(Data Source Name),它是 go-sql-driver 定义的一些数据库链接和配置信息。它支持如下格式:
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
- db.Prepare() 函数用来返回准备要执行的 sql 操作,然后返回准备完毕的执行状态。
- db.Query() 函数用来直接执行 Sql 返回 Rows 结果。
- stmt.Exec() 函数用来执行 stmt 准备好的 SQL 语句
- 传入的参数都是 =? 对应的数据,这样做的方式可以一定程度上防止 SQL 注入。