Golang 中要使用数据库需要用到 database/sql
库,但是这个库只提供了基本的接口,不涉及具体的数据库操作实现,要操作数据库还需要引入一些额外的数据库驱动。在这里列举了部分数据库驱动,其中就包含今天重点要说明的 go-sql-driver
。
数据库连接池
在 Go 中使用 db,_:=sql.Open("mysql","root:@/gotest")
来打开一个数据库对象。但是要注意调用 Open 之后并没有和数据库建立连接,只是简单的创建了一个对象方便后续使用。所以 Open 调用成功也不代表连接数据库成功。要判断是否连接成功可以调用 Ping()
进行判断。
db, _ := sql.Open("mysql", "root:@/gotest?parseTime=true")
err := db.Ping()
if err != nil {
fmt.print(err)
}
数据库连接池是由 database/sql
包维护的,使用的时候不用关心连接池的状态。这个库提供了几个函数来控制连接池的状态。
SetMaxOpenConns
:设置最大打开的连接数。SetMaxIdleConns
:设置最大空间连接数。SetConnMaxLifetime
:设置每个连接数的存活时间。
Prepared Statement
使用了预编译 (prepared statement) 可以避免 SQL 注入。预编译执行 SQL 过程如下:
客户端把 SQL 模板发送到 MySQL 服务端,SQL 语句中参数使用 ? 来占位,MySQL 会返回一个 id 来标示本次连接。
客户端把查询参数和 1 中返回的 id 发送给 MySQL 服务端进行查询,得到返回结果。
SQL 执行完毕,调用 close 关闭数据库连接。
对于 Go 而言,代码过程和上面的流程有点不一样。由于使用的时候不维护数据库连接池状态,在 Go 中预编译的过程变成了下面的状态:
调用
prepare
函数告诉数据库连接池要进行 SQL 查询的预编译,数据库连接池会选择某一个连接,然后和 MySQL 服务端通信进行。stmt 会保存该连接,并不关心 MySQL 服务端返回的 id。当执行
Exec
函数发送参数进行 SQL 查询,会尝试使用 1 中保存的连接去发送参数,如果该连接已经被关闭,或者在进行其他查询,那么会从连接池中重新选择一个新的连接再次进行 prepare,然后使用新的连接发送参数进行查询。
比较两个流程可以发现,Go 中屏蔽了代码和数据库的连接,代码只和 database/sql
维护的连接池交互。
但是这种设计会存在一个问题:在高并发的时候,一个连处理 prepare 之后又去处理新的逻辑,导致执行 Exec
在执行的时候使用新的连接重新 prepare
。
其他
package main
import (
"database/sql"
"fmt"
"time"
)
import _ "github.com/go-sql-driver/mysql"
type Article struct {
ID int
Title string
Content string
UserID string
Time time.Time
}
func main() {
db, _ := sql.Open("mysql", "root:@/gotest?parseTime=true")
defer db.Close()
err := db.Ping()
if err != nil {
fmt.Println(err)
}
article := new(Article)
stmt, err := db.Prepare("select id,title,content,user_id,time from articles where id = ?") // ①
defer stmt.Close() // ③
if err != nil {
fmt.Println(err)
}
err = stmt.QueryRow(13).Scan(&article.ID, &article.Title, &article.Content, &article.UserID, &article.Time) // ②
if err != nil {
fmt.Println(err)
}
fmt.Println(article)
}
以上面的代码为例,看下一些容易疑惑的问题。
Scan 函数参数的顺序是怎样子?
Scan 函数用来接收数据库返回的数据,如果 SQL 语句中 select
字段为 *
,那么 Scan 函数的参数顺序应该和表结构定义的顺序一样。如果 SQL 语句中 select
指定了字段,那么 Scan 函数参数顺序应该和 SQL 语句中的指定的字段顺序一致。
数据库中字段为 varchar 类型,那么接收返回值的字段只能是 string 吗?
这个问题本质是会不会进行类型转换的问题,答案是会自动进行类型转换,Scan 在执行的时候会对类型进行判断,如果接收数据类型和表字段类型不一致,会隐式的帮你进行一次类型转换,如果转换失败就直接报错。
数据库中的
timestamp
如何转换成 Go 中的time.Time
如果数据库中定义字段为 timestamp 类型,那么在 Go 中想要用 time.Time
类型来接收,在 open 数据库连接的时候需要加上 parseTime=true
的参数
db, _ := sql.Open("mysql", "root:@/gotest?parseTime=true")




