公众号:程序员读书,欢迎关注
对数据库的CURD
应该是现代应用程序的必备功能吧,Go
语言当然也对数据库的操作提供了非常完善的支持。
尽管在Go语言社区中有很多优秀的ORM
库或框架(比如GORM
)能让我们更方便地操作数据库,不过要更好地使用ORM
库,掌握Go原生操作数据库database/sql
包的使用还是有必要的。
所以,在这篇文章中,我们来学习一下database/sql
包的使用吧。
database/sql简介
我们可以把标准库中的database/sql
包看作一个数据库操作抽象层 ,因为database/sql
并不直接连接并操作数据库,而是为不同的数据库驱动提供了统一的API
,database/sql
包与驱动包(driver
)的关系如下图所示:
对数据库的具体操作由对应数据库的驱动包来完成,访问不同的类型的数据库需要导入不同的驱动包,而所有驱动包都必须实现database/sql/driver
包下的相关接口,因此database/sql
、database/sql/driver
以及驱动包的关系如下图所示:
这样做的好处在于,如果某一天我们想迁移数据库,比如从MySQL
切换为Postgres
,只需要更换驱动包即可,而不需要把数据库操作的代码重写一遍。
连接数据库
连接到数据库,获取一个数据库操作句柄,简单来说可以分为三步:
-
选择对应数据库的驱动,并导入。
-
配置连接数据库所需的
DSN
。 -
使用
sql.Open()
函数打开数据连接,获得*sql.DB
对象。
DSN
是Data Source Name
,包含了连接数据库所需的参数,比如用户,密码、数据编码、数据库名称等。
下面我们以MySQL
,Postgres
,SQLite
为例介绍在Go
语言中如何连接到数据库。
MySQL
连接MySQL
驱动包推荐用github.com/go-sql-driver/mysql
库,其完整的DSN
为如以所示,可以看到MySQL
的DSN
各个部分都是可选的:
go
[username[:password]@][protocol[host[:port]]]/dbname[?param1=value1&...¶mN=valueN]
username
:用户名。password
:密码,与用户名之间要用冒号分隔。protocol
:网络协议,默认用tcp
即可。host
:数据库地址。port
:端口号,默认为3306dbname
:数据库名称param1~paramN
:可选参数。
go
package main
import (
"database/sql"
// 匿名导入
_ "github.com/go-sql-driver/mysql"
)
func main(){
//db为*sql.DB
//DSN要根据实际替换
db, err := sql.Open("mysql", "user:password@/dbname")
if err != nil {
panic(err)
}
}
Postgres
连接Postgres
数据库推荐用github.com/lib/pq
,其连接DSN
为:
go
host=192.168.0.1 user=postgres dbname=postgres port=5432 password=123456 sslmode=disable
user
:用户名。password
:密码,与用户名之间要用冒号分隔。dbname
:数据库名称。sslmode
:是否使用ss模式,其值可以是disable
,required
,verify-ca
,verify-full
等。host
:主机port
:端口号
go
package main
import (
"context"
"fmt"
"os"
"github.com/lib/pq"
)
func main(){
dsn := "user=postgres dbname=postgres password=123456 sslmode=disable"
db, err := sql.Open("postgres", dsn)
if err != nil {
log.Fatal(err)
}
defer db.Close()
}
SQLite
连接SQLite
数据库推荐用github.com/mattn/go-sqlite3
,SQLite
是嵌入式数据库,因此它的DSN
是数据库文件所在的路径:
go
import (
"database/sql"
_ "github.com/mattn/go-sqlite3"
)
func main(){
dsn := "./test.db"
db,err := sql.Open("sqlite3",dsn)
}
关闭连接
打开数据库连接要记得关闭数据连接,一般在defer
语句后面调用sql.DB
的Close()
方法:
go
import (
"database/sql"
_ "github.com/mattn/go-sqlite3"
)
func main(){
dsn := "./test.db"
db,err := sql.Open("sqlite3",dsn)
if err != nil{
panic(err)
}
//关闭数据库连接
defer db.Close()
}
连接池设置
sql.DB
对象内包含一个数据库连池,并支持通过以下几个方法设置连接池的相关配置:
go
db.SetConnMaxLifetime(0) //最大打开的连接时间
db.SetMaxIdleConns(50) //最大闲置连接数
db.SetMaxOpenConns(50) //最大打开的连接数
示例数据表
为了后面更好的进行实例演示,我们需要一个演示数据库,这里以SQLite
数据库为例,数据库名称为test.db
,创建一个users
数据表:
shell
$ sqlite3 ./test.db
sqlite> create table users(
...> id integer not null primary key autoincrement, -- 用户id
...> name text not null, --用户名
...> gender tinyint not null default 0, --性别
...> mobile test not null default '' --手机号
); mobile test not null default '' --手机号
...> );
sql.DB
前面我们调用sql.Open()
获得了sql.DB
对象,这是操作数据的句柄,也可以理解为一个数据库连接池,可以通过sql.DB
的Conn()
可以获得数据库连接池里的单个连接对象sql.Conn
:
go
ctx, _ := context.WithCancel(context.Background())
conn, err := db.Conn(ctx)
无论是sql.DB
,还是sql.Conn
,其对数据库执行CURD
操作的方法是类似,一般推荐使用sql.DB
。
执行insert
、update
、delete
用Exec()
和ExecContext()
方法,其签名如下:
go
func (db *DB) Exec(query string, args ...any) (Result, error)
func (db *DB) ExecContext(ctx context.Context, query string, args ...any) (Result, error)
上面的方法执行后返回sql.Result
接口的实例,这个接口只有两个方法:
go
type Result interface {
//执行insert语句时,返回自增id
LastInsertId() (int64, error)
//影响行数
RowsAffected() (int64, error)
}
示例代码:
go
package main
import (
"database/sql"
"fmt"
_ "github.com/mattn/go-sqlite3"
)
func main() {
db, err := sql.Open("sqlite3", "../test.db")
if err != nil {
panic(err)
}
defer db.Close()
insertSql := "INSERT INTO users(name,gender,mobile) VALUES(?,?,?),(?,?,?)"
insertResult, err := db.Exec(insertSql, "小白", 2, "166xxxxxxxx", "小张", 1, "13493023333")
if err != nil {
panic(err)
}
lastInsertId, _ := insertResult.LastInsertId()
fmt.Printf("最新记录id:%d\n", lastInsertId)
rowsAffected, _ := insertResult.RowsAffected()
fmt.Println("影响行数:", rowsAffected)
updateSql := "UPDATE users SET mobile = ? WHERE id = ?"
updateResult, err := db.Exec(updateSql, "136xxxxxxxx", 1)
if err != nil {
panic(err)
}
rowsAffected, _ = updateResult.RowsAffected()
fmt.Printf("影响行数:%d\n", rowsAffected)
deleteSql := "DELETE FROM users WHERE id = ?"
deleteResult, err := db.Exec(deleteSql, 14)
if err != nil {
panic(err)
}
rowsAffected, _ = deleteResult.RowsAffected()
fmt.Printf("影响行数:%d\n", rowsAffected)
}
如果你只想从数据表查询一行数据,可以调用QueryRow()
和QueryRowContext()
方法:
go
func (db *DB) QueryRow(query string, args ...any) *Row
func (db *DB) QueryRowContext(ctx context.Context, query string, args ...any) *Row
上面的方法返回的是sql.Row
对象,代表查询的那一行数据,这个对象只有Scan
方法可以获取对象里的数据,调用Scan
方法时传进去参数个数必须与查询返回的数据列数一致:
go
package main
import (
"database/sql"
"fmt"
_ "github.com/mattn/go-sqlite3"
)
func main() {
db, err := sql.Open("sqlite3", "./test.db")
if err != nil {
panic(err)
}
defer db.Close()
selectOne := "SELECT * FROM users WHERE id = ?"
row := db.QueryRow(selectOne, 1)
var (
id int
name string
gender uint8
mobile string
)
//扫描数据
err = row.Scan(&id, &name, &gender, &mobile)
if err != nil {
panic(err)
}
genderText := "未知"
switch gender {
case 1:
genderText = "男"
case 2:
genderText = "女"
}
fmt.Printf("用户:%s | 性别:%s | 手机:%s\n", name, genderText, mobile)
}
然而,更多的时候,我们需要查询多行数据,可以调用Query()
和QueryContext()
方法:
go
func (db *DB) Query(query string, args ...any) (*Rows, error)
func (db *DB) QueryContext(ctx context.Context, query string, args ...any) (*Rows, error)
上面两个方法返回的是sql.Rows
对象,代表查询回来的多行数据,可以在for
循环语句中通过Next()
和Scan()
方法扫描每一行数据,传给Scan
方法的参数数量必须与查询回来的列数相同,另外,要记得调用Close()
方法关闭sql.Rows
对象:
go
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/mattn/go-sqlite3"
)
func main() {
db, err := sql.Open("sqlite3", "./test.db")
if err != nil {
panic(err)
}
defer db.Close()
selectMany := "SELECT * FROM users"
rows, err := db.Query(selectMany)
if err != nil {
panic(err)
}
defer rows.Close()
for rows.Next() {
var (
id int
name string
gender uint8
mobile string
)
if err := rows.Scan(&id, &name, &gender, &mobile); err != nil {
log.Fatal(err)
}
genderText := "未知"
switch gender {
case 1:
genderText = "男"
case 2:
genderText = "女"
}
fmt.Printf("用户:%s | 性别:%s | 手机:%s\n", name, genderText, mobile)
}
}
sql.Stmt
SQL
语句预编译机制允许先把带有参数占位符的SQL语句发送给数据库,数据库会提前对SQL语句进行编译,之后我们再发送对应占位符的参数给数据库 。
SQL语句的好处在于:
- 预防SQL注入攻击。
- 复杂的SQL语句,提前编译提交SQL语句执行效率。
调用sql.DB
、sql.Conn
和sql.Tx(下面会讲到)
的Prepare()
方法或者PrepareContext()
会返回一个sql.Stmt
:
go
stmt, err := db.Prepare("INSERT INTO users(name,) VALUES(?,?,?),(?,?,?)")
因为SQL语句已经预发送给数据库,因此调用sql.Stmt
的ExecXXX()
和QueryXXX()
时,只需要发送参数即可:
go
Stmt.Exec("小红",2,"135xxxxxxxx","小刘",1,"136xxxxxxxx")
sql.Stmt
对象里的方法返回值与sql.DB
一样是sql.Result
,具体使用参考前面的例子。
sql.Tx
sql.Tx
表示一个数据库事务对象,调用sql.DB
或者sql.Conn
对象的Begin()
或者BeginTx()
方法会返回一个sql.Tx
:
go
tx,err := db.Begin()
sql.Tx
与sql.DB
执行CURD
的方法基本相同,所不同的是,通过sql.Tx
执行的语句,最后要调用sql.Tx
的Commit()方法
提交事务,如果执行时有错误发生,则应该调用Rollback()
方法回滚事务:
go
package main
import (
"database/sql"
_ "github.com/mattn/go-sqlite3"
)
func main() {
db, err := sql.Open("sqlite3", "./test.db")
if err != nil {
panic(err)
}
defer db.Close()
tx, err := db.Begin()
if err != nil {
panic(err)
}
if _, err := tx.Exec("INSERT INTO users VALUES(?,?,?,?)", 1, "小龙", 1, "137xxxxxxxx"); err != nil {
tx.Rollback()
}
if _, err := tx.Exec("INSERT INTO users VALUES(?,?,?,?)", 2, "小明", 1, "137xxxxxxxx"); err != nil {
tx.Rollback()
}
tx.Commit()
}
小结
看到这里,相信你应该学会了在Go语言如何操作数据库了吧,好了,稍稍总结一下:
database/sql
包作为数据库抽象层,并不会直接连接和操作数据库,具体的数据库操作交由对应数据库的驱动包来完成。sql.DB
对象表示一个数据库句柄,其中包含一个数据库连接池。sql.Conn
对象表示一个连接池里的普通数据库连接。sql.Tx
表示一个数据库事务对象,通过该对象执行的SQL语句要调用Commit()
方法才会生效,如果执行过程发生错误,要调用Rollback()
方法回滚事务。sql.Stmt
表示一个预编译对象,通过这个对象执行的SQL语句会先发送给数据库,之后再发送参数,这样可以避免SQL注入以及提前编译语句,提高执行效率。sql.Rows
是调用QueryXXX
这类方法返回的表示多行数据的对象,内置迭代器,可以调用for
语句迭代查询回来的数据。sql.Row
是调用QueryRowXXX
这类方法返回的对象,代表一行数据。sql.Result
是调用ExecXXX
这类方法进行update
,delete
,insert
操作之后返回的结果。