golang连接数据库需要使用到系统标准库 database/sql
和 数据库驱动,database/sql
支持数据库连接池,是并发安全的。
下面以mysql
为例,展示下连接,查询,写入,更新,删除,事务等操作。
下载数据库连接驱动
go get -u github.com/go-sql-driver/mysql
数据库操作
连接数据库
连接数据库使用 sql.Open()
函数打开数据库连接(此时只会判断DSN是否错误,不会连接数据库),并使用 db.Ping()
进行数据库连接。
func Open(driverName, dataSourceName string) (*DB, error)
dataSourceName
的格式为username:password@tcp(host:port)/database
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql" // 导入mysql包,并不使用
)
// 连接mysql示例
func main() {
dsn := "root:123123@tcp(127.0.0.1:3306)/golang"
db, err := sql.Open("mysql", dsn) // 打开一个数据库连接,不会校验用户名和密码是否正确
if err != nil {
fmt.Printf("DSN[%s]格式错误: %v \n", dsn, err) // 如果出现错误,则DSN格式错误
return
}
err = db.Ping() // 尝试连接mysql
if err != nil {
fmt.Printf("连接数据库失败: %v \n", err)
return
}
fmt.Println("连接数库成功")
}
查询
单行查询
单行查询使用 db.QueryRow()
进行查询
func (db *DB) QueryRow(query string, args ...interface{}) *Row
func (r *Row) Scan(dest ...interface{}) error
QueryRow
函数会返回一个 *Row
指针,通过该指针对象的 Scan()
方法可以获得结果,如果没有查询到结果,会返回一个 sql.ErrNoRows
。
package main
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
"log"
)
var db *sql.DB
// 声明一个user结构体
type user struct {
id int
nickname string
avatar *string // 数据库字段可以为空时,使用指针类型
}
func initDB() (err error) {
dsn := "root:@tcp(127.0.0.1:3306)/golang"
db, err = sql.Open("mysql", dsn) // 打开一个数据库连接,不会校验用户名和密码是否正确
if err != nil {
return err
}
err = db.Ping()
if err != nil {
return err
}
return
}
// 连接mysql示例
func main() {
err := initDB() // 连接数据库
if err != nil {
log.Println("数据库连接失败,", err)
return
}
sqlStr := "select id, nickname, avatar from users where id = ?" // 构建查询语句
var u user
err = db.QueryRow(sqlStr, 1).Scan(&u.id, &u.nickname, &u.avatar) // 查询单条数据并扫描结果
if err != nil {
if err == sql.ErrNoRows {
log.Print("未查询到结果!")
return
}
log.Printf("查询失败: %s", err)
return
}
log.Printf("查询结果:%#v", u)
}
注意
- Scan 方法会释放当前的数据库连接回到连接池中,如果调用了QueryRow不调用Scan方法,则会导致数据库连接无法释放;
- Scan的时候,需要保证查询的字段顺序及数量和变量保持一致,否则会返回错误。
- 当数据库中字段可以为空时,字段应使用指针类型或
sql.NullString
- 如果未查询到结果,
Scan
会返回一个sql.ErrNoRows
,需要通过err == sql.ErrNoRows
来确定。
多行查询
多行查询使用 db.Query()
函数进行查询,返回一个*Rows
和 error
;
示例:
// 多行查询
func queryMany() {
sqlStr := "select id, nickname, avatar from users"
rows, err := db.Query(sqlStr) // 查询sql语句
if err != nil {
fmt.Println("查询失败", err)
return
}
users := make([]user, 0)
defer rows.Close() // rows需要手动调用Close()释放连接
for rows.Next() { // 遍历调用Scan方法
var u user
err = rows.Scan(&u.id, &u.nickname, &u.avatar) // 对对象进行赋值
if err != nil {
fmt.Println("获取数据失败", err)
continue
}
users = append(users, u)
}
fmt.Printf("%#v", users)
}
注意:
Rows
需要手动调用Close()
方法释放数据库连接;- 获取
Rows
中的数据,需要使用循环来进行操作。
插入、修改和删除
插入、修改和删除统一通过 Exec
函数进行操作。
func (db *DB) Exec(query string, args ...interface{}) (Result, error)
Exec
方法返回一个 Result
对象,该对象有两个方法:
LastInsertId()
用于获取插入的id,
RowsAffected()
用户获取受影响的行数。
插入数据
func insert() {
sqlStr := "insert into users(name, age) values('王五', 10)" // 插入sql
ret, err := db.Exec(sqlStr) // 执行插入语句
if err != nil {
fmt.Println("插入数据失败,", err)
return
}
lastId, err := ret.LastInsertId() // 获取插入ID
if err != nil {
fmt.Println("插入数据失败,", err)
return
}
fmt.Println("插入数据成功:", lastId)
}
更新数据
func update() {
sqlStr := "update users set age = 12 where name = '王五'"
ret, err := db.Exec(sqlStr)
if err != nil {
fmt.Println("更新数据失败", err)
return
}
rows, err := ret.RowsAffected()
if err != nil {
fmt.Println("获取行数失败", err)
return
}
fmt.Println("更新数据行数:", rows)
}
删除数据
func deleteRow() {
sqlStr := "delete from users where age = 12"
ret, err := db.Exec(sqlStr)
if err != nil {
fmt.Println("删除数据失败", err)
return
}
rows, err := ret.RowsAffected()
if err != nil {
fmt.Println("获取行数失败", err)
return
}
fmt.Println("删除行数:", rows)
}
Mysql 预处理
什么是预处理
普通Sql语句执行过程:
- 客户端对sql语句进行占位符替换得到完整的sql语句
- 客户端发送完整的sql语句到mysql服务端
- mysql服务端执行完整的sql语句,并将结果返回给客户端
预处理sql语句执行过程
- 将sql语句分成命令部分和数据部分
- 先把命令发送到mysql服务端,mysql服务端对sql语句进行预处理
- 然后将数据部分发送给mysql服务端,mysql服务端对sql语句进行占位符替换
- mysql服务端执行完整的sql语句,并将结果返回给客户端
为什么要预处理?
- 优化mysql服务器重复执行sql的方法,提升服务器性能。对于相同的sql,可以一次编译多次执行,节省编译的性能和时间开销
- 避免sql注入
使用预处理
使用预处理需要使用到 db.Prepare()
方法
func (db *DB) Prepare(query string) (*Stmt, error)
该方法返回一个 Stmt
的指针对象,通过调用Stmt
的 Query()
, QueryRow()
, Exec()
等方法来执行sql语句,该对象需要调用 Close()
进行关闭
- 调用
db.Prepare()
方法进行sql预处理,得到Stmt
对象 - 通过调用
Stmt
的Query()
,QueryRow()
,Exec()
等方法来执行sql语句, - 关闭
Stmt
对象
func prepareQueryDemo(id int) {
sqlStr := "select id, name, age from users where id = ?"
stmt, err := db.Prepare(sqlStr)
if err != nil {
fmt.Println("预处理失败", err)
return
}
defer stmt.Close()
row := stmt.QueryRow(id)
var u user
err = row.Scan(&u.id, &u.name, &u.age)
if err != nil {
fmt.Println("获取数据失败", err)
return
}
fmt.Printf("获取数据成功%#v\n", u)
}
事务
事务使用 db.BeginTx
进行处理,开启事务后,事务内的sql需要使用返回的*Tx
进行处理。不能使用 db
进行操作,否则事务不生效。还可以通过sql.TxOption
配置隔离等级。
func transaction() {
ctx := context.Background()
tx, err := db.BeginTx(ctx, &sql.TxOptions{}) //开启事务
if err != nil {
log.Fatal("begin tx error:", err)
}
// do something
sqlStr1 := "insert into users(nickname, avatar) values('王五', null)" // 插入sql
_, err = tx.Exec(sqlStr1) // 执行插入语句
sqlStr2 := "insert into users(nickname, avatar) values('李四', null)" // 插入sql
_, err = tx.Exec(sqlStr2) // 执行插入语句
if err != nil {
tx.Rollback() // 事务回滚
log.Println("Rollback", err)
return
}
tx.Commit() // 事务提交
log.Println("committed")
}
我的博客 《Golang数据库操作》