Server sent charset (255) unknown to the client. Please, report to the developers的解决方法

MySQL 8将默认字符集更改为utf8mb4。但是有些客户端不知道这个字符集。因此,当服务器向客户端报告其默认字符集,而客户端不知道其含义时,就会抛出此错误。

该错误针对C++实现的MySQL Connector,因此它影响的不仅仅是PHP。

正确的解决方案是升级你的客户端,但与此同时我通过将服务器的字符集更改为utf8来使其正常工作,以与未升级的客户端兼容。我将以下配置添加到/etc/my.cnf并重新启动mysqld:

[mysqld]
collation-server = utf8_unicode_ci
character-set-server = utf8

[client]
default-character-set=utf8

[mysql]
default-character-set=utf8

参考

https://stackoverflow.com/questions/43437490/pdo-construct-server-sent-charset-255-unknown-to-the-client-please-rep

https://bugs.mysql.com/bug.php?id=71606

Go语言database/sql包教程

本文翻译自《Go database/sql tutorial》。

在Go中使用SQL或类SQL的数据库的惯用方法是通过database/sql包。它为面向行的数据库提供了一个轻量级的接口。这个网站是关于如何使用它的最常见的方面的参考。

为什么需要这样做?这个包的文档告诉了你所有的功能,但并没有告诉你如何使用这个它。我们中的许多人发现自己希望获得一份入门手册,即讲故事,而不是列事实。欢迎你来做贡献:请在此处发送拉取请求。

概述

要在Go中访问数据库,你需要使用sql.DB。你可以使用sql.DB类型来创建语句和事务、执行查询和获取结果。

你应该知道的第一件事是sql.DB不是一个数据库连接。它也没有映射到任何特定数据库软件的“数据库”或“模式”这些概念。它是数据库的接口和数据库本身的抽象,抽象的内部可以多种多样:数据库可以是本地文件,可以通过网络连接访问,也可以存在于内存和进程中。

sql.DB在幕后为你执行一些重要任务:

  • 它通过数据库驱动程序打开和关闭与实际底层数据库的连接。
  • 它根据需要管理一个连接池,其中可能包括前面提到的各种内容。

sql.DB抽象旨在避免你担心如何管理对底层数据存储的并发访问。当你使用连接执行任务时,它会被标记为正在使用,然后在不再使用时返回到可用连接池。这样做的一个后果是,如果无法将连接释放回到连接池,可能会导致sql.DB打开大量连接,从而可能耗尽资源(连接太多、打开的文件句柄太多、缺少可用的网络端口等)。我们稍后将对此进行更多讨论。

在创建了一个sql.DB之后,你可以使用它来查询它所代表的数据库,以及创建语句和事务。

导入一个数据库驱动

要使用database/sql,你需要该包本身,和要使用的特定数据库的驱动程序。

你通常不应该直接使用特定数据库的驱动程序包,尽管一些驱动程序鼓励你这样做。(在我们看来,这通常是个坏主意。)相反,如果可能,你的代码应该只引用database/sql中定义的类型。这有助于避免让你的代码依赖于特定的驱动程序,当需要更改底层的数据库驱动程序(以及你正在访问的数据库)时,你就可以通过最少的代码来更改。你应该使用Go语言的惯用法,而不是驱动程序作者可能提供的特定的惯用法。

在本文档中,我们将使用@julienschmidt和@arnehormann开发的优秀的MySQL驱动程序作为示例。 将以下内容添加到Go源代码文件的顶部:

import (
	"database/sql"
	_ "github.com/go-sql-driver/mysql"
)

请注意,我们正在匿名加载这个驱动程序,将其包的别名设置为_,因此我们的代码看不到任何它的导出名称。在底层,该驱动程序将自己注册为对database/sql包可用,通常除了运行包的init函数之外没有其他任何事情发生。

现在你已准备好了访问数据库。

访问数据库

现在你已经加载了数据库驱动程序包,你已准备好创建一个数据库对象,一个sql.DB

要创建sql.DB,你可以使用sql.Open(),返回一个*sql.DB

func main() {
	db, err := sql.Open("mysql",
		"user:password@tcp(127.0.0.1:3306)/hello")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()
}

在以上示例中,我们说明了几件事:

1 sql.Open函数的第一个参数是驱动程序的名称。这是驱动程序用来向database/sql注册自己的字符串,通常与包名相同以避免混淆。例如,上例中是github.com/go-sql-driver/mysql包的mysql。一些驱动程序不遵循这一约定使用对应的数据库名称,例如github.com/mattn/go-sqlite3包的sqlite3github.com/lib/pq包的postgres

2 第二个参数是特定于驱动程序的数据源名称字符串(译者注:数据库连接字符串),它告诉驱动程序如何访问底层的数据存储。在此示例中,我们连接到本地的MySQL服务器中的“hello”数据库实例。

3 你应该(几乎)总是检查和处理从所有database/sql操作返回的错误。稍后我们将讨论一些特殊情况,在这些情况下这样做没有意义。

4 如果sql.DB的生命周期不应超出调用它函数的范围,则defer db.Close()是惯用的做法。

也许与直觉相反,sql.Open()并不建立任何与数据库的连接,也不验证驱动程序连接参数。相反,它只是为以后的使用做好准备,在程序确实需要用到与底层数据存储的实际连接时,才真正建立第一个连接。如果你想立即检查数据库是否可用和可访问(例如,检查你是否可以建立网络连接并登录),请使用db.Ping()来执行此操作,并记住检查是否返回了一个错误:

err = db.Ping()
if err != nil {
	// 错误处理
}

尽管在完成数据库时Close()是惯用方法,但sql.DB对象被设计为长寿命的。不要经常Open()Close()数据库。相反,为你需要访问的每个不同的数据存储(数据库)创建一个sql.DB对象,并保留它,直到程序完成对该数据存储的访问。根据需要传递,或者以某种方式在全局范围内提供,保持打开状态。不要从一个短暂的函数中Open()Close()。相反,将sql.DB作为参数传递到那个短暂的函数中。

如果你不将sql.DB视为一个长时间存活的对象,你可能会遇到一些问题,例如连接的重用和共享不足、可用网络资源不足,或者由于许多TCP连接仍处于TIME_WAIT状态而出现偶发故障。这些问题表明你没有按照设计使用database/sql

现在是时候使用sql.DB对象了。

获取结果集

有几个惯用操作可以从数据存储中检索结果:

1 执行返回多行数据的查询。

2 准备一个要重复使用的SQL预处理语句,多次执行,然后销毁它。

3 以一次性的方式执行SQL语句,而无需为重复使用做准备。

4 执行一个返回单行数据的查询。对于这种特殊情况,有一种获取数据的快捷的方式。

Go的database/sql包里的函数名称非常重要。如果函数名中包含Query,那么它被设计为向数据库询问问题,并返回一组行数据或空。不返回行数据的语句不应该使用Query函数;他们应该使用Exec函数。

从数据库中获取数据

让我们看一个如何查询数据库并处理返回结果的示例。我们将在users表中查询id1的用户,并打印出该用户的idname。我们将把结果分配给变量,一次一行,用rows.Scan()函数。

var (
	id int
	name string
)
rows, err := db.Query("select id, name from users where id = ?", 1)
if err != nil {
	log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
	err := rows.Scan(&id, &name)
	if err != nil {
		log.Fatal(err)
	}
	log.Println(id, name)
}
err = rows.Err()
if err != nil {
	log.Fatal(err)
}

以下是上面代码中发生的情况:

1 我们使用db.Query()将查询发送到数据库。我们像往常一样检查错误。

2 我们推迟执行rows.Close()。这非常重要。

3 我们用rows.Next()对行进行迭代。

4 我们使用rows.Scan()将每一行中的列值读取到对于的变量里。

5 在完成对行的迭代之后,我们检查这一过程中是否有错误发生。

这几乎是Go语言中唯一的方式。例如,你不能将一行数据读取到一个map里。这是因为所有东西都是强类型的。你需要创建正确类型的变量,并将指针传递给rows.Scan(),如上所示。

其中有几个部分很容易出错,并可能会产生不良后果:

  • 你应该始终在for rows.Next()循环的末尾检查是否有错误发生。如果在循环过程中出现错误,你需要了解它。不要只假设循环会正常迭代完毕所有行。
  • 其次,只要有一个打开的结果集(由rows表示),底层连接就很忙,不能用于任何其他查询。这意味着它在连接池中不可用。如果你使用rows.Next()遍历所有行,最终你将读取完最后一行,rows.Next()将遇到内部EOF错误并为你调用rows.Close()。但是,如果出于某种原因退出了那个循环——提前返回,等等——那么这些rows就不会关闭,底层数据库连接仍然是打开的。(不过,如果rows.Next()由于错误而返回false,它将会自动关闭)。记得defer rows.Close(),否则很容易耗尽资源。
  • 如果rows已经关闭,那么rows.Close()已是一个无害的空操作(no-op),所以你可以多次调用它。然而,请注意,我们首先检查是否有错误发生,如果没有错误发生才调用rows.Close(),以避免运行时panic
  • 你应该始终defer rows.Close(),即使你也在循环结束时显式调用rows.Close(),这也不是一个坏主意。
  • 不要在循环体中deferdefer语句要等到函数退出后才能执行,所以长时间运行的函数不应该使用它。如果这样做,你会慢慢耗尽内存。如果在循环中重复查询和使用结果集,那么在处理完每个结果后,应该显式调用rows.Close(),而不要使用defer

Scan()是怎么工作的

当你迭代行并将其中的列值扫描到目标变量中时,Go会在后台为你执行数据类型转换工作。它基于目标变量的类型。意识到这一点可以清理代码并避免重复工作。

