go 原生提供了对数据库的支持,就是 database/sql 包,对关系型的数据库进行了通用的抽象,轻量、面向行的接口,所以使用这个包还需要下载对相应的数据库驱动,比如 mysql 的驱动包 github.com/go-sql-driver/mysql,执行:
go get -u github.com/go-sql-driver/mysql
由于不需要调用驱动包的含糊,只需要其执行一次 init 函数,imoprt 部分往往是:
"database/sql"
_ "github.com/go-sql-driver/mysql"
不引入驱动,调用 sql 包的函数的时候会收到提示:
sql: unknown driver "mysql" (forgotten import?)
为什么引入 mysql 的包后就可以工作了呢? 这是因为 mysql 包里的 init 函数里执行了 sql.Register 函数:
func init() {
sql.Register("mysql", &MySQLDriver{})
}
查看 Register 函数的实现,会发现内部把驱动类型和驱动实例做了一个映射保存在 drivers 中,对 MySQLDriver 来说,就是 “mysql” => &MySQLDriver{},Register 函数的第二个参数是 driver.Driver 接口对象,它只要求实现 1 个方法:
type Driver interface {
Open(name string) (Conn, error)
}
Open 函数返回一个 driver.Conn 对象,根据注释得知该 Conn 对象在同一时间只会被一个 goroutine 所占用,通过查看 MySQLDriver 的 Open 函数发现它使用了内部的 connector 对象来实现,而 connector 对象又将功能更委托给了 mysqlConn 对象,该连接对象负责和 mysql 之间交互的协议。
sql 包使用 sql.Open 来获得一个数据库操作对象 sql.DB,而这个 DB 结构体中最重要的就是 driver.Connector,它要求实现 2 个方法:
1. Connect(context.Context) (Conn, error)
2. Driver() Driver
func getMysqlDB() *sql.DB {
db, _ := sql.Open("mysql", "root:@tcp(127.0.0.1:3306)/test?charset=utf8&parseTime=true")
//db, _ := sql.Open("mysql", "root:@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True")
//db.Ping()
return db
}
问题是这个 sql.Open 里的 connector 是怎么和 mysql 驱动的 connector 连接上的呢? F12 进入 sql.Open 函数,发现它会尝试从 dirvers 获取 MySQLDriver 的实例,尝试转换成 driver.DriverContext 后调用其 OpenConnector 函数,如果转换失败了也会构造一个内部的 dsnconnector 对象,调用 driver 上的 Open 方法。
而 MySQLDriver 对 Open 和 OpenConnector 都提供了实现:
func (d MySQLDriver) Open(dsn string) (driver.Conn, error) {
cfg, err := ParseDSN(dsn)
if err != nil {
return nil, err
}
c := &connector{
cfg: cfg,
}
return c.Connect(context.Background())
}
func (d MySQLDriver) OpenConnector(dsn string) (driver.Connector, error) {
cfg, err := ParseDSN(dsn)
if err != nil {
return nil, err
}
return &connector{
cfg: cfg,
}, nil
}
sql 包依靠 Open、Register 2 个函数实现了驱动包的注入,获得 driver.Conn 对象后,主要工作就是解析 sql 查询和获得结果了(mysql 交互协议),因为查询通常分为 2 种类型:
1. query 查询带结果,比如 select 等
2. exec 只需执行,比如 update、delete 等
具体 sql 包在调用时会尝试把 driver.Conn 对象转换成 Queryer、QueryerContext 和 Execer、ExecerContext 执行其对应的方法,不过这一切使用 sql.DB 做为中间对象来操作,在 DB 内部它对 driver.Conn 对象做了进一层带锁的包装 driverConn,在任何查询前都会使用 db.conn 内部方法里获取 driverConn,它加锁以后从 freeConn 里取出一个并标记为 inUse 在使用,否则就检测是否达到最大连接数,没有就就调用 connector 对象的 Connect 方法获得 driver.Conn 后构造一个新的 driverConn,用完了后都会调用它的 relaseConn 把连接对象重新 put 回 freeConn 列表里。至此,go sql 驱动包的加载和执行的流程都清楚了。
mysql 的 go hello 版本如下:
func main() {
var str string db := getMysqlDB()
row := db.QueryRow("SELECT 'hello go-mysql'")
if row.Scan(&str) == nil {
fmt.Println(str)
}
}
批量查询和使用预编译的查询:
func scan() {
db := session.GetMysqlDB()
rows, _ := db.Query("SELECT * FROM user")
defer rows.Close()
// 遍历 rows
}
func single() {
var user User
db := session.GetMysqlDB()
db.QueryRow("SELECT * FROM user WHERE id = 1").Scan(&user.id, &user.passport, &user.password, &user.nickname, &user.createdAt)
fmt.Println(user)
}
func prepare() {
db := session.GetMysqlDB()
stmt, _ := db.Prepare("SELECT * FROM user WHERE id = ?")
defer stmt.Close()
rows, _ := stmt.Query(1)
defer rows.Close()
//
}
怎么遍历 rows 呢? 需要使用 Next() 来判断,当遇到 io.EOF 或者 rows 被 close 会结束遍历。
for rows.Next() {
var user User
rows.Scan(&user.id, &user.passport, &user.password, &user.nickname, &user.createdAt)
fmt.Println(user)
}
// 使用 rows.Error 来得到错误
插入时使用 LastInsertId 来获取最后一行的 id:
func main() {
db := session.GetMysqlDB()
stmt, _ := db.Prepare("INSERT INTO user(`passport`, `password`,`nickname`) VALUES(?, ?, ?)")
result, _ := stmt.Exec("lisi", "abc123", "李四")
fmt.Println(result.LastInsertId())
}
使用 begin 和 beginTx 开启一个事务,begin 使用默认的 context.Background() 来调用 beginTx,事务会独占数据库连接,但是 Tx 对象上的方法和 sql.DB 都是一一对应,调用 commit 或者 rollback 后,Tx 对象才被 Close 掉。
func main() {
db := session.GetMysqlDB()
stmt, _ := db.Prepare("INSERT INTO user(`passport`, `password`,`nickname`) VALUES(?, ?, ?)")
result, _ := stmt.Exec("lisi", "abc123", "李四")
fmt.Println(result.LastInsertId())
}
NULL 字段有时候会很麻烦,处理 NULL 有两个办法:
1. 使用数据库函数 COALESCE 让可能 NULL 的字段不可能返回 NULL
2. 使用 sql.NullString、sql.NullInt32 等类型
type NullString struct {
String string
Valid bool // Valid is true if String is not NULL
}
对于第二种办法,Scan 操作后需要先用 Valid 判断一下,为 true 则调用 String 或者 Value 方法,值得注意的并没有 sql.NullUint32 等类型,可以自定义 NULL 类型,实现 Scan 和 Value 方法。比如 NullInt32 的实现如下:
// NullInt32 represents an int32 that may be null.
// NullInt32 implements the Scanner interface so
// it can be used as a scan destination, similar to NullString.
type NullInt32 struct {
Int32 int32
Valid bool // Valid is true if Int32 is not NULL
}
// Scan implements the Scanner interface.
func (n *NullInt32) Scan(value interface{}) error {
if value == nil {
n.Int32, n.Valid = 0, false
return nil
}
n.Valid = true
return convertAssign(&n.Int32, value)
}
// Value implements the driver Valuer interface.
func (n NullInt32) Value() (driver.Value, error) {
if !n.Valid {
return nil, nil
}
return int64(n.Int32), nil
}
如你所见,上面的 Scan 的用法其实非常的不方便,实际项目里用 struct tag + orm 来做比较多,下面是使用 sqlx (go get 的例子:
type User struct {
ID int `db:"id"`
Passport string `db:"passport"`
Password string `db:"password"`
Nickname string `db:"nickname"`
CreatedAt time.Time `db:"create_time"`
}
func main() {
var total int
var user User
var users []User
var names []string
db := session.GetSqlxDB()
db.Get(&total, "SELECT COUNT(*) FROM user")
fmt.Println(total)
db.Get(&user, "SELECT * FROM user LIMIT 1")
fmt.Println(user)
db.Select(&users, "SELECT * FROM user")
fmt.Println(users)
db.Select(&names, "SELECT nickname FROM user")
fmt.Println(names)
db.QueryRowx("SELECT * FROM user LIMIT 1").StructScan(&user)
fmt.Println(user)
rows, _ := db.Queryx("SELECT * FROM user")
defer rows.Close()
for rows.Next() {
var u User
rows.StructScan(&u)
fmt.Println(u)
}
}
除了好用的 Get、Select、StructScan,还有 MapScan、 SliceScan 分别对应 map[string]interface{} 和 []interface{},如果需要数据库 ORM 可以关注下 。
本章节的代码