Go标准库database/sql的设计| 青训营笔记

386 阅读4分钟

Go标准库database/sql的设计| 青训营笔记

这是我参与「第三届青训营 -后端场」笔记创作活动的的第2篇笔记

database/sql目标

只要数据库实现了标准库的接口,就可以用统一的方法操作关系型数据库。

基本用法

image.png

step1 import driver实现,使用driver+DSN初始化DB连接

image.png

DSN: Data Sourse Name,用来描述对一个数据源的连接的字符串。

DSN的组成,包括但不限于:

  • 数据源名名称
  • 数据源位置
  • 数据库驱动名称
  • user ID(如果需要的话)
  • user password(如果需要的话) [username[:password]@][protocol[(address)]]/dbname[?param1=value1&...&paramN=valueN]

fullest form: username:password@protocol(address)/dbname?param=value

step2 执行一条sql,通过rows取回返回的数据。处理完毕后需要释放链接

image.png

rows在close时也可能会出错,为了接收到这个错误,defer可以这样写:

image.png

实际上通过不断rows.Next()到最后也会自动把rows关闭掉,但是如果在下面scan数据的过程中发生panic提前返回了,那么rows就没办法自动关闭了,会出现服务卡死的现象。所以要用这个defer确保函数返回前把rows关闭。

而且如果在rows.Next()中close掉rows的话,无法接收到err,但rows.Close可以接收到err。

step3 不断移动游标(rows.Next()),scan出数据,错误处理

image.png

step4 处理rows.Err()错误

rows.Err()错误是非数据相关的错误 image.png

设计原理

image.png

级联接口。

  • 对上层应用程序提供操作接口

  • 下层驱动实现连接接口和操作接口

    要支持一个数据库,只需这个数据库实现这个包对下层驱动提供的连接接口和操作接口。

  • 实现连接池的管理(池化技术)

    image.png

sql执行的实现(伪实现)

image.png

  • 默认重试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: image.png

连接方式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) {
    ...
}

业务代码中调用sql.OpenDB方法即可得到连接

image.png

预留的操作接口(供数据库来实现)

image.png