例如,假设你从包含字符串列值的表中选择一些行,例如包含VARCHAR(45)的列或类似的行。然而,你碰巧知道,这个字符串列总是包含数字。如果将指针传递给字符串,Go将把字节复制到字符串中。现在,你可以使用strconv.ParseInt()或类似的方法将值转换为数字。你必须检查SQL操作中是否有错误发生,以及解析整数时是否有错误发生。这既混乱又乏味。

或者,你可以直接向Scan()传递一个指向整数的指针。Go将检测到这一点,并为你调用strconv.ParseInt()。如果转换中出现错误,调用Scan()会返回错误。你的代码现在更整洁、更小了。这是我们建议使用database/sql包的方式。

准备语句(预处理语句)

通常,你应该始终准备要多次使用的查询语句。准备查询语句的结果是一个准备好的语句(译者注:准备语句,预处理语句),它可以为语句的参数提供占位符(也称为绑定值)。由于所有常见的原因(例如,避免SQL注入攻击),这比连接SQL语句加参数值字符串的做法要好得多。

在MySQL中,参数占位符是?,在PostgreSQL中是$N,其中N是一个数字。SQLite接受其中任何一种。在Oracle中,占位符以冒号开头并命名,如:param1。我们将使用?因为我们使用MySQL作为示例。

stmt, err := db.Prepare("select id, name from users where id = ?")
if err != nil {
	log.Fatal(err)
}
defer stmt.Close()
rows, err := stmt.Query(1)
if err != nil {
	log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
	// ...
}
if err = rows.Err(); err != nil {
	log.Fatal(err)
}

在底层,db.Query()实际上准备、执行和关闭一个准备好的语句。这会对数据库进行三次往返访问。如果你不小心,你的应用程序进行的数据库交互次数可能会增加三倍!某些驱动程序可以在特定情况下避免这种情况,但并非所有驱动程序都这样做。有关详细信息,请参阅准备好的语句

查询单行

如果查询最多返回一行,则可以使用以下快捷的方式绕过一些冗长的样板代码:

var name string
err = db.QueryRow("select name from users where id = ?", 1).Scan(&name)
if err != nil {
	log.Fatal(err)
}
fmt.Println(name)

查询中的错误会延迟到调用Scan(),然后返回。准备好的语句也可以调用QueryRow()

stmt, err := db.Prepare("select name from users where id = ?")
if err != nil {
	log.Fatal(err)
}
defer stmt.Close()
var name string
err = stmt.QueryRow(1).Scan(&name)
if err != nil {
	log.Fatal(err)
}
fmt.Println(name)

修改数据和使用事务

现在我们已经准备好了解如何修改数据和处理事务。如果你习惯于使用“语句(statement)”对象来获取行和更新数据的编程语言,那么Go语言的做法与它们有点区别,这种区别看起来是人为的,但有一个重要的原因。

修改数据的语句

使用Exec(),最好与准备语句(预处理语句)一起使用,以完成INSERTUPDATEDELETE或其他不返回行数据的语句。以下示例显示如何插入行并检查有关操作的元数据:

stmt, err := db.Prepare("INSERT INTO users(name) VALUES(?)")
if err != nil {
	log.Fatal(err)
}
res, err := stmt.Exec("Dolly")
if err != nil {
	log.Fatal(err)
}
lastId, err := res.LastInsertId()
if err != nil {
	log.Fatal(err)
}
rowCnt, err := res.RowsAffected()
if err != nil {
	log.Fatal(err)
}
log.Printf("ID = %d, affected = %d\n", lastId, rowCnt)

执行Exec()会返回一个sql.Result,它可以访问语句的元数据:最后插入行的ID和受影响的行数。

如果你不在乎执行结果怎么办?如果你只想执行一条语句并检查是否有任何错误发生,但忽略执行的结果怎么办?下面两行代码会做同样的事情吗?

_, err := db.Exec("DELETE FROM users")  // 可行
_, err := db.Query("DELETE FROM users") // 不可行

答案是不一样。它们不做同样的事情,你不应该像这样使用Query()Query()将返回一个sql.Rows,它保留数据库连接直到sql.Rows关闭。由于可能有未读数据(例如多行数据的情况),因此无法重用连接。在上面的示例中,连接将永远不会被再次释放。垃圾收集器最终会为你关闭底层的net.Conn,但这可能需要很长时间之后。此外,database/sql包一直在连接池中跟踪这条连接,希望你在某个时候释放它,以便可以再次使用该连接。因此,这种反模式可能会耗尽资源(例如,太多连接)。

使用事务

在Go中,事务本质上是一个保留到数据库连接的对象。它允许执行到目前为止我们见过的所有操作,并且保证它们将在同一条连接上执行。

通过调用db.begin()开始事务,并对生成的Tx变量使用Commit()Rollback()方法结束事务。在底层,Tx从连接池中获取一条连接,并将其保留为仅用于该事务。sql.DB的方法Tx也拥有对应的方法,例如Query()等等。

在事务中创建的准备语句专门绑定到该事务。有关详细信息,请参阅准备好的语句

你不应该在代码中混合使用事务相关的函数(例如Begin()Commit())和SQL语句(例如BeginCommit)。可能会出现以下不好的结果:

  • Tx对象保持打开的状态,保留连接池中的一条连接而不返回。
  • 数据库的状态可能与用来表示它的Go变量的状态不同步。
  • 你可能会认为你在事务内部的一个连接上执行查询,而实际上Go已经无形地为你创建了几个连接,而有些语句不属于事务的一部分。

在事务内部工作时,应注意不要调用db变量。应该使用db.Begin()创建的Tx变量进行所有调用。db不在事务中,只有Tx对象在事务中。如果你调用db.Exec()或类似的函数,这些调用将发生在该事务范围之外的其他连接上,不在该事务中执行。

如果你需要执行修改连接状态的多个语句,即使你本身不想使用事务,也需要Tx。例如:

  • 创建仅对一个连接可见的临时表。
  • 设置数据库变量,例如MySQL的SET @var := somevalue语法。
  • 更改数据库的连接选项,例如字符集或超时时间。

如果你需要做这些事情中的任何一件,你需要将你的活动绑定到一个连接上,而在Go中做到这一点的唯一方法就是使用Tx

使用准备好的语句

准备好的语句具有Go中所有常见的好处:安全、高效、方便。但它们的实现方式与你习惯的可能有点不同,尤其是在如何与database/sql包的一些内部交互方面。

准备好的语句和数据库连接

在数据库级别,准备好的语句被绑定到单个数据库连接。典型的流程是,客户端向服务器发送一个带有参数占位符的SQL语句进行准备,服务器生成该语句的一个ID进行响应,然后客户端通过发送其ID和参数来执行该语句。

然而,在Go中,数据库连接不会直接暴露给database/sql包的用户。你不是在一条数据库连接上准备SQL语句。你可以在一个DBTx的实例上准备它。database/sql包有一些方便的行为,比如自动重试。由于这些原因,存在于驱动程序级别的准备好的语句和数据库连接之间的潜在关联,对代码来说是透明的。

以下是它的工作原理:

1 准备语句时,它是在连接池中的某条连接上准备的。

2 Stmt实例会记住使用了哪条连接。

3 当你执行Stmt时,它会尝试使用该连接。如果该连接因为关闭或忙于做其他事情而不可用,它将从连接池中获取另一条连接,并在这条连接上重新准备语句

由于语句在原始连接繁忙时,会根据需要重新准备,因此当数据库在高并发使用时,可能会使许多连接繁忙,从而创建大量准备好的语句。这可能会导致明显的语句泄漏,语句的准备和重新准备的频率比你想象的要高,甚至会遇到服务器端对语句数量的限制。

避免准备好的语句

Go在底层为你创建准备好的语句。例如,一个简单的db.Query(sql, param1, param2)的工作原理是准备SQL语句,然后用参数执行它,最后关闭该语句。

然而,某些时候准备好的语句并不是你想要的,可能有如下几个原因:

1 某些数据库不支持准备好的语句。例如,当使用MySQL驱动程序时,你可以连接到MemSQL和Sphinx,因为它们支持MySQL连接协议。但它们不支持包含准备好的语句的“二进制”协议,因此它们可能会以令人困惑的方式执行失败。

2 某些SQL语句的复用程度不足以使用准备好的语句,而且安全问题可以用其他方式处理,因此无需额外性能开销。这方面的一个例子可以在VividCortex的博客上看到。

如果你不想使用准备好的语句,你需要使用fmt.Sprint()或类似的方法来组装SQL,并将其作为唯一的参数传递给db.Query()db.QueryRow()。你的驱动程序需要支持执行纯文本查询,这是通过ExecerQueryer接口在Go 1.1中实现的,文档在此

在事务中使用准备好的语句

Tx实例中创建的准备好的语句专门绑定到Tx实例本身,因此前文关于重新准备的注意事项不再适用。当你对Tx实例进行操作时,你的操作会直接使用Tx实例底层连接的唯一一条连接。

这也意味着在Tx实例中创建的准备好的语句不能与它分开使用。同样,在DB中创建的准备好的语句也没法在事务中使用,因为它们被绑定到不同的数据库连接。

要在Tx中使用在事务外部准备的语句,可以使用Tx.Stmt(),它将从事务外部的准备好的语句创建一个新的绑定到本事务的准备好的语句。它通过获取一个已准备好的现有语句,设置与事务的连接,并在每次执行时重新准备所有语句来实现这一点。这种行为及其实现是不可取的,甚至在database/sql包源代码中有一个TODO来改进它;我们建议不要使用这种方法。

在事务中使用准备好的语句时必须谨慎。考虑以下示例:

tx, err := db.Begin()
if err != nil {
	log.Fatal(err)
}
defer tx.Rollback()
stmt, err := tx.Prepare("INSERT INTO foo VALUES (?)")
if err != nil {
	log.Fatal(err)
}
defer stmt.Close() // 危险!
for i := 0; i < 10; i++ {
	_, err = stmt.Exec(i)
	if err != nil {
		log.Fatal(err)
	}
}
err = tx.Commit()
if err != nil {
	log.Fatal(err)
}
// stmt.Close()在此处return

