暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

深入一点的 Go 数据库使用

为六斗米折腰 2018-11-08
252

Golang 中要使用数据库需要用到 database/sql
库,但是这个库只提供了基本的接口,不涉及具体的数据库操作实现,要操作数据库还需要引入一些额外的数据库驱动。在这里列举了部分数据库驱动,其中就包含今天重点要说明的 go-sql-driver

数据库连接池

在 Go 中使用 db,_:=sql.Open("mysql","root:@/gotest")
来打开一个数据库对象。但是要注意调用 Open 之后并没有和数据库建立连接,只是简单的创建了一个对象方便后续使用。所以 Open 调用成功也不代表连接数据库成功。要判断是否连接成功可以调用 Ping()
进行判断。

  1. db, _ := sql.Open("mysql", "root:@/gotest?parseTime=true")

  2. err := db.Ping()

  3. if err != nil {

  4.    fmt.print(err)

  5. }

数据库连接池是由 database/sql
包维护的,使用的时候不用关心连接池的状态。这个库提供了几个函数来控制连接池的状态。

  • SetMaxOpenConns
     :设置最大打开的连接数。

  • SetMaxIdleConns
     :设置最大空间连接数。

  • SetConnMaxLifetime
     :设置每个连接数的存活时间。

Prepared Statement

使用了预编译 (prepared statement) 可以避免 SQL 注入。预编译执行 SQL 过程如下:

  1. 客户端把 SQL 模板发送到 MySQL 服务端,SQL 语句中参数使用 ? 来占位,MySQL 会返回一个 id 来标示本次连接。

  2. 客户端把查询参数和 1 中返回的 id 发送给 MySQL 服务端进行查询,得到返回结果。

  3. SQL 执行完毕,调用 close 关闭数据库连接。

对于 Go 而言,代码过程和上面的流程有点不一样。由于使用的时候不维护数据库连接池状态,在 Go 中预编译的过程变成了下面的状态:

  1. 调用 prepare
     函数告诉数据库连接池要进行 SQL 查询的预编译,数据库连接池会选择某一个连接,然后和 MySQL 服务端通信进行。stmt 会保存该连接,并不关心 MySQL 服务端返回的 id。

  2. 当执行 Exec
     函数发送参数进行 SQL 查询,会尝试使用 1 中保存的连接去发送参数,如果该连接已经被关闭,或者在进行其他查询,那么会从连接池中重新选择一个新的连接再次进行 prepare,然后使用新的连接发送参数进行查询。

比较两个流程可以发现,Go 中屏蔽了代码和数据库的连接,代码只和 database/sql
维护的连接池交互。

但是这种设计会存在一个问题:在高并发的时候,一个连处理 prepare 之后又去处理新的逻辑,导致执行 Exec
在执行的时候使用新的连接重新 prepare

其他

  1. package main


  2. import (

  3.    "database/sql"

  4.    "fmt"

  5.    "time"

  6. )


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


  8. type Article struct {

  9.    ID      int

  10.    Title   string

  11.    Content string

  12.    UserID  string

  13.    Time    time.Time

  14. }


  15. func main() {

  16.    db, _ := sql.Open("mysql", "root:@/gotest?parseTime=true")


  17.    defer db.Close()


  18.    err := db.Ping()

  19.    if err != nil {

  20.        fmt.Println(err)

  21.    }


  22.    article := new(Article)


  23.    stmt, err := db.Prepare("select id,title,content,user_id,time from articles where id = ?") // ①

  24.    defer stmt.Close()   // ③

  25.    if err != nil {

  26.        fmt.Println(err)

  27.    }


  28.    err = stmt.QueryRow(13).Scan(&article.ID, &article.Title, &article.Content, &article.UserID, &article.Time)  // ②

  29.    if err != nil {

  30.        fmt.Println(err)

  31.    }

  32.    fmt.Println(article)

  33. }

以上面的代码为例,看下一些容易疑惑的问题。

Scan 函数参数的顺序是怎样子?

Scan 函数用来接收数据库返回的数据,如果 SQL 语句中 select
字段为 *
,那么 Scan 函数的参数顺序应该和表结构定义的顺序一样。如果 SQL 语句中 select
指定了字段,那么 Scan 函数参数顺序应该和 SQL 语句中的指定的字段顺序一致。

数据库中字段为 varchar 类型,那么接收返回值的字段只能是 string 吗?

这个问题本质是会不会进行类型转换的问题,答案是会自动进行类型转换,Scan 在执行的时候会对类型进行判断,如果接收数据类型和表字段类型不一致,会隐式的帮你进行一次类型转换,如果转换失败就直接报错。

数据库中的 timestamp
如何转换成 Go 中的 time.Time

如果数据库中定义字段为 timestamp 类型,那么在 Go 中想要用 time.Time
类型来接收,在 open 数据库连接的时候需要加上 parseTime=true
的参数

  1. db, _ := sql.Open("mysql", "root:@/gotest?parseTime=true")


文章转载自为六斗米折腰,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论