一、介绍

database/sql 采用策略模式,每个driver实现驱动,提供一个注册连接词。

var (
    driversMu sync.RWMutex
    drivers   = make(map[string]driver.Driver)
)

// Register makes a database driver available by the provided name.
// If Register is called twice with the same name or if driver is nil,
// it panics.
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
}

当我们使用不同的数据库的时候,只要把使用的驱动注册进去就可以了。
比如在 github.com/go-sql-driver/mysql mysql驱动库里。
有一段代码
github.com/go-sql-driver/mysql/driver.go

func init() {
    sql.Register("mysql", &MySQLDriver{})
}

这里注册是在drivers的map的一个key,关联一个driver。

二、使用流程

2.1 sql.Open()创建连接池句柄sql.DB

func Open(driverName, dataSourceName string) (*DB, error) {
    driversMu.RLock()
    driveri, ok := drivers[driverName]
    driversMu.RUnlock()
    if !ok {
        return nil, fmt.Errorf("sql: unknown driver %q (forgotten import?)", driverName)
    }

    if driverCtx, ok := driveri.(driver.DriverContext); ok {
        connector, err := driverCtx.OpenConnector(dataSourceName)
        if err != nil {
            return nil, err
        }
        return OpenDB(connector), nil
    }

    return OpenDB(dsnConnector{dsn: dataSourceName, driver: driveri}), nil
}

func OpenDB(c driver.Connector) *DB {
    ctx, cancel := context.WithCancel(context.Background())
    db := &DB{
        connector:    c,
        openerCh:     make(chan struct{}, connectionRequestQueueSize),
        lastPut:      make(map[*driverConn]string),
        connRequests: make(map[uint64]chan connRequest),
        stop:         cancel,
    }

    go db.connectionOpener(ctx)

    return db
}

type DB struct {
    // Atomic access only. At top of struct to prevent mis-alignment
    // on 32-bit platforms. Of type time.Duration.
    waitDuration int64 // Total time waited for new connections.

    connector driver.Connector
    // numClosed is an atomic counter which represents a total number of
    // closed connections. Stmt.openStmt checks it before cleaning closed
    // connections in Stmt.css.
    numClosed uint64

    mu           sync.Mutex // protects following fields
    freeConn     []*driverConn
    connRequests map[uint64]chan connRequest
    nextRequest  uint64 // Next key to use in connRequests.
    numOpen      int    // number of opened and pending open connections
    // Used to signal the need for new connections
    // a goroutine running connectionOpener() reads on this chan and
    // maybeOpenNewConnections sends on the chan (one send per needed connection)
    // It is closed during db.Close(). The close tells the connectionOpener
    // goroutine to exit.
    openerCh          chan struct{}
    closed            bool
    dep               map[finalCloser]depSet
    lastPut           map[*driverConn]string // stacktrace of last conn's put; debug only
    maxIdle           int                    // 最大空闲连接数
    maxOpen           int                    // <= 0 means unlimited
    maxLifetime       time.Duration          // maximum amount of time a connection may be reused
    cleanerCh         chan struct{}
    waitCount         int64 // Total number of connections waited for.
    maxIdleClosed     int64 // Total number of connections closed due to idle.
    maxLifetimeClosed int64 // Total number of connections closed due to max free limit.

    stop func() // stop cancels the connection opener and the session resetter.
}

注意sql.Open()只是初始化一个 sql.DB,此时并不会真正的创建连接。
初始化内容,设置driver的参数,解析dns等。

2.2设置数据库连接参数

func TestOpen(t *testing.T) {
    db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/database?charset=utf8")
    t.Log(db, err)
    db.SetMaxIdleConns(10)                 //最大空闲连接数
    db.SetMaxOpenConns(11)                 //最大连接数
    db.SetConnMaxLifetime(5 * time.Second) //最大连接时间
}

打印输出:

