本文翻译自https://github.com/go-sql-driver/mysql/wiki
2023/03/05
database/sql包的详细介绍可以在这里找到:http://go-database-sql.org/
浅谈sql.Open
函数
首先,你应该了解sql.DB
不是一个连接。当你使用sql.Open()
时,你将获得数据库的句柄。database/sql
包在后台管理一个连接池,并且在你需要它们之前不会打开任何连接。因此sql.Open()
不直接打开连接。因此,如果服务器不可用或连接数据的(用户名,密码)不正确,sql.Open()
不会返回一个错误。如果你想在进行查询之前检查是否成功连接到数据库(例如在应用程序启动时),你可以使用db.Ping()
。
db, err := sql.Open("mysql", "user:password@/dbname")
if err != nil {
panic(err.Error()) // 此处只是示例,在实际开发中你应该使用合适的错误处理函数,而不是使用panic
}
defer db.Close()
// 上面的Open函数不真的打开一个连接。可以使用以下函数来验证连接字符串是否能成功连接到数据库
err = db.Ping()
if err != nil {
panic(err.Error()) // 在实际开发中不要使用painc,而是使用合适的错误处理函数
}
// 以下省略执行查询语句等代码
[...]
预处理语句
假设一个具有以下结构的空表,表名是squareNum:
+--------------+---------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+--------------+---------+------+-----+---------+-------+
| number | int(11) | NO | PRI | NULL | |
| squareNumber | int(11) | NO | | NULL | |
+--------------+---------+------+-----+---------+-------+
在此示例中,我们准备了两条语句,一条用于插入元组(行),一条用于查询。
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
func main() {
db, err := sql.Open("mysql", "user:password@/database")
if err != nil {
panic(err.Error())
}
defer db.Close()
// 用于插入数据的预处理语句
stmtIns, err := db.Prepare("INSERT INTO squareNum VALUES( ?, ? )") // ?号是占位符
if err != nil {
panic(err.Error())
}
defer stmtIns.Close() // 当main函数快要运行完毕(程序终止)时,关闭预处理语句,释放占用的资源
// 用于查询数据的预处理语句
stmtOut, err := db.Prepare("SELECT squareNumber FROM squarenum WHERE number = ?")
if err != nil {
panic(err.Error())
}
defer stmtOut.Close()
// 插入0到24数字到数据库
for i := 0; i < 25; i++ {
_, err = stmtIns.Exec(i, (i * i)) // 插入元组(i, i^2)
if err != nil {
panic(err.Error())
}
}
var squareNum int // 我们在此处浏览结果
// 查询13的平方数
err = stmtOut.QueryRow(13).Scan(&squareNum) // WHERE number = 13
if err != nil {
panic(err.Error())
}
fmt.Printf("The square number of 13 is: %d", squareNum)
// 查询1的平方数试试
err = stmtOut.QueryRow(1).Scan(&squareNum) // WHERE number = 1
if err != nil {
panic(err.Error())
}
fmt.Printf("The square number of 1 is: %d", squareNum)
}
忽略NULL值
也许你已经遇到了这个错误:sql: Scan error on column index 1: unsupported driver -> Scan pair: <nil> -> *string
通常在这种情况下你会使用sql.NullString
。但有时你并不关心该值是否为NULL
,你只想将其视为一个空字符串。
你可以使用一个变通方法来做到这一点,这利用了一个事实,即nil []byte
被转换为空字符串。你可以简单地使用*[]byte
(或*sql.RawBytes
),而不是使用*string
作为rows.Scan(...)
的目标,因为它们可以接收nil
值:
[...]
var col1, col2 []byte
for rows.Next() {
// 用[]byte类型的变量作为Scan函数的出参
err = rows.Scan(&col1, &col2)
if err != nil {
panic(err.Error())
}
// 再转换为字符串,用于输出
fmt.Println(string(col1), string(col2))
}
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
func main() {
// 打开数据库连接
db, err := sql.Open("mysql", "user:password@/dbname")
if err != nil {
panic(err.Error())
}
defer db.Close()
// 执行查询
rows, err := db.Query("SELECT * FROM table")
if err != nil {
panic(err.Error())
}
// 获取列名
columns, err := rows.Columns()
if err != nil {
panic(err.Error())
}
// 创建一个切片用于存储一行数据
values := make([]sql.RawBytes, len(columns))
// rows.Scan想要'[]interface{}'作为一个参数, 所以我们必须把值复制到这样一个切片里。更多细节参考http://code.google.com/p/go-wiki/wiki/InterfaceSlice
scanArgs := make([]interface{}, len(values))
for i := range values {
scanArgs[i] = &values[i]
}
// 获取行数据
for rows.Next() {
// 从数据中获取RawBytes
err = rows.Scan(scanArgs...)
if err != nil {
panic(err.Error())
}
// 现在使用获取到的数据做点事情,此处我们仅仅输出每列的值
var value string
for i, col := range values {
// 这里我们可以检查值是否是nil(在数据库里就是NULL值)
if col == nil {
value = "NULL"
} else {
value = string(col) // 把列值转换为字符串用来输出
}
fmt.Println(columns[i], ": ", value)
}
fmt.Println("-----------------------------------")
}
// 检查在获取行数据的过程中,是否遇到了一个错误
if err = rows.Err(); err != nil {
panic(err.Error())
}
}
旧密码(old_passwords)
什么是old_passwords?
MySQL 4.1版(2004年发布!)更改了协议,引入了更安全的密码认证。添加了变量old_password
,它启用旧密码认证这种传统的认证方式,但禁用新的更安全的密码认证方式。旧密码认证方式使用非常弱的哈希,这就是为什么它被认为是不安全的。如果你不需要传统的旧密码认证方式,就不应该使用它!
由于旧密码不安全且已被弃用,因此Go语言的MySQL驱动程序在默认情况下不启用此认证方式。如果你依赖它,可以通过在DSN中添加allowOldPasswords=true
来显式启用它。
如何禁用它?
在MySQL的配置文件my.cnf
(Windows上为my.ini
)中将old_passwords
设置为false
。在Linux上,你可以在/etc/my.cnf
找到此文件。
变量old_passwords
属于mysqld
小节,如果在配置文件里找不到它,就添加它:
[mysqld]
old_passwords = 0
你可能还需要重新生成密码。有关如何升级,参考http://code.openark.org/blog/mysql/upgrading-passwords-from-old_passwords-to-new-passwords。
测试(Testing)
也许你需要编辑已经打开的数据库连接的参数。以下是如何设置不同密码的示例:
Linux/Unix
$ export MYSQL_TEST_PASS=root
Windows
$ SET MYSQL_TEST_PASS=root
参数列表
MYSQL_TEST_USER ( User用户名 )
MYSQL_TEST_PASS ( Password 密码)
MYSQL_TEST_PROT ( Network Protocol 网络协议)
MYSQL_TEST_ADDR (网络地址,IPv4或IPv6)
MYSQL_TEST_DBNAME (数据库名称)
MYSQL_TEST_CONCURRENT ( 值为1启用并发测试)
开发思路
在Auth响应中发送连接参数[MySQL 5.6.6+]
http://dev.mysql.com/doc/internals/en/capability-flags.html#flag-CLIENT_CONNECT_ATTRS
支持更多认证插件
- .mylogin.cnf http://dev.mysql.com/doc/refman/5.6/en/mysql-nutshell.html
- SHA-256 http://dev.mysql.com/doc/refman/5.6/en/sha256-authentication-plugin.html
- auth_socket http://dev.mysql.com/doc/refman/5.6/en/socket-authentication-plugin.html
网络压缩模式
http://dev.mysql.com/doc/internals/en/compression.html
使用分离的goroutine从连接中读取
目前,我们发送请求数据包,然后接收响应数据包。
由于Go没有提供有效的非阻塞EOF检查方法,即使服务器已经关闭连接,我们也可能会发送请求数据包。在已关闭的连接上发送请求会使ErrBadConn
不安全。 如果我们可以在发送数据包之前检测到EOF,我们就可以安全地使用ErrBadConn
。
使用分离的goroutine从连接中读取数据是Go的常规编程方式。读数据的goroutine 也可能需要实现对Context
支持。
可能会有显着的性能衰退。但我认为我们应该这样做。