database/sql SQL/SQL-Like 数据库操作接口


Go没有实现连接数据库的驱动 只提供了接口 供第三方实现
和数据库交互 使用 database/sql 包 操作 SQL/SQL-Like 数据库的通用接口
database/sql 包 成员列表
Variables   //某些常量
type Scanner  //是一个接口,每一个从数据库接收的结果块都有实现该接口的读方法,用于收到的结果列赋值给变量
type NullBool  //几种基本类型前缀上Null表示可空的该类型,valid为false时表示Null,用于处理数据库Null
func (n *NullBool) Scan(value interface{}) error //实现了scanner的Scan方法,把值赋给一个bool变量
func (n NullBool) Value() (driver.Value, error)
type NullInt64
func (n *NullInt64) Scan(value interface{}) error
func (n NullInt64) Value() (driver.Value, error)
type NullFloat64
func (n *NullFloat64) Scan(value interface{}) error
func (n NullFloat64) Value() (driver.Value, error)
type NullString
func (ns *NullString) Scan(value interface{}) error
func (ns NullString) Value() (driver.Value, error)
type RawBytes   //重定义[]byte
type Result     //执行的结果类型
type DB         //数据库的句柄类型
func Open(driverName, dataSourceName string) (*DB, error) //打开一个数据库并返回db句柄
func (db *DB) Driver() driver.Driver  //驱动方法
func (db *DB) Ping() error  //检查是否能ping通
func (db *DB) Close() error  //关闭数据库
func (db *DB) SetMaxOpenConns(n int)  //设置最大开连接
func (db *DB) SetMaxIdleConns(n int)   //限制最大连接数的区间的限制连接数
func (db *DB) Exec(query string, args ...interface{}) (Result, error) //执行任意语句
func (db *DB) Query(query string, args ...interface{}) (*Rows, error) //查询返回多行
func (db *DB) QueryRow(query string, args ...interface{}) *Row //查询单行
func (db *DB) Prepare(query string) (*Stmt, error)  //为db准备一段将要执行的sql语句
func (db *DB) Begin() (*Tx, error)   //开启一则事务
type Row   //查询单行的返回值
func (r *Row) Scan(dest ...interface{}) error //查询结果赋给指定变量,按列名顺序
type Rows  //多行返回结果,不能简单的用Scan来赋值,要先Next循环遍历,然后单个Scan入相关变量
func (rs *Rows) Columns() ([]string, error)  //按列返回,不知道有什么用~~~~
func (rs *Rows) Scan(dest ...interface{}) error  
func (rs *Rows) Next() bool  //用于遍历rows
func (rs *Rows) Close() error //关闭rows
func (rs *Rows) Err() error   //循环过程中出现错误,该错误会被记录在Err()里
type Stmt
func (s *Stmt) Exec(args ...interface{}) (Result, error)
func (s *Stmt) Query(args ...interface{}) (*Rows, error)
func (s *Stmt) QueryRow(args ...interface{}) *Row
func (s *Stmt) Close() error
type Tx
func (tx *Tx) Exec(query string, args ...interface{}) (Result, error)
func (tx *Tx) Query(query string, args ...interface{}) (*Rows, error)
func (tx *Tx) QueryRow(query string, args ...interface{}) *Row
func (tx *Tx) Prepare(query string) (*Stmt, error)
func (tx *Tx) Stmt(stmt *Stmt) *Stmt
func (tx *Tx) Commit() error  //事务提交
func (tx *Tx) Rollback() error //回滚
func Register(name string, driver driver.Driver)
Go 标准库 没有提供具体数据库的实现 需要结合第三方的驱动来使用该接口
如 mysql 驱动 github.com/go-sql-driver/mysql
import (
    _ "github.com/go-sql-driver/mysql"
    "database/sql"
    "fmt"
)
database/sql 包有个子包 driver 定义了一些接口
供 数据库驱动实现 一般业务代码中使用 database/sql 包  尽量避免使用 driver 这个子包

database/sql 是什么

database/sql 是 Go 标准库  用于和 SQL/SQL-Like 数据库(关系或类似关系数据库)通讯
提供了和 ODBC, Perl的 DBI, Java的JDBC和PHP的PDO 类似的功能
利于构建健壮 高性能的 基于 database 的应用
database/sql 提供 抽象概念 和具体数据库无关 具体的数据库实现 驱动来做 更换数据库 很方便