在Go 1.4关闭一个*sql.Tx之前,它会把与其关联的连接释放回连接池中,但对准备好的语句的延迟Close调用是在这之后执行的,可能会导致对基础连接的并发访问,从而导致连接状态不一致。如果使用Go 1.4或更低版本,则应确保在提交或回滚事务之前始终关闭该准备好的语句。在Go 1.4中修复了此问题,见CR 131650043

参数占位符语法

准备语句中占位符参数的语法是特定于数据库的。例如,比较MySQL、PostgreSQL和Oracle:

MySQL               PostgreSQL            Oracle
=====               ==========            ======
WHERE col = ?       WHERE col = $1        WHERE col = :col
VALUES(?, ?, ?)     VALUES($1, $2, $3)    VALUES(:val1, :val2, :val3)

错误处理

几乎所有使用database/sql包里的类型的操作都会返回一个错误变量作为最后一个值。你应该经常检查这些错误,永远不要忽视它们。

在一些地方,错误行为代表了一种特殊情况,或者你可能需要了解其他信息。

迭代结果集的错误处理

考虑以下代码:

for rows.Next() {
	// ...
}
if err = rows.Err(); err != nil {
	// 在此处处理错误
}

来自rows.Err()的错误可能是rows.Next()循环中,各种可能发生的错误的其中一种。除了正常完成循环之外,循环可能会出于某种原因退出,因此你始终需要检查循环是否正常终止。异常终止会自动调用rows.Close(),尽管多次调用它是无害的。

关闭结果集的错误处理

如前所述,如果提早退出循环,则应始终显式关闭sql.Rows。如果循环正常退出或出现错误,它会自动关闭,但你可能会错误地这样做:

for rows.Next() {
	// ...
	break; // 此时rows没有关闭。
}
// 关闭rows并检查是否有错误发生。多次调用rows.Close()是可以的,即使rows已经关闭了。
if err = rows.Close(); err != nil {
	// 如果此处真的有错误发生,我们又该怎么做呢?
	log.Println(err)
}

常规规则是最好在所有数据库操作中捕获并检查是否有错误发生,rows.Close()返回错误是常规规则的唯一例外。如果rows.Close()返回错误,则不清楚你应该怎么做。记录错误消息到日志或引发panic可能是唯一明智的做法,如果这不明智,那么也许你应该忽略该错误。

QueryRow()的错误处理

考虑以下返回单行数据的代码:

var name string
err = db.QueryRow("select name from users where id = ?", 1).Scan(&name)
if err != nil {
	log.Fatal(err)
}
fmt.Println(name)

如果没有id1的用户怎么办?这样结果中就不会有行,.Scan()也不会将值扫描到name中。然后会发生什么?

Go定义了一个特殊的错误常量,称为sql.ErrNoRows,当结果集为空时,它会从QueryRow()返回。在大多数情况下,这需要作为特殊情况处理。应用程序代码通常不会将空结果集视为一个错误,如果你不检查错误是否等于此特殊常量,则会导致意想不到的应用程序代码错误。

查询中的错误会延迟到调用Scan(),然后返回。上面的代码最好这样写:

var name string
err = db.QueryRow("select name from users where id = ?", 1).Scan(&name)
if err != nil {
	if err == sql.ErrNoRows {
		// 一行数据都没有,也没有其他错误发生
	} else {
		log.Fatal(err)
	}
}
fmt.Println(name)

有人可能会问,为什么空的结果集被认为是一个错误。空结果集并非错误。原因是QueryRow()方法需要使用这种特殊情况,以便让调用者区分QueryRow()是否真的找到了一行;如果没有它,Scan()就什么也不做,而你可能根本没有意识到你的变量没有从数据库中获得任何值。

只有在使用QueryRow()时才会遇到此错误。如果你在其他地方遇到这个错误,那就是你做错了什么。

鉴别特定的数据库错误

编写如下代码可能很诱人:

rows, err := db.Query("SELECT someval FROM sometable")
// err包含如下字符串:
// ERROR 1045 (28000): Access denied for user 'foo'@'::1' (using password: NO)
if strings.Contains(err.Error(), "Access denied") {
	// 处理"Access denied"错误
}

不过,这并不是最好的做法。例如,数据库服务器发送的错误消息所使用的语言不同时,我们会收到不同的字符串。比较错误号码来确定具体的错误是什么要好得多。

然而,实现这一点的机制因驱动程序而异,因为这不是database/sql本身的一部分。在本教程重点介绍的MySQL驱动程序中,你可以编写以下代码:

if driverErr, ok := err.(*mysql.MySQLError); ok { //现在可以直接获取错误号码
	if driverErr.Number == 1045 {
		// 处理拒绝权限错误
	}
}

同样,这里的MySQLError类型是由这个特定的驱动程序提供的,不同的驱动程序的.Number字段可能不同。然而,该数字的值取自MySQL的错误消息,因此是特定于数据库的,而不是特定于驱动程序的。

以上代码仍然很难看。像1045这样的神奇的数字是一种代码臭味。一些驱动程序(虽然不是MySQL驱动程序,但原因与这里的主题无关)提供了错误标识符列表。Postgres数据库的pq驱动程序在error.go中就是这样做的。还有一个由VividCortex维护的MySQL错误号码外部包。使用这样的列表,上面的代码可以更好地这样写:

if driverErr, ok := err.(*mysql.MySQLError); ok {
	if driverErr.Number == mysqlerr.ER_ACCESS_DENIED_ERROR {
		// 处理拒绝权限错误
	}
}

处理连接错误

如果你与数据库的连接被断开、终止或出现错误,该怎么办?

发生这种情况时,你不需要实现任何逻辑来重试失败的语句。作为database/sql中连接池的一部分,处理失败的连接是Go内建的。如果执行查询或其他语句,而底层连接出现故障,Go将重新打开一个新连接(或只是从连接池中获取另一个),然后重试,最多10次。

然而,可能会有一些意想不到的后果。当发生错误情况时,可能会重试某些类型的错误。这也可能是特定于驱动程序的。MySQL驱动程序的一个例子是,使用KILL取消不需要的语句(如长时间运行的查询),会导致该语句被重试多达10次。

处理空列值

可为null的列很烦人,会导致很多难看的代码。如果可以的话,应该避免它们。如果没有,那么你需要使用database/sql包中的特殊类型来处理它们,或者定义自己的类型。

database/sql包定义了可为null的布尔值、字符串、整数和浮点值的类型。以下是如何使用它们的一个示例:

for rows.Next() {
	var s sql.NullString
	err := rows.Scan(&s)
	// 检查err
	if s.Valid {
	   // 使用s.String
	} else {
	   // 是NULL值
	}
}

可为null的类型的限制,以及避免可为null列的原因:

1 没有sql.NullUint64或其他sql.NullYourFavoriteType类型。你需要为此定义自己的。

2 使用可为null的类型是一种奇技淫巧,而且不能经得起未来的考验。如果你认为某个东西不会为空,但你错了,你的程序就会崩溃,可能很少会在发布之前发现错误。

3 Go的一个好处是为每个变量都有一个有用的默认零值。但这不是可为null的类型的工作方式。

如果需要定义自己的类型来处理NULL,可以复制sql.NullString的设计来实现这一点。

如果你无法避免在数据库中使用NULL值,那么大多数数据库系统都支持另一种方法,即使用COALESCE()函数。你可以在不引入无数sql.Null*类型的情况下编写类似于以下的代码:

rows, err := db.Query(`
	SELECT
		name,
		COALESCE(other_field, '') as otherField
	WHERE id = ?
`, 42)

for rows.Next() {
	err := rows.Scan(&name, &otherField)
	// ..
	// 如果`other_field`列的值为NULL, 最终存入`otherField`变量的值就是空字符串。这种方式也以同样的逻辑处理其他数据类型。
}

处理未知列数

函数Scan()要求你传递正确数量的目标变量。如果你不知道查询将返回什么,该怎么办?

如果你不知道查询将返回多少列,可以使用columns()返回一个列名的列表。你可以检查此列表的长度以查看有多少列,还可以将一个具有正确列数的切片传递到Scan()中。例如,MySQL的一些分支执行SHOW PROCESSLIST命令返回不同的列,因此你必须为此做好准备,否则会导致错误。这里有一种方法:

cols, err := rows.Columns()
if err != nil {
	// 处理这个错误
} else {
	dest := []interface{}{ // 标准MySQL的列
		new(uint64), // id
		new(string), // host
		new(string), // user
		new(string), // db
		new(string), // command
		new(uint32), // time
		new(string), // state
		new(string), // info
	}
	if len(cols) == 11 {
		// 如果返回了11列,那么使用的数据库可能是Percona server for MySQL
	} else if len(cols) > 8 {
		// 如果返回的列数大于8,处理这种情况
	}
	err = rows.Scan(dest...)
	// 处理dest里的值
}

如果你不知道列数或它们的类型,那么应该使用sql.RawBytes

cols, err := rows.Columns() // 记得无论如何都要检查err,此处省略,因为与主题无关
vals := make([]interface{}, len(cols))
for i, _ := range cols {
	vals[i] = new(sql.RawBytes)
}
for rows.Next() {
	err = rows.Scan(vals...)
	// Now you can check each element of vals for nil-ness, and you can use type introspection and type assertions to fetch the column into a typed variable.现在,你可以检查vals的每个元素是否为nil,并且可以使用类型内省(type introspection)和类型断言(type assertions)将列获取到特定类型的变量中。
}

连接池

