Go语言MySQL驱动程序GitHub wiki中文翻译

本文翻译自https://github.com/go-sql-driver/mysql/wiki

2023/03/05

例子

旧密码(old_passwords)

测试(Testing)

开发思路

例子

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))
}

RawBytes

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/connection-phase-packets.html#packet-Protocol::HandshakeResponse41

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支持。

可能会有显着的性能衰退。但我认为我们应该这样做。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注