&{0 0xc0000a2028 0 {0 0} [] map[] 0 0 0xc00008e2a0 false map[] map[] 10 11 5000000000 <nil> 0 0 0 0x10d28a0} <nil>

2.3访问数据库

当我们初始化后,我们ping一下数据库,然后可以看到是否连接通。
源码如下:src/database/sql/sql.go

func (db *DB) Ping() error {
    return db.PingContext(context.Background())
}

func (db *DB) PingContext(ctx context.Context) error {
    var dc *driverConn
    var err error

    for i := 0; i < maxBadConnRetries; i++ {
        dc, err = db.conn(ctx, cachedOrNewConn)
        if err != driver.ErrBadConn {
            break
        }
    }
    if err == driver.ErrBadConn {
        dc, err = db.conn(ctx, alwaysNewConn)
    }
    if err != nil {
        return err
    }

    return db.pingDC(ctx, dc, dc.releaseConn)
}

func (db *DB) pingDC(ctx context.Context, dc *driverConn, release func(error)) error {
    var err error
    if pinger, ok := dc.ci.(driver.Pinger); ok {
        withLock(dc, func() {
            err = pinger.Ping(ctx)
        })
    }
    release(err)
    return err
}

我们使用一下:

import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
    "testing"
    "time"
)

func TestOpen(t *testing.T) {
    //1 获取sql.DB
    db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/database?charset=utf8")
    t.Log(db, err)
    //2 设置连接参数
    db.SetMaxIdleConns(10)                 //最大空闲连接数
    db.SetMaxOpenConns(11)                 //最大连接数
    db.SetConnMaxLifetime(5 * time.Second) //最大连接时间
    t.Log(db, err)
    //3 访问数据库
    err = db.Ping()
    t.Log(db, err)
}

输出如下:

&{0 0xc00010a028 0 {0 0} [] map[] 0 0 0xc000122240 false map[] map[] 0 0 0 <nil> 0 0 0 0x10d29c0} <nil>
&{0 0xc00010a028 0 {0 0} [] map[] 0 0 0xc000122240 false map[] map[] 10 11 5000000000 <nil> 0 0 0 0x10d29c0} <nil>
&{0 0xc00010a028 0 {0 0} [0xc0001501c0] map[] 0 1 0xc000122240 false map[0xc0001501c0:map[0xc0001501c0:true]] map[] 10 11 5000000000 0xc000122420 0 0 0 0x10d29c0} <nil>

2.4 执行SQL语句

// 只执行,不返还结果集,返回执行 Result影响的行数和最后一条id
func (db *DB) Exec(query string, args ...interface{}) (Result, error) {}
func (db *DB) ExecContext(ctx context.Context, query string, args ...interface{}) (Result, error) {}

// 返回大于0条结果集
func (db *DB) Query(query string, args ...interface{}) (*Rows, error) {}
func (db *DB) QueryContext(ctx context.Context, query string, args ...interface{}) (*Rows, error) {}

// 预期结果集只有一行,没有结果集Scan时报ErrNoRows,Scan结果如果有多行,只取第一行,多余的数据行丢弃
func (db *DB) QueryRow(query string, args ...interface{}) *Row {}
func (db *DB) QueryRowContext(ctx context.Context, query string, args ...interface{}) *Row {}

2.4.1返回结果集方面差别

表列 AExec()Query()QueryRow()
结果集多条1条

2.4.2 是否请求的时候带上下文Context

ExecContext()Exec()
QueryContext()Query()
QueryRowContext()QueryRow()

超时查询的例子
在做数据库查询时,需要对数据的查询做超时控制,例如:

ctx = context.WithTimeout(context.Background(), time.Second)
rows, err := pool.QueryContext(ctx, "select * from products where id = ?", 100)

上面的代码基于 Background 派生出一个带有超时取消功能的ctx,传入带有context查询的方法中,如果超过1s未返回结果,则取消本次的查询。使用起来非常方便。

2.4.3 args参数