database/sql包中有一个基本的连接池。没有太多的能力来控制或检查它,但以下是一些你可能会觉得有用的事情:

  • 连接池意味着在一个数据库上执行两个连续的语句可能会打开两个连接并分别执行。对于程序员来说,对他们的代码为什么表现不佳感到困惑是很常见的。例如,LOCK TABLES后面跟着INSERT可能会阻塞,因为INSERT位于不包含LOCK TABLES锁表的连接上。
  • 当连接池中没有可用的连接时,database/sql包会在需要时创建新的连接。
  • 默认情况下,对连接的数量没有限制。如果你试图同时做很多事情,你可以创建任意数量的连接。这可能会导致数据库返回诸如“连接过多”之类的错误。
  • 在Go 1.1或更新的版本中,可以使用db.SetMaxIdleConns(N)来限制连接池中的空闲连接数。不过,这并没有限制连接池的大小(size)。
  • 在Go 1.2.1或更新的版本中,可以使用db.SetMaxOpenConn(N)来限制到数据库的打开连接的总数。不幸的是,一个死锁Bug修复)使db.SetMaxOpenConn(N)无法在Go 1.2中安全使用。
  • 连接的回收速度相当快。使用db.SetMaxIdleConns(N)设置更多空闲连接的数目可以减少这种流失,并有助于保持连接以供重用。
  • 长时间保持空闲的连接可能会导致问题(例如Microsoft Azure上的MySQL的问题)。如果由于连接空闲时间过长而导致连接超时,请尝试db.SetMaxIdleConns(0)
  • 你还可以通过设置db.SetConnMaxLifetime(duration)来指定连接可以重复使用的最长时间,因为重复使用寿命过长的连接可能会导致网络问题。这会延迟关闭未使用的连接,即可能会延迟关闭过期的连接。

惊讶、反模式和限制

尽管一旦你习惯了database/sql包,数据库编程就很简单,但你可能会对它所支持的其他一些巧妙用法感到惊讶。这在Go的核心库中很常见。

资源耗尽

正如本网站中所提到的,如果你没有按预期使用database/sql,你肯定会给自己带来麻烦,通常是消耗一些资源或阻止它们被有效地重用:

  • 过于频繁地打开和关闭数据库可能会导致资源耗尽。
  • 未能读取完毕所有行数据或rows.Close()执行失败,将占据连接池中的一条连接。
  • 对不返回行数据的语句使用Query()将占据连接池中的一条连接。
  • 没有意识到准备好的语句是如何工作的,可能会导致大量额外的数据库活动。

uint64

这是一个令人惊讶的错误。如果设置了高位(high bit),则不能将大的无符号整数作为参数传递给语句:

_, err := db.Exec("INSERT INTO users(id) VALUES", math.MaxUint64) // Error

这将引发一个错误。如果使用uint64值,请小心,因为它们可能一开始很小,工作时没有错误,但随着时间的推移而增大,并开始抛出错误。

连接状态不匹配

有些事情可能会改变连接状态,这可能会导致问题,原因有两个:

  • 某些连接状态,例如是否处于事务中,应该通过Go类型来处理。
  • 你可能会假设你的查询在某一个连接上运行,而事实并非如此。

例如,用USE语句设置当前数据库是许多人通常要做的事情。但在Go中,这么做只会影响当前运行它的这条数据库连接。除非在事务中(译者注:或者使用专用连接),否则你认为在该连接上执行的其他语句,实际上可能会在从连接池中获得的其他连接上运行,所以可能无法应用这些语句的更改。

此外,在你更改连接后,它将返回到池中,并可能污染其他代码的状态。这也是为什么永远不应该直接将BEGINCOMMIT语句作为SQL命令发出的原因之一。

数据库的特定语法

database/sql包的API提供了面向行的数据库的抽象,但特定的数据库和驱动程序可能在行为和/或语法上有所不同,例如准备好的语句的占位符语法

多个结果集

Go驱动程序无论如何都不支持来自单个查询返回的多个结果集,而且似乎没有任何计划这样做,尽管有一个支持批量复制等批量操作的功能请求

这意味着,返回多个结果集的存储过程将无法正常工作。

调用存储过程

调用存储过程的语法是特定于驱动程序的,但在MySQL驱动程序中,目前无法做到这一点。你似乎可以通过执行以下操作来调用一个返回单个结果集的简单存储过程:

err := db.QueryRow("CALL mydb.myprocedure").Scan(&result) // Error

实际上它不能按预期执行,反而会报错:”Error 1312: PROCEDURE mydb.myprocedure can’t return a result set in the given context.”。这是因为MySQL希望连接被设置为多语句模式(multi-statement mode),即使只是返回单个结果,而驱动程序目前不会这样做(尽管请参阅此议题)。

支持多语句

database/sql包目前没有明确支持多语句,这意味着它的行为依赖于后端实现:

_, err := db.Exec("DELETE FROM tbl1; DELETE FROM tbl2") // 出错或不能返回预期结果

database/sql包目前没有明确支持多语句,这意味着它的行为依赖于后端实现:

_, err := db.Exec("DELETE FROM tbl1; DELETE FROM tbl2") // 出错或不能返回预期结果

服务器可以按照自己的意愿对此进行解释,包括返回一个错误、只执行第一条语句或同时执行这两条语句。

类似地,没有办法在事务中批处理SQL语句。事务中的每条语句都必须串行执行,并且必须扫描或关闭结果中的资源,例如RowRows,以便底层连接可以供下一条语句使用。这与非事务的普通行为不同。在普通行为下,完全可以执行查询,在Rows上循环遍历,并在循环中对数据库进行另一次查询(这将在一条新连接上发生):

rows, err := db.Query("select * from tbl1") // 使用数据库连接1
for rows.Next() {
	err = rows.Scan(&myvariable)
	// 因为连接1已经在使用,以下代码将不会使用连接1
	db.Query("select * from tbl2 where id = ?", myvariable)
}

但是事务只绑定到一个数据库连接,所以上述代码在事务中是不可能的:

tx, err := db.Begin()
rows, err := tx.Query("select * from tbl1") // 使用绑定到tx的数据库连接
for rows.Next() {
	err = rows.Scan(&myvariable)
	tx.Query("select * from tbl2 where id = ?", myvariable) // ERROR,tx的连接正在忙!
}

不过,Go并不能阻止你去尝试。如果你试图在第一个语句释放其资源并在其自身清理之前执行另一个语句,就可能会导致连接损坏。这也意味着事务中的每条语句与数据库服务器之间的网络通讯,是一组单独的网络往返。

其他相关文章和资源

以下是我们发现有帮助的一些外部信息资源:

我们希望这个网站会有所帮助。如果你有任何改进建议,请到https://github.com/VividCortex/go-database-sql-tutorial发送拉取请求(pull request)或开启一个议题。

访问关系型数据库

本文翻译自《Accessing relational databases》。

目录

支持的数据库管理系统

用于执行查询或更改数据库的函数

事务

取消查询

管理连接池

使用Go,你可以将各种各样的数据库和数据访问方法集成到你的应用程序中。本节中的主题描述了如何使用标准库的database/sql包来访问关系数据库。

有关Go访问数据的入门教程,请参阅教程:访问关系型数据库

Go还支持其他数据访问技术,包括用于对关系型数据库进行更高级别访问的ORM库,以及非关系型的NoSQL数据库。

  • 对象关系映射(Object-relational mapping,ORM)库。虽然database/sql包包括用于较低级别的数据访问逻辑的函数,但你也可以使用Go访问更高抽象级别的数据访问。有关Go的两个流行的对象关系映射(ORM)库的更多信息,请参阅GORM引用该包的地址)和ent引用该包的地址)。
  • NoSQL数据存储。Go社区已经为大多数NoSQL数据存储开发了驱动程序,包括MongoDBCouchbase。你可以搜索pkg.go.dev了解更多信息。

支持的数据库管理系统

Go支持所有最常见的关系型数据库管理系统,包括MySQL、Oracle、Postgres、SQL Server、SQLite等。

你可以在SQLDrivers页面上找到驱动程序的完整列表。

用于执行查询或更改数据库的函数

database/sql包包含专门为你正在执行的数据库操作设计的函数。例如,虽然你可以使用QueryQueryRow来执行查询,但QueryRow是为只期望返回一行数据的情况而设计的,省去了返回仅包括一行数据的sql.Rows的开销。你可以使用Exec函数传入SQL语句(例如INSERTUPDATEDELETE)更改数据库。

更多信息请参考:

事务

通过sql.Tx,可以编写代码在事务中执行数据库操作。在事务中,多个操作可以一起执行并以最终提交(commit)结束,以在一个原子步骤中应用所有更改,或者回滚以所有更改。

有关事务的更多信息,请参阅执行事务

取消查询

当你希望能够取消数据库操作时,例如当客户端的连接关闭或操作运行时间超过你的预期时,你可以使用context.Context

对于任何数据库操作,你可以使用database/sql包里的将Context作为参数的函数。使用Context,你可以为操作指定超时时间或截止时间。你还可以使用Context通过应用程序将取消请求传播到执行SQL语句的函数,确保资源在不再需要时得到释放。 有关更多信息,请参阅取消正在进行中的数据库操作。

管理连接池

当你使用sql.DB数据库句柄时,你正在使用一个内置的连接池,该连接池根据你的代码创建和处理数据库连接。通过sql.DB的句柄是使用Go进行数据库访问的最常见方式。有关详细信息,请参阅打开一个数据库句柄

database/sql包为你管理连接池。但是,对于更高级的需求,你可以设置连接池属性,详见设置连接池属性

对于那些需要单个专用数据库连接的操作,database/sql包提供了sql.Conn。当使用sql.Tx的事务不是一个好选择时,Conn尤其有用。

例如,你的代码可能需要:

  • 通过DDL更改数据库模式,包括自身就带事务语义的逻辑。将sql包的事务函数与SQL事务语句混合是一种不良做法,如执行事务中所述。
  • 执行创建临时表等查询锁定(译者注:期间不允许执行查询语句)的操作。