该包提供了一些类型(概括性的) 每个类型可能包括一个或多个概念
DB
sql.DB 类型代表 一个数据库  它并不代表一个到数据库的具体连接 而是一个能操作的数据库对象
具体的连接 在内部通过连接池来管理 对外不暴露 每一次数据库操作 都产生一个 sql.DB 实例 操作完 Close

Results
定义了三种结果类型 sql.Rows sql.Row 和 sql.Result 分别用于 获取 多个多行结果 一行结果 和 修改数据库影响的行数(或其返回last insert id)

Statements
sql.Stmt 代表一个语句 如 DDL DML等

Transactions
sql.Tx 代表带有特定属性的一个事务

sql.DB 的使用

DB  是数据库句柄 代表一个具有零到多个底层连接的连接池 它可以安全的被多个 goroutine 同时使用
sql包 会自动创建和释放连接 它也会维护一个闲置连接的连接池 如果数据库具有单连接状态的概念 该状态只有在事务中被观察时才可信
一旦调用了 BD.Begin 返回的 Tx 会绑定到单个连接 当调用事务 Tx 的 Commit 或 Rollback 后 该事务使用的连接会归还到DB的闲置连接池中
连接池的大小可以用 SetMaxIdleConns 方法控制
由于 DB 并非一个实际的到数据库的连接 且可以被多个 goroutine 并发使用 因此 程序中只需要拥有一个全局的实例即可
db, err := sql.Open("mysql", "root:@tcp(localhost:3306)/ecms?charset=utf8")
if err != nil {
    panic(err)
}
defer db.Close()
实际 defer db.Close() 可以不调用
官方文档 DB.Close 说明也提到了 Close 用于关闭数据库 释放任何打开的资源
一般不会关闭 DB 因为 DB 句柄通常被多个 goroutine 共享 并长期活跃
如果你确定 DB 只会被使用一次 之后不会使用了 应该调用 Close
所以 实际的 Go 程序 应该在一个go文件中的 init 函数中调用 sql.Open 初始化全局的 sql.DB 对象 供 程序中所有需要进行 数据库 操作的地方使用
前面说过 sql.DB 并不是实际的数据库连接 因此 sql.Open 函数并没有进行数据库连接 只有在 驱动未注册时才会返回 err != nil
例如 db, err := sql.Open("mysql", "root:root@tcp(localhost233:3306)/ecms?charset=utf8")
虽然这里的 dsn 是错误的 但依然 err == nil 只有在实际操作数据库(查询 更新等)或 调用 Ping 时才会报错

Open 函数 参数
sql.Open 第一个参数是 驱动程序名称 是驱动程序用于注册的字符串
database/sql 通常与包名称相同以避免混淆
如 mysql 适用于 github.com/go-sql-driver/mysql    
有 不按约定和使用的 数据库名称 如sqlite3 对于github.com/mattn/go-sqlite3 和 postgres 对 github.com/lib/pq
sql.Open 第二个参数是 驱动程序特定的语法 告诉 驱动程序 如何访问底层数据存储
在这个例子中 连接到本地 MySQL服务器 实例中的 ecms 数据库
func init() {
    sql.Register("mysql", &MySQLDriver{})
}
其中 mysql 即是注册的驱动名 由于 注册驱动 是在 init 函数中进行的 这也就是为什么采用 github.com/go-sql-driver/mysql 这种方式引入驱动包
第二个参数是 DSN(数据源名称) 这个是和具体驱动相关的 database/sql 包并没有规定 具体书写方式参见驱动文档

连接池的工作原理

获取 DB 对象后 连接池是空的 第一个连接在需要的时候才会创建 通过下面的代码 验证
db, _ := sql.Open("mysql", "root:root@tcp(localhost:3306)/ecms?charset=utf8")
fmt.Println("please exec show processlist")
time.Sleep(10 * time.Second)
fmt.Println("please exec show processlist again")
db.Ping()
time.Sleep(10 * time.Second)
在 Ping 执行之前 和 之后 show processlist 多了一条记录 即多了一个连接 Command 列是 Sleep

连接池的工作方式

