目 录CONTENT

文章目录
Go

使用go-sql-driver操作Mysql

Hello!你好!我是村望~!
2023-02-13 / 0 评论 / 2 点赞 / 184 阅读 / 3,688 字
温馨提示:
我不想探寻任何东西的意义,我只享受当下思考的快乐~

使用go-sql-driver操作Mysql

简介

用于Godatabse/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&...&paramN=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 函数的源码位置!

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

这个 sql 就是 databse/msyql 标准库的 sql

导出MySQLDriver是为了使驱动可以直接访问。一般来说,驱动程序是通过database/sql使用的。

image-20230213153125012

sql.Register传入了一个 name 和一个空的 MySQLDriver 的结构体

  • name 表示注册驱动的名称 exp:“mysql”
  • MySQLDriver 就是实现标准库的驱动接口

image-20230213153501028

Register通过提供的名称使一个数据库驱动可用。如果Register以相同的名字被调用两次,或者drivernil。它就会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 标准库和第三方驱动库做了一个大致流程了解!

2

评论区