有关更多信息,请参阅使用专用连接

取消正在进行中的数据库操作

本文翻译自《Canceling in-progress operations》。 你可以使用Go的context.Context管理正在进行中的操作。Context是一个标准的Go类型,可用来报告它所代表的整体操作是否已被取消并且不再需要。通过在你的应用程序中跨函数或服务传递context.Context,可以提前停止工作并返回一个错误。有关Context的更多信息,请参阅Go并发模式:Context

例如,你可能希望:

  • 结束长时间运行的操作,包括耗时过长的数据库操作。
  • 传播来自其他地方的“取消继续工作”请求,例如当客户端关闭连接时。

Go的许多API都采用Context参数,使你更容易在整个应用程序中使用Context

超时后取消正在进行中的数据库操作

你可以使用Context来设置超时时间或截止时间,在此之后操作将被取消。要派生具有超时时间或截止时间的Context,请调用Context.WithTimeoutContext.WithDeadline函数。

以下示例中的代码派生一个附带超时时间的Context,并将其传递到sql.DBQueryContext方法中。

func QueryWithTimeout(ctx context.Context) {
    // 创建一个附带超时时间的Context。
    queryCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()

    // 传入这个附带超时时间的Context和一个查询语句。
    rows, err := db.QueryContext(queryCtx, "SELECT * FROM album")
    if err != nil {
        log.Fatal(err)
    }
    defer rows.Close()

    // 处理返回的行数据。
}

当一个context从外部的context派生而来时,由于queryCtx在本例中是从函数参数ctx派生的,如果外部的context被取消,那么派生的context也会自动取消。例如,在HTTP服务器中,HTTP.Request.Context方法返回与请求相关联的context。如果HTTP客户端断开连接或取消HTTP请求(可能使用HTTP/2),则该context将被取消。如果整个HTTP请求被取消,或者查询耗时超过五秒,那么将HTTP请求的context传递给上面的QueryWithTimeout函数将导致数据库查询操作提前结束。

注意:当你在创建带有超时时间或截止时间的一个新context时,总是应该defer对这个context的cancel方法的调用。当包含这个新context的函数退出时,就能释放这个新context所持有的资源。它还将取消queryCtx,当这个新context的cancel方法返回时,就不应该再使用queryCtx

查询数据

本文翻译自《Querying for data》。

目录

查询单行

查询多行

处理可null列的值

从列中获取数据

处理多个结果集

执行返回行数据的SQL语句时,请使用database/sql包中提供的Query方法之一。其中每一个都返回一行或多行,你可以使用Scan方法将其数据复制到变量中。例如,你可以使用这些方法来执行SELECT语句。

执行不返回行数据的语句,可以改用ExecExecContext方法。有关详细信息,请参阅执行不返回数据的SQL语句

database/sql包提供了两种执行查询并返回结果的方法:

  • 查询单行QueryRow方法从数据库中最多返回一行作为一个Row结构体。有关详细信息,请参阅查询单行
  • 查询多行Query方法返回所有匹配的行作为代码可以循环的Rows结构体。有关详细信息,请参阅查询多行

如果你的代码将重复执行相同的SQL语句,请考虑使用准备好的语句(译者注:预处理语句)。有关详细信息,请参阅使用准备好的语句

注意:不要使用fmt.Sprintf等字符串拼接函数来组装SQL语句!否则你可能会引入SQL注入风险。有关更多信息,请参阅避免SQL注入风险

查询单行

QueryRow函数最多检索单个数据库行,例如通过唯一ID查找数据时。如果查询返回多行,则Scan方法会丢弃除第一行以外的所有行。

QueryRowContext函数的工作方式类似于QueryRow,但带有context.Context参数。有关更多信息,请参阅取消正在进行的数据库操作

以下示例使用查询来确定是否有足够的库存来支持购买。如果足够,则SQL语句返回true,否则返回falseRow.Scan方法通过指针将布尔返回值复制到enough变量中。

func canPurchase(id int, quantity int) (bool, error) {
    var enough bool
    // 查询基于单行的值。
    if err := db.QueryRow("SELECT (quantity >= ?) from album where id = ?",
        quantity, id).Scan(&enough); err != nil {
        if err == sql.ErrNoRows {
            return false, fmt.Errorf("canPurchase %d: unknown album", id)
        }
        return false, fmt.Errorf("canPurchase %d: %v", id)
    }
    return enough, nil
}

注意:预处理语句中的参数占位符因所使用的DBMS和驱动程序而异。例如,Postgres的pq驱动程序需要像$1这样的占位符,而MySQL驱动程序使用?号。

处理错误

QueryRow函数本身不返回错误。相反,Scan方法会报告查找和扫描中的任何错误。当查询未找到任何行时,它返回sql.ErrNoRows错误。

以下列出返回单行的函数

DB.QueryRow

DB.QueryRowContext

单独运行单行查询。

Tx.QueryRow

Tx.QueryRowContext

在较大的事务中运行单行查询。有关更多信息,请参阅执行事务

Stmt.QueryRow

Stmt.QueryRowContext

使用一个准备好的语句运行单行查询。有关更多信息,请参阅使用准备好的语句。

Conn.QueryRowContext

与专用的数据库连接一起使用。有关更多信息,请参阅管理数据库连接

查询多行

你可以使用QueryQueryContext函数查询多行,它们返回表示查询结果的Rows结构体。你的代码使用rows.Next方法对返回的行进行迭代。每次迭代都调用Scan方法将列值复制到变量中。

QueryContext函数的工作方式与Query类似,但带有context.Context参数。有关更多信息,请参阅取消正在进行中的数据库操作

以下示例执行查询以返回指定艺术家(artist)的专辑(album)信息。albumsql.Rows中返回。以下代码使用Rows.Scan函数将列值复制到由指针指出的变量中。

func albumsByArtist(artist string) ([]Album, error) {
    rows, err := db.Query("SELECT * FROM album WHERE artist = ?", artist)
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    // 一个切片用来存储返回的行数据。
    var albums []Album

    // 循环遍历行数据,使用Scan方法把每一行的各列的值,赋值给一个结构体变量的对应字段。Loop through rows, using Scan to assign column data to struct fields.
    for rows.Next() {
        var alb Album
        if err := rows.Scan(&alb.ID, &alb.Title, &alb.Artist,
            &alb.Price, &alb.Quantity); err != nil {
            return albums, err
        }
        albums = append(albums, album)
    }
    if err = rows.Err(); err != nil {
        return albums, err
    }
    return albums, nil
}

请注意对rows.Close函数的延迟调用。无论函数如何返回,这都会释放sql.Rows占有的所有资源。虽然循环遍历完毕所有的行也会隐式关闭它,但最好使用defer来确保无论如何都能关闭。

注意:预处理语句中的参数占位符因所使用的DBMS和驱动程序而异。例如,Postgres的pq驱动程序需要像$1这样的占位符,而MySQL驱动程序使用?号。

处理错误

请务必在遍历查询结果后检查sql.Rows中是否有错误发生。如果查询失败,这就是你的代码发现错误的方式。

以下列出返回多行数据的函数

DB.Query

DB.QueryContext

单独运行查询。

Tx.Query

Tx.QueryContext

在较大的事务中执行查询。有关更多信息,请参阅执行事务

Stmt.Query

Stmt.QueryContext

使用一个准备好的语句运行查询。有关更多信息,请参阅使用准备好的语句。

Conn.QueryContext

与专用的数据库连接一起使用。有关更多信息,请参阅管理数据库连接

处理可null列的值

当列的值可能为null时,database/sql包提供了几种特殊类型,可以用作Scan函数的参数。每个特殊类型都包括一个Valid字段,该字段报告列值是否为非null,如果为非null则该类型有另一个字段包含该列值。

以下示例中的代码用于查询客户名称。如果名称值为null,则代码将用另一个值替换它,以在应用程序中使用。

var s sql.NullString
err := db.QueryRow("SELECT name FROM customer WHERE id = ?", id).Scan(&s)
if err != nil {
    log.Fatal(err)
}

name := "Valued Customer"
// 如果客户名称不为null(合法,s.Valid的值为true),那么赋值给name变量
if s.Valid {
    name = s.String
}

请参阅sql包关于每个特殊类型的更多信息:

从列中获取数据

在查询返回的行上循环遍历时,可以使用Scan函数将行的列值复制到Go值中,例如rows.Scan的参考文档中所述。

所有驱动程序都支持一组基本的数据类型转换,例如将SQL的INT转换为Go的INT。一些驱动程序扩展了这组转换;有关详细信息,请参阅每个驱动程序的文档。

正如你所料,Scan函数将把列类型转换为类似的Go类型。例如,Scan将把SQL的CHARVARCHARTEXT转换为Go字符串。但是,Scan还可以把列值转换为另一个非常适合的Go类型。例如,如果列是始终包含数字的VARCHAR,则可以指定数字Go类型(例如int)来接收值,Scan将使用strconv.Atoi函数进行转换。

有关Scan函数进行类型转换的更多详细信息,请参阅Rows.Scan参考文档。

处理多个结果集

当数据库操作可能返回多个结果集时,你可以使用Rows.NextResultSet函数来检索这些结果集。例如,当你发送分别查询多个表的一个SQL语句,并为其中每个表都返回一个结果集时,这可能很有用。

Rows.NextResultSet函数会准备下一个结果集,以便调用Rows.Next函数从下一个集合中检索第一行。它返回一个布尔值,指示是否存在下一个结果集。

以下示例中的代码使用DB.Query函数来执行两条Select语句。第一个结果集来自第一个Select查询,检索相簿album表中的所有行。下一个结果集来自第二个Select查询,从歌曲表song中检索行。

rows, err := db.Query("SELECT * from album; SELECT * from song;")
if err != nil {
    log.Fatal(err)
}
defer rows.Close()

// 循环遍历第一个结果集。
for rows.Next() {
    // 处理结果集…
}

