使用go-sql-driver操作Mysql
简介
用于
Go
的databse/sql
标准库的的MySQL-Driver
Go语言本身没有没有提供连接mysql的驱动,但是定义了标准接口供第三方开发驱动
Go语言中的database/sql
包提供了保证SQL或类SQL数据库的泛用接口,并不提供具体的数据库驱动。
使用database/sql
包时必须注入(至少)一个数据库驱动 这里使用的是 go-sql-driver
一款轻量和快速的sql驱动!
- 原生 Go 实现。没有C语言绑定,只有纯粹的Go
- 通过TCP/IPv4、TCP/IPv6、Unix域套接字或自定义协议进行连接
- 自动处理中断的连接
- 自动连接池(通过数据库/sql包)
- 可选的time.Time解析
安装/使用
首先准备一个 MySQL
数据库环境,这里我用 Docker
启动了一个本地的 MySQL
服务 mysql docker hub 地址
version: '3.1'
services:
db:
container_name: go-dirver-mysql-test
image: mysql
command: --default-authentication-plugin=mysql_native_password
restart: always
environment:
MYSQL_ROOT_PASSWORD: testgomysqldriver
ports:
- 3306:3306
volumes:
- /Users/hope/sdy/temp_go_test/mysql:/var/lib/mysql
然后就是成功启动我们本地docker
中的MySQL!顺便创建一个 tgo 的 database !
❯ docker compose up -d
[+] Running 1/0
⠿ Container go-dirver-mysql-test Running 0.0s
❯ docker container ls -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c430c5a7f84e mysql "docker-entrypoint.s…" 5 minutes ago Up 5 minutes 0.0.0.0:3306->3306/tcp, 33060/tcp go-dirver-mysql-test
下一步在golang
项目中安装mysql
驱动
go get -u github.com/go-sql-driver/mysql
Go MySQL驱动是Go的 data/sql/驱动接口的实现。只需要让驱动执行其内部的 init
方法,我们就可以使用了!
所以你只需要导入该驱动,就可以使用完整的database/sql API,不需要其变量名,那么在golang中我们可以用 _
代替导入变量名!
import (
"database/sql" // 使用的mysql包
_ "github.com/go-sql-driver/mysql" //驱动
)
使用mysql作为driverName,使用一个有效的DSN
作为dataSourceName
数据源名称有一个通用的格式,就像PEAR DB使用的那样,但没有类型前缀(方括号标记的可选部分)。
[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]
一个最完整形式的DSN。
username:password@protocol(address)/dbname?param=value
测试连接
这里需要注意一下:Open 函数只是验证其参数格式是否正确,实际上并不创建与数据库的连接。
package main
import (
"database/sql"
"fmt"
"time"
_ "github.com/go-sql-driver/mysql"
)
var err error
func main() {
dbUser := "xxx"
dbPassword := "xxx"
dbName := "xxx"
dbURL := "127.0.0.1:3301"
sqlDSN := fmt.Sprintf("%s:%s@tcp(%s)/%s", dbUser, dbPassword, dbURL, dbName)
db, err := sql.Open("mysql", sqlDSN)
if err != nil {
panic(err)
}
defer db.Close()
db.SetConnMaxLifetime(time.Minute * 3)
db.SetMaxOpenConns(10)
db.SetMaxIdleConns(10)
fmt.Println("end")
}
比如上面的执行之后,控制台不会有任何连接失败的报错信息
❯ go run main.go
end
所以如果要检查数据源的名称是否真实有效,应该调用Ping方法。
err = db.Ping()
if err != nil {
panic(err)
}
那么此时,再去执行下 main.go 就发现连接失败并抛出异常了!
❯ go run main.go
panic: dial tcp 127.0.0.1:3301: connect: connection refused
goroutine 1 [running]:
main.main()
/Users/hope/sdy/temp_go_test/main.go:25 +0x218
exit status 2
连接成功示例:
package main
import (
"database/sql"
"fmt"
"time"
_ "github.com/go-sql-driver/mysql"
)
var err error
func main() {
dbUser := "root"
dbPassword := "testgomysqldriver"
dbName := "tgo"
dbURL := "127.0.0.1:3306"
sqlDSN := fmt.Sprintf("%s:%s@tcp(%s)/%s", dbUser, dbPassword, dbURL, dbName)
db, err := sql.Open("mysql", sqlDSN)
if err != nil {
panic(err)
}
err = db.Ping()
if err != nil {
panic(err)
}
fmt.Println("datebase connection success ! ")
defer db.Close()
db.SetConnMaxLifetime(time.Minute * 3)
db.SetMaxOpenConns(10)
db.SetMaxIdleConns(10)
}
执行main.go
❯ go run main.go
datebase connection success !
这样就是真的连接成功了!
关于DB
SQL库的open方法,返回一个 DB 对象。
返回的
DB
对象可以安全地被多个goroutine
并发使用,并且维护其自己的空闲连接池。因此,
Open
函数应该仅被调用一次,很少需要关闭这个DB对象。
func Open(driverName, dataSourceName string) (*DB, error)
一些重要的设置:
1.db.SetConnMaxLifetime()
db.SetConnMaxLifetime
确保在MySQL服务器、操作系统或其他中间件关闭连接之前,驱动会安全地关闭连接。由于一些中间件在5分钟前关闭空闲连接,我们建议超时时间短于5分钟。这个设置也有助于负载平衡和改变系统变量。
2.db.SetMaxOpenConns(n int)
用来限制应用程序使用的最大连接数 (go程序和mysql之间建立的最大连接数)。
如果n大于0且小于最大闲置连接数,会将最大闲置连接数减小到匹配最大开启连接数的限制。
如果n<=0,不会限制最大开启连接数
默认为0(无限制)。
3.db.SetMaxIdleConns()
SetMaxIdleConns
设置连接池中的最大闲置连接数。如果n大于最大开 启连接数,则新的最大闲置连接数会减小到匹配最大开启连接数的限制。
如果n<=0,不会保留闲置连接。
推荐与db.SetMaxOpenConns()
设置相同的值。
一个 defer db.Close()
简简单单的注意事项!
上面代码中有一个关闭操作
db, err := sql.Open("mysql", sqlDSN)
if err != nil {
panic(err)
}
defer db.Close()
这里去思考一下能不能写成下面这样呢?
db, err := sql.Open("mysql", sqlDSN)
defer db.Close()
if err != nil {
panic(err)
}
其实很简单啦!如果open
操作出错了的话,那么db
就可能是一个nil
那么此时如果把 db.Close()
注册到延迟执行的栈中!那么到后面会有一个不可控的报错!所以一定要做完错误检查之后,保证 db
可用,在把 db
的一些 close
操作注册到延迟执行栈中!
当使用其他第三方库,涉及到延迟操作的,也都要注意这个问题喔
建库建表测试执行sql语句
Exec 可以执行任何 sql 语句!
func (db *DB) Exec(query string, args ...interface{}) (Result, error)
package main
import (
"database/sql"
"fmt"
"time"
_ "github.com/go-sql-driver/mysql"
)
var err error
func main() {
dbUser := "root"
dbPassword := "testgomysqldriver"
dbName := "tgo"
dbURL := "127.0.0.1:3306"
sqlDSN := fmt.Sprintf("%s:%s@tcp(%s)/%s", dbUser, dbPassword, dbURL, dbName)
fmt.Println(sqlDSN)
db, err := sql.Open("mysql", sqlDSN)
if err != nil {
panic(err)
}
defer db.Close()
db.SetConnMaxLifetime(time.Minute * 3)
db.SetMaxOpenConns(10)
db.SetMaxIdleConns(10)
r, err := db.Exec("CREATE TABLE `userinfo` (`uid` int(10) NOT NULL AUTO_INCREMENT,`username` varchar(64) DEFAULT NULL,`departname` varchar(64) DEFAULT NULL,`created` date DEFAULT NULL,PRIMARY KEY (`uid`)) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;")
if err != nil {
panic(err)
}
fmt.Print(r.RowsAffected())
}
上面的代码,我们连接了mysql之后,然后创建了一个 userinfo
的表
运行一下,然后进入docker的mysql去看看执行情况!
❯ docker exec -it c430c5a7f84efe2342fef35910b6c7085c32c16c73973ae662c2742bea36899a bin/bash
> root@c430c5a7f84e:/# mysql -hlocalhost -uroot -p
Enter password:
mysql> use tgo;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
mysql> show tables;
+---------------+
| Tables_in_tgo |
+---------------+
| userinfo |
+---------------+
1 row in set (0.01 sec)
mysql>
可以看到 我们DB执行sql创建表已经成功了!
在模块化项目的使用姿势
之前演示的时候,都是在main函数里直接执行的!但是在正常模块化项目的话!都是封装在初始化模块中的,返回db
对象和error
!
package initialization
import (
"database/sql"
"fmt"
"time"
)
func InitDB() (*sql.DB, error) {
dbUser := "root"
dbPassword := "testgomysqldriver"
dbName := "tgo"
dbURL := "127.0.0.1:3306"
sqlDSN := fmt.Sprintf("%s:%s@tcp(%s)/%s", dbUser, dbPassword, dbURL, dbName)
db, err := sql.Open("mysql", sqlDSN)
if err != nil {
return nil, err
}
err = db.Ping()
if err != nil {
return db, err
}
fmt.Println("database connection success ! ")
db.SetConnMaxLifetime(time.Minute * 3)
db.SetMaxOpenConns(10)
db.SetMaxIdleConns(10)
return db, nil
}
然后在main程序入口模块调用就可以了!
package main
import (
"database/sql"
"gotest/initialization"
_ "github.com/go-sql-driver/mysql"
)
var db *sql.DB
var err error
func main() {
db, err = initialization.InitDB()
if err != nil {
panic(err.Error())
}
defer db.Close()
}
试试CRUD
Exec执行一次命令(包括查询、删除、更新、插入等),返回的Result是对已执行的SQL命令的总结。参数args表示query中的占位参数。
插入数据
func insertDemo(username string, departname string) (sql.Result, error) {
sqlStr := "insert into userinfo(username,departname) values (?,?);"
return db.Exec(sqlStr, username, departname)
}
调用
result, err := insertDemo("ch", "技术部")
if err != nil {
fmt.Printf(err.Error())
}
rowsAffected, err := result.RowsAffected()
if err != nil {
fmt.Printf(err.Error())
}
fmt.Println(rowsAffected) // 1
查询
为了方便查询,我们事先定义好一个结构体来存储userinfo
表的数据。
type userinfo struct {
uid int64
username string
departname string
}
单行查询
单行查询
db.QueryRow()
执行一次查询,并期望返回最多一行结果(即Row)。
QueryRow总是返回非nil的值,直到返回值的
Scan
方法被调用时,才会返回被延迟的错误。(如:未找到结果)
// 查询单条数据示例
func queryRowDemo(uid int64) (userinfo, error) {
sqlStr := "select uid, username, departname from userinfo where uid= ?"
var u userinfo
// 非常重要:确保QueryRow之后调用Scan方法,否则持有的数据库链接不会被释放
return u, db.QueryRow(sqlStr, uid).Scan(&u.uid, &u.username, &u.departname)
}
❯ go run main.go
database connection success !
7 ch 技术部
多行查询
多行查询
db.Query()
执行一次查询,返回多行结果(即Rows),一般用于执行select命令。参数args表示query中的占位参数。
// 查询多条数据示例
func queryRowsDemo() ([]userinfo, error) {
sqlStr := "select uid, username, departname from userinfo"
var us []userinfo
rows, err := db.Query(sqlStr)
if err != nil {
return us, err
}
// 非常重要:关闭rows释放持有的数据库链接
defer rows.Close()
// 循环结果!
for rows.Next() {
var u userinfo
err := rows.Scan(&u.uid, &u.username, &u.departname)
if err != nil {
return us, err
}
us = append(us, u)
}
return us, err
}
调用
userinfos, err := queryRowsDemo()
if err != nil {
fmt.Println(err.Error())
}
fmt.Println(len(userinfos)) // 7
删除数据
func deleteRowDemo(uid int64) (bool, error) {
sqlStr := "delete from userinfo where uid = ?"
r, err := db.Exec(sqlStr, uid)
if err != nil {
return false, err
}
_, err = r.RowsAffected() // n, err := ret.RowsAffected() // 操作影响的行数
if err != nil {
return false, err
}
return true, nil
}
更新数据
func updateRowDemo(uid int64, u *userinfo) (bool, error) {
sqlStr := "update userinfo set username=?, departname=? where uid = ?"
r, err := db.Exec(sqlStr, u.username, u.departname, uid)
if err != nil {
return false, err
}
_, err = r.RowsAffected() // n, err := ret.RowsAffected() // 操作影响的行数
if err != nil {
return false, err
}
return true, nil
}
执行测试
newUser := &userinfo{
username: "changhao",
departname: "硬件部门",
}
_, err := updateRowDemo(8, newUser)
if err != nil {
panic(err)
}
u, err := queryRowsDemo()
if err != nil {
panic(err)
}
for i := range u {
fmt.Println(u[i].uid, u[i].username, u[i].departname)
}
❯ go run main.go
database connection success !
8 changhao 硬件部门
9 ch 技术部
10 ch 技术部
11 ch 技术部
12 ch 技术部
13 ch 技术部
mysql 注册驱动源码分析
driver init 函数的源码位置!
func init() {
sql.Register("mysql", &MySQLDriver{})
}
这个 sql 就是 databse/msyql
标准库的 sql
导出MySQLDriver是为了使驱动可以直接访问。一般来说,驱动程序是通过database/sql使用的。
sql.Register传入了一个 name
和一个空的 MySQLDriver
的结构体
name
表示注册驱动的名称 exp:“mysql”MySQLDriver
就是实现标准库的驱动接口
Register
通过提供的名称使一个数据库驱动可用。如果Register
以相同的名字被调用两次,或者driver
为nil
。它就会panic
。
var (
driversMu sync.RWMutex
drivers = make(map[string]driver.Driver)
)
func Register(name string, driver driver.Driver) {
// 因为 drivers 是一个原生 map 类型!在go语言中的实现是并发不安全的
// 所以要加锁,防止并发的时候同一个驱动被多次注册!造成panic!
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
}
注册完成后,就会把当前的sql驱动注册到我们的datebase
标准库中!
注册成功当Open去连接的时候!对Open函数加了一个读锁
根据 name 取出数据库驱动!进行了一个类型转换!把第三方驱动 driveri
,转换为 sql
标准库的类型 DriverContext
!
第三方本身就是实现的标准库接口,所以没有任何问题!
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
}
然后下一步就是去 OpenDB 打开一个数据库使用连接器,返回一个DB对象
func OpenDB(c driver.Connector) *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 // 等待新连接的总时间。
connector driver.Connector
numClosed uint64 // numClosed是一个原子计数器,代表了已关闭连接的总数。
mu sync.Mutex // protects following fields
freeConn []*driverConn // 按照returnAt从旧到新排序的空闲连接数
connRequests map[uint64]chan connRequest
nextRequest uint64 // Next key to use in connRequests.
numOpen int //已开和待开的连接数
// 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 // 最后一个连接的堆栈跟踪;仅用于调试。
maxIdleCount int // zero means defaultMaxIdleConns; negative means 0
maxOpen int // 最大连接数 0 表示没有限制
maxLifetime time.Duration // 一个连接可重复使用的最大时间
maxIdleTime time.Duration // 最大闲置时间
cleanerCh chan struct{}
waitCount int64 // Total number of connections waited for.
maxIdleClosed int64 // Total number of connections closed due to idle count.
maxIdleTimeClosed int64 // Total number of connections closed due to idle time.
maxLifetimeClosed int64 // Total number of connections closed due to max connection lifetime limit.
stop func() // stop cancels the connection opener.
}
这里也只是对内部 databse/sql
标准库和第三方驱动库做了一个大致流程了解!
评论区