当调用一个函数 需要访问数据库时 该函数会请求从连接池中获取一个连接 如果连接池中存在一个空闲连接 它会将该空闲连接给该函数 否则 会打开一个新的连接 当该函数结束时 该连接要么返回给连接池 要么传递个某个需要该连接的对象 该对象完成时 连接才会返回给连接池 相关方法的处理说明(假设 sql.DB 的对象是 db)
db.Ping()  将连接 立马 返回给连接池
db.Exec()  将连接 立马返回给连接池 但是它返回的 Result 对象会引用该连接 所以 之后可能会再次被使用
db.Query() 传递连接给 sql.Rows 对象 直到完全遍历了所有的行 或 Rows 的 Close 方法被调用了 连接才会返回给连接池
db.QueryRow() 传递连接给 sql.Row 对象 当该对象的 Scan 方法被调用时 连接会返回给连接池
db.Begin()    传递连接给 sql.Tx 对象 当该对象的 Commit 或 Rollback 方法被调用时 该链接会返回给连接池
大部分时候  不需要关心连接不释放问题 它们会自动返回给连接池 只有 Query 方法有点特殊 后面讲解如何处理
如果某个连接有问题(broken connection) database/sql 内部会进行最多10次的重试
从连接池中获取 或 新开一个连接来服务 因此 你的代码中不需要重试的逻辑

控制连接池

Go 提供了两个方法来控制连接池
db.SetMaxOpenConns(n int) 设置 连接池 中最多保存打开多少个数据库 连接 注意 它包括在使用的和空闲的
如果某个方法调用需要一个连接 但连接池中没有空闲可用 且打开的连接数达到该方法设置的最大值 该方法调用将堵塞 默认限制是0 表示最大打开数没有限制
db.SetMaxIdleConns(n int) 设置连接池中能够保持的最大空闲连接的数量 默认值  2
两个设置 可以用程序实际测试 可以验证 MaxIdleConns 是 2
db, _ := sql.Open("mysql", "root:root@tcp(localhost:3306)/ecms?charset=utf8")
// 去掉注释 可以看看相应的空闲连接是不是变化了
// db.SetMaxIdleConns(3)
for i := 0; i < 10; i++ {
    go func() {
        db.Ping()
    }()
}
time.Sleep(20 * time.Second)
通过 show processlist 命令 可以看到有两个是 Sleep 的连接

操作操作mysql 增查改删

[就是CRUD]
github.com/go-sql-driver/mysql  支持database/sql
全部采用go写
驱动比较新 维护的比较好
完全支持database/sql接口
支持 keepalive 保持长连接
CREATE TABLE `user_info` (
  `uid` INT(10) NOT NULL AUTO_INCREMENT,
  `username` VARCHAR(64) NULL DEFAULT NULL,
  `departname` VARCHAR(64) NULL DEFAULT NULL,
  `created` DATE NULL DEFAULT NULL,
  PRIMARY KEY (`uid`)
);
mysql.go 代码
package main
import (
    _ "github.com/go-sql-driver/mysql"
    "database/sql"
    "fmt"
)
func main() {
  db, err := sql.Open("mysql", "root:root@tcp(localhost:3306)/ecms?charset=utf8")//创建连接池 sql.Open
  checkErr(err)    
  stmt, err := db.Prepare("INSERT user_info SET username=?,departname=?,created=?")// 插入数据
  checkErr(err)
  res, err := stmt.Exec("tester", " 研发部门", "2017-12-09")
  checkErr(err)
  id, err := res.LastInsertId()
  checkErr(err)
  fmt.Println(id)
  stmt, err = db.Prepare("update user_info set username=? where uid=?") // 更新数据
    checkErr(err)
    res, err = stmt.Exec("tester", id)
    checkErr(err)
    affect, err := res.RowsAffected()
    checkErr(err)
    fmt.Println(affect)    
    rows, err := db.Query("SELECT * FROM user_info")// 查询数据
    checkErr(err)
    for rows.Next() {
         var uid int
         var username string
         var department string
         var created string
         err = rows.Scan(&uid, &username, &department, &created)
         checkErr(err)
         fmt.Println(uid)
         fmt.Println(username)
         fmt.Println(department)
         fmt.Println(created)
    }   
    stmt, err = db.Prepare("delete from user_info where uid=?")// 删除数据
    checkErr(err)
        //res, err = stmt.Exec(id)
    checkErr(err)
    affect, err = res.RowsAffected()
    checkErr(err)
    fmt.Println(affect)
    db.Close()
}
func checkErr(err error) {
    if err != nil {
        panic(err)
    }
}