// 前进到下一个结果集。
rows.NextResultSet()

// 循环遍历第二个结果集。
for rows.Next() {
    // 处理结果集…
}

// 检查在处理所有结果集的过程中,是否有错误发生。
if err := rows.Err(); err != nil {
    log.Fatal(err)
}

避免SQL注入风险

本文翻译自《Avoiding SQL injection risk》。

你可以通过提供SQL参数值作为sql包里函数的参数来避免SQL注入风险。sql包中的许多函数为SQL语句(包括准备语句)的参数提供参数值。

以下示例中的代码使用?作为SQL语句的参数的占位符,由Query函数的id参数提供值:

// 使用参数执行一个SQL语句的正确格式
rows, err := db.Query("SELECT * FROM user WHERE id = ?", id)

执行数据库操作的sql包的函数根据你提供的参数创建SQL准备语句。在运行时,sql包将SQL语句转换为准备语句并将其与参数分开发送。

注意:参数占位符因你使用的DBMS和驱动程序而异。例如,Postgres的pq驱动程序使用$1形式的占位符形式,而不是?号。

你可能想使用fmt包中的函数将SQL语句组装为包含参数值的字符串——如下所示:

// 有安全风险!
rows, err := db.Query(fmt.Sprintf("SELECT * FROM user WHERE id = %s", id))

这不安全!执行此操作时,Go会组装整个SQL语句,在将完整语句发送到DBMS之前用参数值替换%s格式动词。这会带来SQL注入风险,因为代码的调用者可能会发送意外的SQL片段作为id参数。该片段可能会以不可预测的方式执行SQL语句,这对你的应用程序是危险的。

例如,通过传递一个特定的%s值,你可能会得到类似于以下内容的SQL语句,它可能会返回数据库中的所有用户记录:

SELECT * FROM user WHERE id = 1 OR 1=1;

使用准备好的语句(prepared statement)

本文翻译自《Using prepared statements》。

目录

什么是准备好的语句?

如何使用准备好的语句

准备好的语句的行为

你可以定义准备好的语句以供重复使用。这可以避免每次代码执行数据库操作时重新创建语句的额外开销,从而帮助你的代码运行得更快一些。

注意:准备好的语句中的参数占位符因你使用的DBMS和驱动程序而异。例如Postgres的pq驱动程序需要$1形式的占位符,而不是?号占位符。

什么是准备好的语句?

准备好的语句是由DBMS解析和保存的SQL,通常包含占位符但没有实际参数值。稍后,可以使用一组参数值来执行该语句。(译者注:准备好的语句,准备语句,也叫预处理语句。)

如何使用准备好的语句

当你希望重复执行同一条SQL时,可以使用一个sql.Stmt结构体预先准备好SQL语句,然后按需执行。

以下示例创建一个准备好的语句,从数据库中选择一个特定的专辑(album)的信息。DB.Prepare函数返回一个sql.Stmt结构体,代表给定SQL文本的准备语句。你可以将SQL语句的参数值传递给Stmt.ExecStmt.QueryRowStmt.Query方法以运行该语句。

// AlbumByID函数返回一个特定的专辑(album)的信息。
func AlbumByID(id int) (Album, error) {
    // 定义准备好的语句。你通常会在别处定义该语句并将其保存以供在诸如本函数之类的函数中使用。
    stmt, err := db.Prepare("SELECT * FROM album WHERE id = ?")
    if err != nil {
        log.Fatal(err)
    }

    var album Album

    // 执行准备好的语句,为占位符为?的参数传入一个id值。
    err := stmt.QueryRow(id).Scan(&album.ID, &album.Title, &album.Artist, &album.Price, &album.Quantity)
    if err != nil {
        if err == sql.ErrNoRows {
            // 处理没有一行数据返回的情况。
        }
        return album, err
    }
    return album, nil
}

准备好的语句的行为

准备好的sql.Stmt提供常用的ExecQueryRowQuery方法来调用语句。有关使用这些方法的更多信息,请参阅查询数据执行不返回数据的SQL语句

但是,由于sql.Stmt已经代表了一条预设的SQL语句,所以它的ExecQueryRowQuery方法只需占位符对应的参数值,省略了SQL文本。

你可以用不同的方式定义一个新的sql.Stmt,这取决于你将如何使用它。

  • DB.PrepareDB.PrepareContext方法创建一个准备好的语句,它可以在事务之外单独执行,就像DB.ExecDB.Query方法一样。
  • Tx.PrepareTx.PrepareContextTx.StmtTx.StmtContext方法创建用于特定事务的准备好的语句。PreparePrepareContext方法使用SQL文本来定义语句。StmtStmtContext方法使用DB.PrepareDB.PrepareContext方法的结果。也就是说,它们将一个非事务的sql.Stmt转换为一个事务的sql.Stmt。
  • Conn.PrepareContext方法从sql.Conn方法创建准备好的语句,表示专用的连接。

当你的代码结束时,请确保调用stmt.Close方法。这将释放可能与其关联的任何数据库资源(例如底层连接)。对于函数中仅作为局部变量的语句,defer stmt.Close()就足够了。

用于创建准备好的语句的函数

DB.Prepare
DB.PrepareContext

准备一个单独执行的语句,或者使用Tx.Stmt方法把它转换为事务中的准备语句。

Tx.Prepare
Tx.PrepareContext
Tx.Stmt
Tx.StmtContext

准备用于特定事务的语句。有关更多信息,请参阅执行事务

Conn.PrepareContext

与专用的连接一起使用。有关更多信息,请参阅管理数据库连接

管理数据库连接

本文翻译自《Managing connections》。

目录

设置连接池属性

使用专用连接

对于绝大多数程序,你都无需调整sql.DB连接池的默认值。但对于某些高级程序,你可能需要调整连接池参数或显式地处理连接。本文教你如何操作。

sql.DB数据库句柄对于多个goroutine并发使用是安全的(这意味着该句柄是其他编程语言可能称之为“线程安全(thread-safe)”的)。其他一些数据库访问库基于每次只能进行一个操作的数据库连接方式。为了弥补这一差距,每个sql.DB都管理一个连接到底层数据库的活动连接池,根据需要可以在Go程序中创建新的并行的连接。

连接池适用于大多数的数据库访问需求。当你调用sql.DBQueryExec方法时,sql.DB会从池中检索可用的连接,或者在需要时创建一个连接。当不再需要连接时,它会将连接返回到池。这种方式能支持大规模并行访问数据库。

设置连接池属性

你可以设置属性来指导sql包如何管理连接池。要获取有关这些属性效果的统计信息,请使用DB.Stats方法。

设置能打开的连接的最大数量

DB.SetMaxOpenConns方法对打开的连接数量进行了限制。超过此限制,新的数据库操作将等待现有操作完成,然后sql.DB将创建另一个连接。默认情况下,当需要连接时,sql.DB会在所有现有连接都在使用时创建一个新连接。

请记住,设置连接数限制会让Go程序连接数据库时使用类似于获取锁或信号量的机制,从而导致程序在等待新的数据库连接时可能会被阻塞等待。

设置空闲连接的最大数量

DB.SetMaxIdleConns方法更改sql.DB维护的最大空闲连接数。

当SQL操作在给定的数据库连接上完成时,通常不会立即关闭这个连接:应用程序可能很快再次需要使用该连接,保持打开的连接可以避免在下一次操作时重新连接到数据库。默认情况下,sql.DB在任何给定时刻保持两个空闲连接。提高最大空闲连接数可以避免在大规模并行程序中频繁重新建立连接。

设置连接可以空闲的最大时间

DB.SetConnMaxIdleTime方法设置连接在关闭之前可以处于空闲状态的最大时间长度。这会导致sql.DB关闭空闲时间超过给定持续时间的连接。

默认情况下,当一个空闲连接被添加到连接池时,它会一直保持在那里,直到再次需要它为止。使用DB.SetMaxIdleConns函数来增加并行活动突发期间允许的空闲连接数,也可以使用DB.SetConnMaxIdleTime函数来安排稍后在系统安静时释放这些连接。

设置连接的最大生存时间

使用DB.SetConnMaxLifetime函数设置连接在关闭之前可以保持打开的最大时间长度。

默认情况下,连接可以在任意长的时间内使用和重用,但要遵守上述限制。在某些系统中,例如使用负载均衡数据库服务器的系统,确保应用程序在不重新连接的情况下永远不会使用特定连接太长时间,这可能会很有用。

使用专用连接

当某个数据库可能为在一个特殊连接上执行的一系列操作指定隐式含义时,database/sql包提供可以使用的函数。

最常见的例子是事务,通常以BEGIN命令开始,以COMMITROLLBACK命令结束,并在这个事务的连接上发出所有命令(包括BEGINCOMMITROLLBACK这几个命令)。对于这个例子,请使用sql包的事务支持。请参阅执行事务

对于其他必须在同一个连接上执行一系列单独操作的例子,sql包提供了专用连接。DB.Conn获得一个专用连接sql.Connsql.ConnBeginTxExecContextPingContextPrepareContextQueryContextQueryRowContext方法,它们的行为与DB上的等效方法类似,但只使用专用的连接。专用连接使用完毕后,你的代码必须使用Conn.Close将其释放。

执行不返回行数据的SQL语句

本文翻译自《Executing SQL statements that don’t return data》。

执行不返回数据的数据库操作时,请使用database/sql包中的ExecExecContext方法。以这种方式执行的SQL语句包括INSERTDELETEUPDATE

当查询可能返回行数据时,请改QueryQueryContext方法。有关详细信息,请参阅查询一个数据库

ExecContext方法的工作原理与Exec方法相同,但有一个额外的context.Context参数,具体作用参见“取消正在进行中的数据库操作”。

以下示例中的代码使用DB.Exec执行一条语句,将一条唱片的信息添加到album表中。