我们可以发现,每个函数参数都是支持可变参数列表,用法和prepare用法一样,用 ? 做占位符,那我们直接拼好sql和使用占位符哪种更优呢?

rows1, err := db.Query("select * from user where name = hisheng”)
rows2, err := db.Query("select * from user where a = ?", "hisheng")

这两条sql执行的结果是一样的,但是底层是不一样的,与不同驱动的具体实现略有差别。

以mysql为例,区别在于第一个Query,实际发送了一条sql(sql_type:3),第二条Query,实际发送了两条sql(sql_type:22 和 sql_tyep:23),先prepare,再execute,虽说二进制协议要快些,但是每次都会发送两条sql,第一次发送的prepare,之后只会execute一次且不会主动回收这个prepare信息。

这个接口设计之初,应该就是按照prepare+execute的思想设计的,当占位符参数个数为0时,能否优化直接发送一条sql,要看底层的驱动接口是否支持,换言之,prepare+execute

2.4.4 Exec(),不返还结果集,用的场景主要是非select的场景。

比如:

  • 创建数据库,
  • 创建表,
  • 修改表,
  • insert数据,
  • update数据,
  • delete数据等
import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
    "testing"
    "time"
)

func TestOpen(t *testing.T) {
    //1 获取sql.DB
    db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/database?charset=utf8")
    t.Log(db, err)
    //2 设置连接参数
    db.SetMaxIdleConns(10)                 //最大空闲连接数
    db.SetMaxOpenConns(11)                 //最大连接数
    db.SetConnMaxLifetime(5 * time.Second) //最大连接时间
    //3 访问数据库
    err = db.Ping()
    //4 执行sql
    rs, err := db.Exec("" +
        "CREATE TABLE `database`.`user` (" +
        "  `id` INT NOT NULL AUTO_INCREMENT,  " +
        "  `name` VARCHAR(45) NULL,  PRIMARY KEY (`id`)" +
        ");")
    lid, err := rs.LastInsertId() //0 ,nil
    t.Log(lid, err)
    rc, err := rs.RowsAffected() //0 ,nil
    t.Log(rc, err)
}

2.4.5 QueryRow 单条结果集

import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
    "testing"
    "time"
)

func TestOpen(t *testing.T) {
    //1 获取sql.DB
    db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/database?charset=utf8")
    t.Log(db, err)
    //2 设置连接参数
    db.SetMaxIdleConns(10)                 //最大空闲连接数
    db.SetMaxOpenConns(11)                 //最大连接数
    db.SetConnMaxLifetime(5 * time.Second) //最大连接时间
    //3 访问数据库
    err = db.Ping()
    //4 执行sql
    row := db.QueryRow("select * from database.user where id = ?", 1)
    t.Log(row, err)
    type User struct {
        Id   int64
        name string
    }
    var user User
    err = row.Scan(&user.Id, &user.name)
    t.Log(user, err)
}

user输出:

{1 hisheng} <nil>

2.4.6 Query 多条结果集

import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
    "testing"
    "time"
)

func TestOpen(t *testing.T) {
    //1 获取sql.DB
    db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/database?charset=utf8")
    t.Log(db, err)
    //2 设置连接参数
    db.SetMaxIdleConns(10)                 //最大空闲连接数
    db.SetMaxOpenConns(11)                 //最大连接数
    db.SetConnMaxLifetime(5 * time.Second) //最大连接时间
    //3 访问数据库
    err = db.Ping()
    //4 执行sql
    rows, err := db.Query("select * from database.user")
    defer rows.Close()
    t.Log(rows, err)
    type User struct {
        Id   int64
        name string
    }
    var users []User
    for rows.Next() {
        var user User
        err = rows.Scan(&user.Id, &user.name)
        users = append(users, user)
    }
    t.Log(users, err)
}

users结果集输出:

[{1 hisheng} {2 haha}] <nil>

谢谢您的观看,欢迎关注我的公众号。

image.png


海生
104 声望32 粉丝

与黑夜里,追求那一抹萤火。