有几种常用的操作可以从数据库中检索结果。
- 执行返回一条数据的查询操作。
- 准备要重复使用的语句,在多次执行该语句后进行销毁。
- 一次性的执行语句,并且不打算将其重复使用。
- 执行返回一条数据的查询,这种特殊情况有一个快捷方式。
Go 的 database/sql
包中函数名称很重要。如果一个函数名字包含 Query
, 那么该函数旨在向数据库发出查询问题, 并且即使它为空,也将返回一组行。不返回行的语句不应该使用 Query
函数; 而应使用 Exec()
。
从数据库获取数据
让我们来看一个如何查询数据库和处理数据的例子. 我们将在用户表 users
中查询 id
为 1 的用户, 那么如何打印该用户的 id
和 name
呢,我们需要使用 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)
}
下面是此段代码所做的事情:
- 我们使用
db.Query()
将查询请求发送给数据库,我们通常要检查是否报错。 - 我们需要在程序最后使用
rows.Close()
来关闭,这是非常重要的。 - 我们将使用
rows.Next()
来遍历每行的数据 - 我们使用
rows.Scan()
来将每一行中每一列的数据赋值给变量。 - 当把每一行的数据进行遍历后,检查是否出错。
这几乎是 Go 中唯一的方法。例如,您无法获得一行作为 map
。那是因为所有内容都是强类型的。您需要创建正确类型的变量,并将指针传递给它们。
其中的两个部分很容易出错,并可能带来严重的后果。
- 您应该始终检查
for rows.Next()
循环的末尾是否有错误。如果循环过程中出现错误,您需要知道它。不要只假设循环会迭代,直到您处理完所有行。 - 其次,只要存在打开的结果集(由
rows
表示),底层连接就很忙,不能用于任何其他查询。这意味着它在连接池中不可用。如果使用rows.Next()
迭代所有行,最终将读取最后一行,并且rows.Next()
将遇到内部 EOF 错误并为您调用rows.Close()
。但是,如果出于某种原因您退出该循环——提前返回,等等情况——那么rows
不会关闭,连接保持打开。(不过,如果rows.Next()
由于错误返回 false,那么它会自动关闭)。这是耗尽资源的一种简单方式。 - 如果
rows.Close()
已经关闭,那么它是一个无害的 no-op,因此您可以多次调用它。但是请注意,我们首先检查错误,只有在没有错误时才调用rows.Close()
,以避免 runtime panic。 - 您应该 始终
defer rows.Close()
,即使您也在循环末尾显式调用rows.Close()
,这不是个坏主意。 - 不要在循环内
defer
。在函数退出之前,不会执行延迟语句,因此长时间运行的函数不应使用该语句。如果这样做,您将慢慢积累内存。如果要在循环中重复查询和使用结果集,则应在处理完每个结果后显式调用rows.Close()
,而不要使用defer
。
Scan() 如何工作
当您遍历行并将其扫描到目标变量中时,Go 会在后台执行数据类型转换。它基于目标变量的类型。意识到这一点可以清理您的代码并有助于避免重复的工作。
例如,假设您从用字符串列定义的表中选择一些行,例如VARCHAR(45)
或类似名称。但是,您偶然知道该表始终包含数字。如果将指针传递给字符串,Go 会将字节复制到字符串中。现在您可以使用 strconv.ParseInt()
或类似方法将值转换为数字。您必须检查 SQL 操作中的错误以及解析整数的错误。这是混乱而又乏味的。
或者,您可以仅传递 Scan()
指向整数的指针。 Go 将检测到该情况并为您调用 strconv.ParseInt()
。如果转换中出现错误,则对 Scan()
的调用将返回该错误。您的代码现在变得更整洁,更小了。这是使用 database/sql
的推荐方法。
预处理查询
通常,您应该始终对要多次使用的查询执行预处理。预处理查询的结果是一个预处理语句,该语句可以具有占位符(也称为绑定值),用于执行该语句时将提供的参数。出于所有常见原因(例如,避免 SQL 注入攻击),这比串联字符串好得多。
在 MySQL 中,参数占位符是 ?
,而在 PostgreSQL 中则是 $N
,其中 N 是一个数字。 SQLite 接受任何一种。在 Oracle 中,占位符以冒号开头并被命名,例如 :param1
。我们将使用 ?
,因为我们使用 MySQL 作为例。
stmt,err := db.Prepare("select id 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
var has bool
err = db.QueryRow("select name from users where id = ?", 1).Scan(&name)
if err != nil {
if err == sql.ErrNoRows {
has = false
} else {
log.Fatal(err)
}
} else { has = true }
fmt.Println(name, has)
错误信息会在 Scan()
之后返回。 QueryRow()
也可以用于预处理语句:
stmt, err := db.Prepare("select name from users where id = ?")
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
var name string
var has bool
err = stmt.QueryRow(1).Scan(&name)
if err != nil {
if err == sql.ErrNoRows {
has = false
} else {
log.Fatal(err)
}
} else { has = true }
fmt.Println(name, has)
转自:https://learnku.com/docs/go-database-sql/retrieving/9477