Go语言 sql驱动原理解析


原文链接: Go语言 sql驱动原理解析

Go 各种数据库连接字符串汇总
数据库操作是一个应用必不可少的部分,但是我们很多时候对golang的sql包仅仅是会用,这是不够的。每一条语句的执行,它的背后到底发生了什么。各式各样对sql包的封装,是不是有必要的,有没有做无用功?

这是go to database package系列文章的第一篇。本系列将按照程序中使用sql包的顺序来展开

Mysql DSN

user@unix(/path/to/socket)/dbname?charset=utf8
user:password@tcp(localhost:5555)/dbname?charset=utf8
user:password@/dbname
user:password@tcp([de:ad:be:ef::ca:fe]:80)/dbname

MySQL

[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]
// user@unix(/path/to/socket)/dbname
// root:pw@unix(/tmp/mysql.sock)/myDatabase?loc=Local
// user:password@tcp(localhost:5555)/dbname?tls=skip-verify&autocommit=true
// user:password@/dbname?sql_mode=TRADITIONAL
// user:password@tcp([de:ad:be:ef::ca:fe]:80)/dbname?timeout=90s&collation=utf8mb4_unicode_ci
// id:password@tcp(your-amazonaws-uri.com:3306)/dbname
// user@cloudsql(project-id:instance-name)/dbname
// user@cloudsql(project-id:regionname:instance-name)/dbname
// user:password@tcp/dbname?charset=utf8mb4,utf8&sys_var=esc%40ped
// user:password@/dbname
// user:password@/

import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
    //_ 操作其实是引入该包,而不直接使用包里面的函数,而是调用了该包里面的init函数,import的时候其实是执行了该包里面的init函数 `sql.Register("mysql", &MySQLDriver{})`

)
func main() {
    // sql.Open函数实际上是返回一个连接池对象,不是单个连接。在open的时候并没有去连接数据库,只有在执行query、exce方法的时候才会去实际连接数据库
    db, err := sql.Open("mysql", "user:password@tcp(ip:port)/database")
    if err != nil {
        log.Println(err)
    }
    //  连接池配置
    db.SetMaxOpenConns(2000) // 默认为0 无限制,设置这个值可以避免并发太高导致连接mysql出现too many connections的错误。
    db.SetMaxIdleConns(1000) // 无访问依然保持和数据库连接的数量,可减少创建连接的开销,适合频繁访问

    defer db.Close()
}

用法很简单,首先Open打开一个数据库,然后调用Query、Exec执行数据库操作,github.com/go-sql-driver/mysql具体实现了database/sql/driver的接口,所以最终具体的数据库操作都是调用github.com/go-sql-driver/mysql实现的方法,同一个数据库只需要调用一次Open即可,下面根据具体的操作分析下"database/sql"都干了哪些事。

1. 驱动注册

import _ "github.com/go-sql-driver/mysql"

  1. 前面的 _ 作用时不需要把该包都导进来,只执行包的init()方法,
  2. mysql驱动正是通过这种方式注册到"database/sql"中的:

    type MySQLDriver struct{}
    // 实现 database/sql 中定义的 Driver 接口 中的 Open() 方法
    func (d MySQLDriver) Open(dsn string) (driver.Conn, error) {
      ...
    }
    //github.com/go-sql-driver/mysql/driver.go
    func init() {
      sql.Register("mysql", &MySQLDriver{})
      // 只有1行,确实非常的简单。调用database/ssql的Register方法注册了一个名为mysql的数据库驱动,而驱动本身就是&MySQLDriver{}。
      // 那我们再看看下面database/ssql包中的Register方法
    }
    
    

    import "database/sql"

    1. init()通过Register()方法将mysql驱动添加到sql.drivers(类型:make(map[string]driver.Driver))中
      go //database/sql/sql.go 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 }
  3. MySQLDriver实现了driver.Driver接口:

    //database/sql/driver/driver.go
    type Driver interface {
    // Open returns a new connection to the database.
    // The name is a string in a driver-specific format.
    //
    // Open may return a cached connection (one previously
    // closed), but doing so is unnecessary; the sql package
    // maintains a pool of idle connections for efficient re-use.
    //
    // The returned connection is only used by one goroutine at a
    // time.
    Open(name string) (Conn, error)
    }
    

    假如我们同时用到多种数据库,就可以通过调用sql.Register将不同数据库的实现注册到sql.drivers中去,用的时候再根据注册的name将对应的driver取出。


    先来看一段简短的代码:

    package main
    import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
    "fmt"
    )
    
    func main() {
    db, err := sql.Open("mysql", "user:password@/dbname")
    if nil != err {
        panic(err)
    }
    age := 18
    
    rows,err := db.Query(`SELECT name,age FROM person where age > ?`, age)
    if nil != err {
        panic(err)
    }
    defer rows.Close()
    
    for rows.Next() {
        var name string
        var age int
        err := rows.Scan(&name, &age)
        if nil != err {
            panic(err)
        }
        fmt.Println(name, age)
    }
    }
    