func AddAlbum(alb Album) (int64, error) {
    result, err := db.Exec("INSERT INTO album (title, artist) VALUES (?, ?)", alb.Title, alb.Artist)
    if err != nil {
        return 0, fmt.Errorf("AddAlbum: %v", err)
    }

    // 获取新插入的这条记录的ID
    id, err := result.LastInsertId()
    if err != nil {
        return 0, fmt.Errorf("AddAlbum: %v", err)
    }
    // 返回这条新记录的ID
    return id, nil
}

DB.Exec返回值:一个sql.Result和一个指示是否有错误发生的变量。当错误为nil时,你可以使用Result来获取最后插入项的ID(如示例中所示)或获取数据表中受操作影响的行数。

注意:预处理语句中的参数占位符因所使用的DBMS和驱动程序而异。例如,Postgres的pq驱动程序需要像$1这样的占位符,而不是?

如果你的代码将重复执行相同的SQL语句,请考虑使用SQL.Stmt从SQL语句中创建一个可重复使用的准备语句。有关详细信息,请参阅使用准备好的语句

注意:不要使用fmt.Sprintf等字符串格式函数来组装SQL语句!因为可能会有SQL注入风险。有关更多信息,请参阅避免SQL注入风险

以下列出用于执行不返回行数据的SQL语句的函数

DB.Exec

DB.ExecContext

孤立地执行单个SQL语句。

Tx.Exec

Tx.ExecContext

在较大的事务中执行SQL语句。有关详细信息,请参阅执行事务

Stmt.Exec

Stmt.ExecContext

执行已准备好的SQL语句。有关详细信息,请参阅使用准备好的语句

Conn.ExecContext

用于专用的连接。有关详细信息,请参阅管理连接

Go语言MySQL驱动程序GitHub README.md中文翻译

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

Go-MySQL-Driver 用于Go语言database/sql包的MySQL驱动。

特性

要求

安装

用法

       DSN(Data Source Name,数据源名称)

              密码

              协议

              地址

              参数

              实例

       连接池和超时

       支持context.Context

       支持ColumnType

       支持LOAD DATA LOCAL INFILE

       支持time.Time

       支持Unicode

测试/开发

License

特性

  • 轻量又快速
  • 纯Go语言实现,并非是对C语言的封装
  • 通过TCP/IPv4、TCP/IPv6、Unix域套接字或自定义协议进行连接
  • 自动处理断开的连接
  • 自动连接池(通过database/sql包)
  • 支持大于16MB的查询字符串
  • 完整的sql.RawBytes支持
  • 智能地处理预处理语句中的LONG DATA
  • 通过文件白名单安全地支持LOAD DATA LOCAL INFILE,也支持io.Reader
  • 可选的time.Time解析
  • 可选的占位符插值

要求

  • Go 1.13或更高版本。我们的目标是支持Go的3个最新版本。
  • MySQL (4.1+)、MariaDB、Percona Server、Google CloudSQL或Sphinx (2.2.3+)

安装

使用shell中的go工具将包简单地安装到你的$GOPATH中:

$ go get -u github.com/go-sql-driver/mysql

确保Git安装在你的机器上和系统的PATH中。

用法

Go语言MySQL驱动是Go的database/sql/driver包的接口的实现。你只需要导入这个驱动,然后就可以使用完整的database/sql包的API。

使用mysql作为driverName,使用有效的DSN作为dataSourceName

import (
	"database/sql"
	"time"

	_ "github.com/go-sql-driver/mysql"
)

// ...

db, err := sql.Open("mysql", "user:password@/dbname")
if err != nil {
	panic(err)
}
// 参见“重要设置”小节。
db.SetConnMaxLifetime(time.Minute * 3)
db.SetMaxOpenConns(10)
db.SetMaxIdleConns(10)

案例见我们的wiki

重要设置

db.SetConnMaxLifetime()确保连接在MySQL服务器、操作系统或其他中间件关闭之前由驱动程序安全地关闭。由于某些中间件会在5分钟后关闭空闲连接,因此我们建议把超时时间设置为短于5分钟的时间。此设置也有助于负载平衡和更改系统变量。

db.SetMaxOpenConns()强烈建议使用该函数来限制应用程序使用的连接数。没有建议的限制数,因为它取决于具体应用程序和MySQL服务器。

db.SetMaxIdleConns()建议设置为与db.SetMaxOpenConns()的相同。当它小于SetMaxOpenConns()的设置时,连接打开和关闭的频率可能比你预期的要高得多。空闲连接可以通过db.SetConnMaxLifetime()设置超时时间。如果你想更快地关闭空闲连接,从Go 1.15开始,你可以使用db.SetConnMaxIdleTime()函数来设置。

DSN(Data Source Name,数据源名称)

数据源名称具有一个通用格式,例如PEAR DB所使用的,但没有类型前缀(用方括号标记可选部分):

[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...&paramN=valueN]

完整形式的DSN:

username:password@protocol(address)/dbname?param=value

除了数据库名称之外,所有值都是可选的。因此,最小DSN为:

/dbname

如果不想预先选择一个数据库实例,请将dbname保留为空:

/

这与空DSN字符串的效果相同。

或者,我们也可以使用Config.FormatDSN通过填充一个结构体来创建DSN字符串。

密码

密码可以由任何字符组成,无需转义。

协议

有关可用网络的更多信息,请参阅net.Dial。一般来说,如果可以,你应该使用Unix域套接字,否则应该使用TCP,以获得最佳性能。

地址

对于TCP和UDP网络,地址的格式为host[:port]。如果省略port,将使用默认端口号。如果主机使用IPv6地址,则必须将其括在方括号中。函数net.JoinHostPortnet.SplitHostPort以这种形式处理地址。

对于Unix域套接字,地址是MySQL服务的套接字的绝对路径,例如:

/var/run/mysqld/mysqld.sock

/tmp/mysql.sock

参数

参数区分大小写!

请注意,trueTRUETrue1中的任何一个表示布尔值“真”。毫不奇怪,布尔值“假”可以指定为以下任意值:falseFALSEFalse0

allowAllFiles
Type:           bool
Valid Values:   true, false
Default:        false

allowAllFiles=true禁用LOAD DATA LOCAL INFILE的文件白名单,并允许所有文件。可能不安全

allowCleartextPasswords
Type:           bool
Valid Values:   true, false
Default:        false

allowCleartextPasswords=true在帐户需要时允许使用明文(cleartext)客户端插件,例如使用PAM身份验证插件定义的插件。以明文形式发送密码在某些配置中可能是一个安全问题。如果密码有可能被拦截,为避免出现问题,客户端应使用保护密码的方法连接到MySQL服务器,包括使用TLS/SSL、IPsec或专用网络。

allowFallbackToPlaintext
Type:           bool
Valid Values:   true, false
Default:        false

allowFallbackToPlaintext=true的作用类似于使用--ssl-mode=PREFERRED选项的MySQL客户端,如用于连接到服务器的命令选项中所述

allowNativePasswords
Type:           bool
Valid Values:   true, false
Default:        true

allowNativePasswords=false禁止使用MySQL本机密码(native password)方式。

allowOldPasswords
Type:           bool
Valid Values:   true, false
Default:        false

allowOldPasswords=true允许使用不安全的旧密码(old password)方式。应该避免使用这种方式,但在某些情况下是必要的。另请参阅old_passwords维基页面

charset
Type:           string
Valid Values:   <name>
Default:        none

设置用于客户端-服务器交互的字符集(“SET NAMES <value>“)。如果设置了多个字符集(用逗号分隔),则在设置当前字符集失败时继续尝试设置后续的字符集。例如, (charset=utf8mb4,utf8)设置utf8mb4(在MySQL 5.5.3中引入),如果是旧服务器就回退到utf8

不鼓励使用charset参数,因为它会向服务器发出额外的查询。除非你需要回退行为,否则请改用collation参数。

checkConnLiveness
Type:           bool
Valid Values:   true, false
Default:        true

在支持的平台上,Go从连接池中检索连接,在使用该连接之前会检查其是否活动。如果不活动,则将相应的连接标记为坏连接,并尝试使用另一个连接。checkConnLiveness=false不检查连接是否活动。

collation
Type:           string
Valid Values:   <name>
Default:        utf8mb4_general_ci

设置连接时用于客户端-服务器交互的字符排序规则。与charset不同,collation不会发出额外的查询。如果指定的排序规则在目标服务器上不可用,则连接将失败。

可以使用SHOW COLLATION检索服务器的有效字符集列表。

MySQL 5.5支持默认排序规则(utf8mb4_general_ci)。对于较旧的MySQL,应该使用较旧的排序规则(例如utf8_general_ci)。 不能使用字符集”ucs2″、”utf16″、”utf16le”和”utf32″的排序规则(参考)。

clientFoundRows
Type:           bool
Valid Values:   true, false
Default:        false

clientFoundRows=true会导致UPDATE语句返回匹配的行数,而不是更改的行数。

columnsWithAlias
Type:           bool
Valid Values:   true, false
Default:        false

columnsWithAlias=true时,调用sql.Rows.Columns()将返回表别名和用点分隔的列名。例如:

SELECT u.id FROM users as u

如果columnsWithAlias=true,将返回u.id而不仅仅是id

interpolateParams
Type:           bool
Valid Values:   true, false
Default:        false

如果interpolateParams=true,则调用db.Query()db.Exec()时的占位符(?)将插入到具有给定参数的单个查询字符串中。这减少了往返次数,因为当interpolateParams=false时,驱动程序必须准备好一条语句,使用给定的参数执行它,然后再次关闭该语句。

这不能与多字节编码BIG5CP932GB2312GBKSJIS一起使用。这些都被拒绝,因为它们可能会引入SQL注入漏洞

loc
Type:           string
Valid Values:   <escaped name>
Default:        UTC

