【转】dive into golang database/sql(1)
转,原文:https://www.jianshu.com/p/3b0b3a4c83da
---------------
数据库操作是一个应用必不可少的部分,但是我们很多时候对golang的sql包仅仅是会用,这是不够的。每一条语句的执行,它的背后到底发生了什么。各式各样对sql包的封装,是不是有必要的,有没有做无用功?
这是go to database package
系列文章的第一篇。本系列将按照程序中使用sql包的顺序来展开
先来看一段简短的代码:
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
方法:
// 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可以画一张图来理解。

当然DB实例还有很多其它细节,但是对于sql.Open
方法来说,以上就够了。总结一下,sql.Open
会根据driverName和dataSourceName生成一个DB实例,并且另起一个goroutine来负责新建连接(监听openerCh的“新建连接请求”)。
在这里可以看出,执行sql.Open
,仅仅返回了DB
实例,但无法得知是否真的和数据库成功连接。按照文档的说法,如果要确认是否和数据库真的连接上了,需要执行Ping
方法:
Open may just validate its arguments without creating a connection to the database. To verify that the data source name is valid, call Ping.
成功拿到DB对象之后,我们就可以操作数据库了。
下一篇,我们的主题将是连接池的维护
和what's behind the db.Query command
作者:suoga
链接:https://www.jianshu.com/p/3b0b3a4c83da
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 25岁的心里话
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
2019-10-17 【转】angular使用代理解决跨域
2019-10-17 在angular项目中使用web-component ----How to use Web Components with Angular
2019-10-17 Error: EACCES: permission denied when trying to install ESLint using npm
2018-10-17 M.2接口NVMe协议的固态硬盘读写速度是SATA接口的两倍
2018-10-17 linux shell的执行方式
2018-10-17 linux查看当前shell的方法
2018-10-17 linux 的空命令:(冒号)