这应该是最简单的使用场景了。本文也会按照以上代码,逐句展开。
import _ "somedriver"是在干什么

先来看一下golang官方文档的说法:

To import a package solely for its side-effects (initialization), use the blank identifier as explicit package name:

import _ "lib/math"

也就是说,import _ "somedriver"仅仅是想调用somedriver包的init方法。那么我们可以一起来看看go-sql-driver/mysql的init方法。它非常简单:

func init() {

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

}

只有1行,确实非常的简单。调用sql的Register方法注册了一个名为mysql的数据库驱动,而驱动本身就是&MySQLDriver{}。

那我们再看看sql包中的Register方法:
```go
// 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

}

Register的第二个参数接收一个driver.Driver的interface,因此go-sql-driver/mysql包中的&MySQLDriver必须实现driver.Driver规定的一系列方法(当然它肯定实现了)。

Register函数如果发现名为name的driver已经注册了,就会触发panic,否则就进行注册。注册其实很简单,drivers[name] = driver。

drivers是一个map

drivers = make(map[string]driver.Driver)

所以简单来说,import _ "somedriver"其实就是调用sql.Register注册一个实现了driver.Driver接口的实例。

驱动给sql包提供了最基本的支持,sql包最终与数据库打交道的操作都是通过driver完成的。其实不应该说sql包,而应该说是DB实例。

在上面程序main函数的一开始,执行sql.Open拿到了一个DB实例,那么什么是DB实例,sql.Open又干了什么?
sql.Open是在干什么

看一下官方文档的介绍:

func Open(driverName, dataSourceName string) (*DB, error)
//Open opens a database specified by its database driver name and a driver-specific data source name, usually consisting of at least a database name and connection information.

//Most users will open a database via a driver-specific connection helper function that returns a *DB. No database drivers are included in the Go standard library. See https://golang.org/s/sqldrivers for a list of third-party drivers.

//Open may just validate its arguments without creating a connection to the database. To verify that the data source name is valid, call Ping.

//The returned DB is safe for concurrent use by multiple goroutines and maintains its own pool of idle connections. Thus, the Open function should be called just once. It is rarely necessary to close a DB.

简单来说,Open返回一个DB实例,DB实例引用了由driverName指定的数据库驱动程序。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)
}
db := &DB{
    driver:   driveri,
    dsn:      dataSourceName,
    openerCh: make(chan struct{}, connectionRequestQueueSize),
    lastPut:  make(map[*driverConn]string),
}
go db.connectionOpener()
return db, nil

}

Open方法:

根据driverName拿到对应的driver
根据driver和dataSourceName生成一个DB实例
另起一个goroutine来执行某种任务A

如果对goroutine比较敏感的同学可能会猜到go db.connectionOpener()是在干嘛。在go中大多数情况下新开一个goroutine都是在:

监听某个channel
往某个channel发消息

根据上面的代码不难猜测,connectionOpener和opennerCh有关。看名字也很容易看出,connectionOpener翻译过来就是连接创建者,负责创建连接。看看代码吧:

// Runs in a separate goroutine, opens new connections when requested.
func (db *DB) connectionOpener() {

for range db.openerCh {
    db.openNewConnection()
}

}

每当从openerCh取到一条消息,connectionOpener就创建一个连接。

如何创建连接其实很简单,就是调用Driver提供的Open方法,具体先暂时不展开了。(不展开的这个决定,和golang的sql包是很吻合的,因为sql包对Open一个连接的处理,仅仅是定义了一个接口,让驱动去实现。也就是说,在逻辑上这里需要Open一个新连接,具体怎么做我不管,Driver你提供Open接口,返回给我我要的就行。)

整个DB可以画一张图来理解。

`