两个概念
1.什么是数据库驱动(database-driver)?
2.什么是驱动管理(driver-manager) ?
本文以mysql数据库为例。
我们在Go中连接mysql数据库的时候,需要下载一个mysql驱动,那么下载的mysql-driver
驱动就是上述的database-driver
之后我们会操作数据库进行增删改查,那么操作数据库的接口可以理解成driver-manager ,go语言database/sql包就是操作数据库的接口,也就是driver-manager
因此manager 是语言自带的,而驱动需要从外部下载获得。go语言没有提供官方的数据库驱动,所有的数据库驱动都是第三方的,但是它们都遵循sql.driver 包里面定义的接口。
第一部分,深入了解一下database/sql
举个栗子,现代数据库多种多样,mysql,sql_server,oracle等等,各个厂商的数据库各异,编程语言总不能为每一个数据库都写一个特定的管理系统吧(那得多麻烦)。
如果是这样的话,那么编程语言得多么臃肿啊,对程序猿也不友好,要掌握那么多的数据库驱动规则(这谁顶得住啊!)
我语言(go语言)如果自定义一套标准的接口(上图database/sql),那么数据库厂商按照我这个提供的这个接口写一个驱动来适配这个接口(这个接口可以与go程序进行交互),那么语言只要制定一套标准不显得臃肿,程序员也可以只要掌握这个接口的使用就可以了(矛盾转移,将任务堆给数据库厂商)
上图可示database/sql接口包括两个层面:
1.面向应用的api,供程序员调用
2.面向数据库的api,供开发厂商开发数据的驱动程序
ok,开始下定义,第一部分完结~:
database/sql (相当于java的jdbc)是一个独立于特定数据库的管理系统,通用的sql数据库存取和操作的公共接口。定义了一组标准,为访问不同数据库提供了统一的途径。
第二部分,程序与数据库交互
要想连接到sql数据库,首先需要加载目标数据库的驱动,驱动里面包含着与该数据库交互的逻辑
那么如何加载目标数据库驱动?
正常的做法是使用sql.Register函数,参数是数据库驱动的名称,和一个实现了driver.Driver 接口的struct,来注册数据库的驱动,比如sql.Register("sqlserver",&drv{})
但是一般情况下我们在导入包 mysql驱动的时候(其他驱动也同理),这个驱动包会有一个init 函数自动执行,执行这个init 函数的时候就进行了自我的注册,也就是调用了上述的sql.register 函数
在引入mysql这个驱动包的时候,把该包的名设置为下划线_,这是因为我们不直接使用数据库驱动(只需要它起的”副作用“),我们只使用database/sql
现在我们看看mysql驱动里面干了啥
首先在引入这个mysql驱动包时候,它里面有个init 函数会被首先自动执行。它调用了database/sql 下的register 函数
Register这个函数所在的位置 goSDK/database/sql
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
}
从代码中可以看到Register这个函数往drivers 这个map中放入mysql这个驱动的名称(即"mysql")和这个驱动。啥是驱动?本质上也就是实现了特定方法的结构体,再来回顾下init 函数
func init() {
sql.Register("mysql", &MySQLDriver{})
}
它的驱动参数为了MySQLDriver 这个结构体,这个结构体就是实现了特定的驱动方法如下:
func (d MySQLDriver) Open(dsn string) (driver.Conn, error)
func (d MySQLDriver) OpenConnector(dsn string) (driver.Connector, error)
OK,这个就是mysql这个驱动包与database/sql包交互的第一层逻辑
mysql驱动包调用database/sql 驱动包下面的register 函数,并往sql 包下的drivers 这个map存放自身的驱动名和驱动信息。
OK,mysql驱动就讲这么多,现在我们回到Go中sql接口层面,毕竟这才是我们需要接触和掌握的
程序中操作数据库的第一行代码为
db, err := sql.Open("mysql", "root@tcp(localhost:3306)/golang?charset=utf8")
那么Open干了啥,当然要从源码入手啦
var (
driversMu sync.RWMutex
drivers = make(map[string]driver.Driver)
)
... ...
func Open(driverName, dataSourceName string) (*DB, error) {
driversMu.RLock()
driveri, ok := drivers[driverName]
driversMu.RUnlock()
//..删去一些代码片段
return OpenDB(dsnConnector{dsn: dataSourceName, driver: driveri}), nil
}
Open函数就是根据你传入的driverName 去drivers 这个map中寻找驱动,为什么它能找到呢,因为mysql这个驱动包init 的时候将驱动名和驱动放进这个map了嘛。
得到这个驱动后,执行OpenDB() 这个方法,参数是一个driver.Connector 接口
type Connector interface {
Connect(context.Context) (Conn, error)
Driver() Driver
}//这个接口实现了Connect和Driver这两方法。
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
}
听听go文档官方注释描述OpenDB 这个方法,毕竟我也不懂啊~
// OpenDB 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 OpenDB // function should be called just once. It is rarely necessary to // close a DB. // DB is a database handle representing a pool of zero or more // underlying connections. It's safe for concurrent use by multiple // goroutines.
意思是:
执行Open()这个函数并不会连接到数据库,甚至不会验证其参数。它只是把后续连接到数据所必需的struct给设置好了。而真正的连接是在被需要的时候才进行懒设置的。
sql.DB 不需要进行关闭。它就是用来处理数据库的,而不是实际的连接。它是一个包含了数据库连接的池的抽象,而且会对这个池进行维护Open() 这个函数执行成功会得到一个指向sql.DB 这个struct的指针
<画重点>sql.DB是用来操作数据库的,它代表了0个或者多个底层连接的池,这些连接由sql包来维护,sql会自动的创建和释放这些连接,它对于多个goroutine并发的使用是安全的。
也就是DB 代表的是一个连接池,连接池里面放了很多与数据库交互的链接。如果把DB看成是go程序和数据之间的桥梁,那么DB 里面的链接就可以看成是Go程序通往数据库的一列列火车。当然里面的细节(链接)不需要你自己去维护,你只要操作这个链接池(DB)就可以了,方便!
那么啥是连接池呢,为啥要有连接池?
//启动一个worker工作池
func (mh *MsgHandle) StartWorkerPool() {
for i := 0; i < int(mh.WorkerPoolSize); i++ {
//一个worker被启动
// 1 当前的worker对应的channel消息队列开辟空间
mh.TaskQueue[i] = make(chan ziface.IRequest, utils.GlobalObjetc.MaxWorkerTaskLen)
//启动当前的worker,阻塞等待消息从channel传进来
go mh.StartOneWorker(i, mh.TaskQueue[i])
}
}
func (mh *MsgHandle) StartOneWorker(workerID int, taskQueue chan ziface.IRequest) {
fmt.Println("WorkerID=", workerID, "is started...")
//不断的阻塞等待对应的消息队列的消息
for {
select {
//如果有消息过来,出列的就是一个客户端的request,执行当前request所绑定的业务
case request := <-taskQueue:
mh.DoMsgHandler(request)
}
}
}
这是我之前写过的一个tcp高并发服务器demo,里面的工作池与连接池类似。一个任务进来,你要开启一个协程去处理这个任务。那么你不能无限开协程啊,无限开个1千万?1e个协程?那你内存不得挂掉嘛?因此如果我们在处理任务前,制作一个池子里面分配适合自己电脑配置的处理任务协程数量,那么任务进来就丢入这个池子中,负载均衡地放入里面的处理任务协程队列中去处理。这样才不会造成熵增嘛~
第三部分,介绍完了Open()函数,该介绍查询方法啦
前面说了Open() 函数只是初始化连接到数据库所需要的各种配置,并不会真正地执行连接。真正的连接是在被需要的时候才进行懒设置的。
那如何测试连接是否成功呢?
package main
import (
"context"
"database/sql"
"fmt"
"log"
)
import _ "github.com/go-sql-driver/mysql"
func main() {
db, err := sql.Open("mysql",
"root@tcp(localhost:3306)/golang?charset=utf8")
if err != nil {
panic(err.Error())
}
ctx := context.Background()
err = db.PingContext(ctx)
if err != nil {
log.Fatalln(err.Error())
}
fmt.Println("Connected")
}
func(db *DB)PingContext() 该函数时用来验证与数据库的连接是否仍然有效,如有必要则建立一个连接。
这个函数需要一个Context(上下文)类型的参数,这种类型参数可以携带截止时间,取消信号和其它请求范围的值,并且可以横跨API边界和进程。
上例中,创建context 使用的时context.Background()函数。该函数返回一个非nil的空Context 它不会被取消,它没有值,没有截止时间。它通常用在main函数、初始化或者测试中,作为传入请求的顶级Context
关于什么是context 呢,以后会再开一个文章详细说说,现在不了解也没关系。
由输出可以得出数据库连接成功。
Tiny tip,连接数据库前先保证mysql服务是启动的
开启mysql服务 net start mysql
mysql -u root -p 进入mysql
sql.DB类型上用于查询的方法有
1·Query (查询的是0行或者是多行的记录)
2·QueryRow (返回的是一条记录)
与这两个配套的方法是加上 上下文,可以在查询的时候带上 上下文,如截止时间,取消操作,或者所需要的值
3·QueryContext
4·QueryRowContext
1·func (db DB) Query(query string, args ...interface{}) (Rows, error)
返回的结果是type Rows struct{... ...}
现在我们在源码中康康Rows它有哪些方法(一定要学会看源码!!!)
func (rs *Rows) Scan(dest ...interface{})
Scan将数据库中当前行的数据拷贝到dest所指向的值中,dest中的值个数必须与Rows中的列数相同。
Scan将从数据库读取的列转换为以下常见的Go类型和sql包提供的特殊类型:
*string
*[]byte
*int, *int8, *int16, *int32, *int64
*uint, *uint8, *uint16, *uint32, *uint64
*bool
*float32, *float64
... ...
在最简单的情况下,如果源列的值类型是整型,bool或字符串类型T, dest类型*T, Scan只需通过指针赋值......
type ColumnType struct {
name string
hasNullable bool
hasLength bool
hasPrecisionScale bool
nullable bool
length int64
databaseType string
precision int64
scale int64
scanType reflect.Type
}
ColumnTypes返回列的类型、长度、是否可为空等信息(即建表时设置的列的属性)。
func (rs *Rows) Columns() ([]string, error)
Columns简单地返回所有的列名,如果在关闭的时候出现错误将返回一个错误
func (rs *Rows) Next() bool
遍历结果集,每次读取结果的一行,返回的结果为true表明还有数据,否之表示读到结果的末尾
2.func (db DB) QueryRow(query string, args ...interface{}) Row
QueryRow返回的是Row,注意和Query不一样啊,人家是Rows,多了个ssssss,那么Row这个方法就简单多了(可以少码很多了ye)
func (r *Row) Scan(dest ...interface{}) error
也是从结果中拷贝数据到后面跟着的dest中去,还要一个err方法我就不讲了。
3当然DB还对应很多方法
在官方文档中pkg.go.dev/database/sq… 还介绍有很多,有兴趣的同学自己查阅~
第四部分,实战
先理清逻辑。
我们在通过Open()函数得到一个*DB(为什么能得到上面有讲解,不理解的往上自习阅读),得到DB后我们就可以操作数据库。
选择DB中的QueryRow方法为例,它返回一个Row。Row中一个方法是Scan,即 可以拷贝数据库中的数据到我们的结构体中。
OK,Talk is free,Show me the code
package main
type user struct {
created_at string
id int
name string
telephone string
pass_word string
}
在model层中定义一个与数据库中字段匹配的结构体,它用来存储scan读出的数据
package main
func getOne(id int) (user, error) {
a := user{}
err := db.QueryRow("SELECT id,name,telephone,pass_word,created_at from users where users.id=?", id).
Scan(&a.id, &a.name, &a.telephone, &a.pass_word, &a.created_at)
return a, err
}
services层中书写与数据交互的代码。
package main
import (
"context"
"database/sql"
"fmt"
"log"
)
import _ "github.com/go-sql-driver/mysql"
var db *sql.DB
var err error
func main() {
db, err = sql.Open("mysql", "root@tcp(localhost:3306)/ginessential?
charset=utf8")
if err != nil {
panic(err.Error())
}
ctx := context.Background()
err = db.PingContext(ctx)
if err != nil {
log.Fatalln(err.Error())
}
fmt.Println("Connected")
//查询一笔
one, err := getOne(2)
if err != nil {
log.Fatalln(err.Error())
}
fmt.Println(one)
}
入口main函数
打印输出的运行结果
打开Navicat工具验证,与数据库中的数据是一致的