Go标准库database/sql的设计| 青训营笔记
这是我参与「第三届青训营 -后端场」笔记创作活动的的第2篇笔记
database/sql目标
只要数据库实现了标准库的接口,就可以用统一的方法操作关系型数据库。
基本用法
step1 import driver实现,使用driver+DSN初始化DB连接
DSN: Data Sourse Name,用来描述对一个数据源的连接的字符串。
DSN的组成,包括但不限于:
- 数据源名名称
- 数据源位置
- 数据库驱动名称
- user ID(如果需要的话)
- user password(如果需要的话) [username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]
fullest form: username:password@protocol(address)/dbname?param=value
step2 执行一条sql,通过rows取回返回的数据。处理完毕后需要释放链接
rows在close时也可能会出错,为了接收到这个错误,defer可以这样写:
实际上通过不断rows.Next()到最后也会自动把rows关闭掉,但是如果在下面scan数据的过程中发生panic提前返回了,那么rows就没办法自动关闭了,会出现服务卡死的现象。所以要用这个defer确保函数返回前把rows关闭。
而且如果在rows.Next()中close掉rows的话,无法接收到err,但rows.Close可以接收到err。
step3 不断移动游标(rows.Next()),scan出数据,错误处理
step4 处理rows.Err()错误
rows.Err()错误是非数据相关的错误
设计原理
级联接口。
-
对上层应用程序提供操作接口
-
下层驱动实现连接接口和操作接口
要支持一个数据库,只需这个数据库实现这个包对下层驱动提供的连接接口和操作接口。
-
实现连接池的管理(池化技术)
sql执行的实现(伪实现)
- 默认重试2次(maxBadConnRetries)
- 尽量复用连接池中原来的链接,如果出错则新建一个连接
- 用defer确保最后连接会放回连接池(放回前会做一些检查,比如检查有无错误,max life time, max idle conns,不符合我们的需求时会丢弃掉不放回连接池)
- 调用用这个连接实现的driver.Query或driver.Execer等接口方法,执行sql语句,接受错误err
- 如果错误是driver.ErrBadConn错误,则重试;若没有错误或错误不是driver.ErrBadConn错误,则退出循环
预留的连接接口(供数据库来实现)
连接方式1: Driver接口 + sql.Open方法
database/sql包提供
-
Driver接口的定义
-
Register(name string, driver driver.Driver)方法
把driver注册到全局变量drivers(map[string]driver.Driver)中, 即 drivers[name] = driver
-
Open(driverName, dataSourceName string) (*DB, error)方法
接收一个driverName返回一个*DB连接(根据参数driverName可以去全局变量drivers中找注册的drivers[driverName]对应的Driver)
driver实现方,比如 github.com/go-sql-driver/mysql 包
- 用MySQLDriver类型实现Driver接口中的方法
- 在init()中调用sql.Register方法把一个MySQLDriver类型的变量注册(调用sql.Register方法)
应用程序调用sql.Open(driverName, dsn)即可得到*连接DB
Driver接口:
type Driver interface {
Open(name string) (Conn, error)
}
注册全局driver:
var (
driversMu sync.RWMutex
drivers = make(map[string]driver.Driver)
)
func Register(name string, driver driver.Driver) {
driversMu.Lock()
defer driversMu.Unlock()
if driver == nil {
panic("sql: Register driver is nil")
}
if _, dup := drivers[name]; dup {
panic("sql: Register called twice for driver " + name)
}
drivers[name] = driver
}
driver的实现在init中调用sql.Register方法:
这是mysql的driver的实现,在init函数中调用了sql.Register方法,这样在这个driver被import的时候import _ "github.com/go-sql-driver/mysql",这个init会被调用,自然就完成了注册
func init() {
sql.Register("mysql", &MySQLDriver{})
}
sql.Open方法:
直接返回*DB
func Open(driverName, dataSourceName string) (*DB, error) {
...
}
业务代码中调用sql.Open方法即可得到连接
eg:
连接方式2: Connector接口 + sql.OpenDB方法
—— 为了改变sql.Open()参数中传很长的DSN字符串的缺陷
database/sql包提供
-
Connector接口的定义
-
OpenDB(c driver.Connector) *DB方法
接收一个Connector返回一个*DB连接
driver实现方,比如 github.com/go-sql-driver/mysql 包
- mysql.NewConnector方法可以返回一个driver.Connector接口变量 这个方法的参数是一个Config结构体,把数据库相关参数以结构体的形式写出来,而非很长的字符串。
应用程序调用sql.OpenDB方法得到*DB连接,参数为mysql.NewConnector方法返回的driver.Connector接口变量
Connector接口:
type Connector interface {
Connect(context.Context) (Conn, error)
Driver() Driver
}
sql.OpenDB方法
接收一个Connector, 直接返回一个*DB
func OpenDB(c driver.Connector) *DB {
...
}
mysql.NewConnector方法可以返回一个driver.Connector接口
返回driver.Connector
参数*Config中可以以结构体的形式传入连接所需的参数
func NewConnector(cfg *Config) (driver.Connector, error) {
...
}