设置time.Time值的时区(当使用parseTime=true时)。如果时区设置为Local,那么设置为系统的时区。有关详细信息,请参阅time.LoadLocation

注意,这将设置time.time值的时区,但不会更改MySQL的time_zone设置。为此,请参见time_zone系统变量,该变量也可以设置为DSN的参数。

请记住,参数值必须是url.QueryEscape编码的。或者,你可以手动将/替换为%2F。例如,US/Pacific(美国/太平洋时区)应该被设置为loc=US%2FPacific

maxAllowedPacket
Type:          decimal number
Default:       4194304

允许的最大数据包的大小(以字节为单位)。默认值为4MiB,应进行调整以匹配服务器设置。maxAllowedPacket=0可用于在每次连接时从服务器自动获取max_allowed_packet变量。

multiStatements
Type:           bool
Valid Values:   true, false
Default:        false

是否允许在一个查询中使用多条语句。虽然这允许批量查询,但也大大增加了被SQL注入的风险。只返回第一个查询语句的结果,所有其他结果都被默默丢弃。当使用multiStatements时,?参数只能在第一条语句中使用。

parseTime
Type:           bool
Valid Values:   true, false
Default:        false

parseTime=trueDATEDATETIME值的输出类型更改为time.Time,而不是[]byte/string类型的日期或日期时间,例如0000-00-00 00:00:00将转换为time.Time的零值。

readTimeout
Type:           duration
Default:        0

I/O读取的超时时间。该值必须是带有单位后缀的十进制数字(”ms”、”s”、”m”、”h”),例如”30s”、”0.5m”或”1m30s”。

rejectReadOnly
Type:           bool
Valid Values:   true, false
Default:        false

rejectReadOnly=true会导致数据库驱动程序拒绝只读连接。这是针对自动故障切换期间可能出现的争用(race condition)情况,即故障切换后mysql客户端将连接到某个只读副本。

请注意,这应该是一种相当罕见的情况,因为自动故障转移通常发生在主服务器关闭时,并且竞态条件(race condition)不会发生,除非它在故障转移开始后立即恢复在线。另一方面,发生这种情况时,MySQL应用程序可能会卡在只读连接上,直到重新启动。然而,这种故障相当容易复现,例如在AWS Aurora的MySQL兼容集群上手动造成故障转移。

如果你不依赖只读事务来拒绝不应该发生的写入操作,那么在某些MySQL服务提供商(例如AWS Aurora)上设置此选项,对于故障切换来说更安全。

请注意,read-only服务器可能会返回1290号错误,此选项将导致重试该错误。但是,存在其他一些情况,也使用相同的1290错误号。因此启用此选项时,应确保应用程序在read-only模式之外,都不会导致1290号错误。

serverPubKey
Type:           string
Valid Values:   <name>
Default:        none

可以使用mysql.RegisterServerPubKey注册服务器的公钥,然后可以在DSN中通过参数使用该公钥。公钥用于传输加密数据,例如用于身份验证。如果服务器的公钥是已知的,则应手动设置它,以避免每次需要它时都从服务器向客户端传输,代价昂贵且可能不安全。

timeout
Type:           duration
Default:        OS default

建立连接时的超时时间,又称拨号超时。该值必须是带单位后缀(”ms”、”s”、”m”、”h”)的十进制数,例如”30s”、”0.5m”或”1m30s”。

tls
Type:           bool / string
Valid Values:   true, false, skip-verify, preferred, <name>
Default:        false

tls=true启用与服务器的TLS/SSL加密连接。如果要使用自签名或无效证书(服务器端),那么使用skip-verify参数;或通过preferred参数使用TLS(仅当得到服务器端建议时),这类似于skip-verify,但还允许回退到未加密的连接。skip-verifypreferred都不添加任何可靠的安全性。你可以在向mysql.RegisterTLSConfig注册后使用自定义的TLS配置。

writeTimeout
Type:           duration
Default:        0

I/O写入超时。该值必须是带单位后缀(”ms”、”s”、”m”、”h”)的十进制数,例如”30s”、”0.5m”或”1m30s”。

系统变量

任何其他参数都被解释为系统变量:

  • <boolean_var>=<value>: SET <boolean_var>=<value>
  • <enum_var>=<value>: SET <enum_var>=<value>
  • <string_var>=%27<value>%27: SET <string_var>='<value>'

规则:

  • 字符串变量的值必须用’引起来。
  • 值也必须使用url.QueryEscape编码过(这意味着字符串变量的值必须用%27包裹)。

例如:

  • autocommit=1: SET autocommit=1
  • time_zone=%27Europe%2FParis%27: SET time_zone='Europe/Paris'
  • transaction_isolation=%27REPEATABLE-READ%27: SET transaction_isolation='REPEATABLE-READ'

实例

user@unix(/path/to/socket)/dbname

root:pw@unix(/tmp/mysql.sock)/myDatabase?loc=Local

user:password@tcp(localhost:5555)/dbname?tls=skip-verify&autocommit=true

通过设置系统变量sql_mode将警告视为错误:

user:password@/dbname?sql_mode=TRADITIONAL

通过IPv6的TCP:

user:password@tcp([de:ad:be:ef::ca:fe]:80)/dbname?timeout=90s&collation=utf8mb4_unicode_ci

远程主机上的TCP,例如Amazon RDS:

id:password@tcp(your-amazonaws-uri.com:3306)/dbname

应用引擎上的谷歌云SQL:

user:password@unix(/cloudsql/project-id:region-name:instance-name)/dbname

使用本地主机上默认端口(3306)的TCP:

user:password@tcp/dbname?charset=utf8mb4,utf8&sys_var=esc%40ped

使用默认协议(tcp)和主机(localhost:3306):

user:password@/dbname

未预先选择数据库:

user:password@/

连接池和超时

连接池由Go的database/sql包管理。有关如何配置池的大小以及连接在池中停留的时间的详细信息,请参阅database/sql文档中的*DB.SetMaxOpenConns*DB.Set MaxIdleConns*DB.Set-ConnMaxLifetime。每个单独连接的读取、写入和拨号超时分别使用DSN参数readTimeoutwriteTimeouttimeout进行配置。

支持context.Context

此驱动程序支持Go 1.8中引入的ColumnType接口,但ColumnType.Length()除外,该接口目前不受支持。所有无符号的数据库类型都将返回带有UnsignedINTTINYINTSMALLINTBIGINT名称。

支持ColumnType

此驱动程序支持Go 1.8中引入的ColumnType接口,但ColumnType.Length()除外,该接口目前不受支持。所有无符号的数据库类型都将返回带有UnsignedINTTINYINTSMALLINTBIGINT名称。

支持context.Context

在Go 1.8,database/sql包添加了对context.Context的支持。此驱动程序支持查询超时和通过context取消语句的执行。有关详细信息,请参阅database/sql包中的context支持

支持LOAD DATA LOCAL INFILE

对于此功能,你需要直接访问软件包。因此,你必须更改导入路径(无_):

import "github.com/go-sql-driver/mysql"

文件必须通过mysql.RegisterLocalFile(filepath)注册来明确允许加载里面的数据(推荐使用这种做法),或者必须通过使用DSN参数allowAllFiles=true来禁用allowlist检查(这种做法可能不安全!)。

要使用io.Reader,必须使用mysql.RegisterReaderHandler(name, handler)注册处理函数,该函数返回io.Readorio.ReadCloser。然后,Reader可以使用文件路径Reader::<name>。可以为不同的处理程序 handler注册不同的名字name,,当你不再需要它时,使用DeregisterReaderHandler反注册。

有关详细信息,请参阅Go MySQL驱动程序的godoc

支持time.Time

MySQL DATEDATETIME值的默认内部输出类型为[]byte,这允许你将值扫描到程序中的[]bytestringsql.RawBytes变量中。

然而,许多人希望将MySQL的DATEDATETIME值扫描到Go语言的time.Time变量中,这在逻辑上等同于MySQL中的DATEDATETIME值。你可以通过使用DSN参数parseTime=true将内部输出类型从[]byte更改为time.Time。你可以使用DSN参数loc设置默认time.Time的时区位置

注意:截至Go 1.1,这使得time.Time成为唯一可以扫描DATEDATETIME值的变量类型。这会破坏例如sql.RawBytes的支持

支持Unicode

由于1.5版本的Go MySQL驱动程序默认情况下自动使用排序规则utf8mb4_general_ci

可以使用DSN参数collation设置其他排序规则/字符集。

1.0版的驱动程序建议将&charset=utf8(即MySQL命令SET NAMES utf8的别名)添加到DSN中,以启用正确的UTF-8支持。但现在已经没有必要这么做了,如果要设置其他排序规则/字符集的话,应该首选collation参数。

有关MySQL的Unicode支持的更多详细信息参见http://dev.mysql.com/doc/refman/8.0/en/charset-unicode.html。

测试/开发

要运行驱动程序测试,你可能需要调整配置。有关详细信息,请参阅测试的Wiki 页面

Go语言的MySQL驱动程序的功能还不完善。如果你想做出贡献,我们将非常感谢你的帮助,你可以处理未解决的问题或查看拉取请求

有关详细信息,请参阅贡献指南

License

Go语言的MySQL驱动程序在Mozilla公共许可证2.0版(Mozilla Public License Version 2.0)下获得许可。

Mozilla将许可范围总结如下:

MPL:许可证适用于任何包含MPL代码的文件

这意味着:

  • 你可以在私人和商业用途中使用未更改的源代码。
  • 分发时,你必须根据a) MPL 2.0本身或b)兼容的许可证(例如GPL 3.0或Apache License 2.0)发布MPL 2.0许可的任何更改了源代码的文件
  • 只要根据MPL 2.0许可的文件未更改,你就无需发布你的库的源代码。

如果你对许可证有更多疑问,请阅读MPL 2.0的常见问题解答

你可以在此处阅读完整条款:LICENSE