【GO基础学习】Go操作数据库MySQL
文章目录
- 建立测试表
- 使用模版
- insert操作
- update操作
- delete操作
- select操作
- 小结
- SQL预处理
- 事务
- sqlx
- 增删改成操作
- 事务
- sqlx.In 的使用
建立测试表
mysql数据库里面新建测试表person、place 表,方便后面测试:
CREATE TABLE `person` (
`user_id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(260) DEFAULT NULL,
`sex` varchar(260) DEFAULT NULL,
`email` varchar(260) DEFAULT NULL,
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
CREATE TABLE place (
country varchar(200),
city varchar(200),
telcode int
)ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
使用模版
先上怎么使用,然后解读每一步的操作,最后上一些详细的操作。
简单的操作MySQL的模版
package main
import (
"database/sql"
"fmt"
"time"
"log"
_ "github.com/go-sql-driver/mysql"
)
// 定义一个全局对象db
var db *sql.DB
// 定义一个初始化数据库的函数
func initMySQLDB() (err error) {
// DSN:Data Source Name
dsn := "user:password@tcp(127.0.0.1:3306)/sql_test?charset=utf8mb4&parseTime=True"
// 不会校验账号密码是否正确
// 注意!!!这里不要使用:=,我们是给全局变量赋值,然后在main函数中使用全局变量db
// 打开数据库连接(但不校验)
db, err = sql.Open("mysql", dsn)
if err != nil {
return fmt.Errorf("sql.Open failed: %v", err)
}
// 校验数据库连接
if err = db.Ping(); err != nil {
return fmt.Errorf("db.Ping failed: %v", err)
}
// 设置数据库连接池参数
db.SetMaxOpenConns(10) // 最大打开连接数
db.SetMaxIdleConns(5) // 最大空闲连接数
db.SetConnMaxLifetime(time.Hour) // 每个连接的生命周期
fmt.Println("Database connection initialized successfully.")
return nil
}
func main() {
// 初始化数据库
if err := initMySQLDB(); err != nil {
log.Fatalf("initMySQLDB failed: %v\n", err)
}
defer db.Close() // 程序退出时关闭数据库连接
// 使用db进行数据库的增删改查操作...
}
database/sql
是 Go 的标准库,提供了与 SQL 数据库交互的接口,它以高度抽象的方式操作数据库,但需要具体的驱动来完成底层实现,如go-sql-driver/mysql
。github.com/go-sql-driver/mysql
是 MySQL 的驱动程序,它实现了database/sql
的接口,并通过 MySQL 的 C/S 协议与数据库通信。
可以通过go get -u github.com/go-sql-driver/mysql
或者go mod tidy
下载库:
在 go-sql-driver/mysql
的导入中,使用匿名导入-
,作用是调用驱动的 init
函数:
func init() {
sql.Register("mysql", &MySQLDriver{})
}
sql.Register
:
- 注册驱动,建立 driverName 和驱动实现的映射关系。
- 当调用 sql.Open(“mysql”, …) 时,通过这个映射找到对应的驱动。
MySQLDriver
实现了 driver.Driver 接口:
Open(name string) (driver.Conn, error)
:建立与数据库的连接。
- 数据库对象
(1)sql.DB
- 代表与数据库的连接池(不是单个连接)。
- 提供线程安全的操作,管理连接复用、超时、连接关闭等。
- 常用方法:
Open(driverName, dataSourceName string)
: 打开数据库连接。Ping()
: 测试连接是否可用。Query()
和Exec()
:执行查询和非查询操作。
(2)sql.Tx
- 代表事务,提供对事务操作的封装。
(3)sql.Rows
- 代表查询返回的多行结果。
(4)sql.Row
- 代表查询返回的单行结果。
db, err = sql.Open("mysql", dns)
(1)sql.Open
- 检查驱动是否注册。
- 初始化
sql.DB
对象(不建立实际连接,仅配置连接信息)。 - 返回一个连接池对象,连接池会在需要时与数据库建立连接。
(2)数据源名称(DSN)的解析
- DSN 是连接信息的字符串形式:
user:password@protocol(address)/dbname?param1=value1¶m2=value2
- 驱动负责解析这些信息,构建与数据库通信的上下文。
(3)连接建立
- 当执行
db.Ping()
或第一次Query
时,sql.DB
会通过驱动的Open
方法与数据库建立物理连接。 go-sql-driver/mysql
解析 DSN 后,通过 TCP 或 Unix 套接字与 MySQL 服务器通信。
- 数据库连接池的管理
sql.DB
管理了一个连接池,提供连接复用、超时管理等功能:
-
最大连接数
SetMaxOpenConns(n int)
:设置最大打开连接数。如果n大于0且小于最大闲置连接数,会将最大闲置连接数减小到匹配最大开启连接数的限制。 如果n<=0,不会限制最大开启连接数,默认为0(无限制)。
-
最大空闲连接数
SetMaxIdleConns(n int)
:设置连接池中的最大空闲连接数。如果n大于最大开启连接数,则新的最大闲置连接数会减小到匹配最大开启连接数的限制。 如果n<=0,不会保留闲置连接。
-
连接生命周期
SetConnMaxLifetime(d time.Duration)
:设置连接的最大生存时间。
insert操作
插入、更新和删除操作都使用Exec
方法。
func main() {
// 初始化数据库
if err := initMySQLDB(); err != nil {
log.Fatalf("initMySQLDB failed: %v\n", err)
}
defer db.Close() // 程序退出时关闭数据库连接
// 执行数据库操作示例
insertRowDemo()
}
// insertRowDemo 插入数据示例
func insertRowDemo() {
sqlStr := "INSERT INTO person(username, sex, email) VALUES (?, ?, ?)"
result, err := db.Exec(sqlStr, "王五", "男", "XXX@qq.com")
if err != nil {
log.Printf("Insert failed: %v\n", err)
return
}
// 获取新插入数据的 ID
lastInsertID, err := result.LastInsertId()
if err != nil {
log.Printf("Get LastInsertId failed: %v\n", err)
return
}
fmt.Printf("Insert success, new ID is %d.\n", lastInsertID)
}
update操作
func main() {
// 初始化数据库
if err := initMySQLDB(); err != nil {
log.Fatalf("initMySQLDB failed: %v\n", err)
}
defer db.Close() // 程序退出时关闭数据库连接
// 执行数据库操作示例
updateRow(2)
}
// 更新数据
func updateRow(userID int) {
sqlStr := "UPDATE person SET username = ?, email = ? WHERE user_id = ?"
result, err := db.Exec(sqlStr, "李四", "XXX@162.com", userID)
if err != nil {
log.Printf("Update failed: %v\n", err)
return
}
rowsAffected, err := result.RowsAffected()
if err != nil {
log.Printf("Get RowsAffected failed: %v\n", err)
return
}
fmt.Printf("Update success, %d rows affected.\n", rowsAffected)
}
delete操作
func main() {
// 初始化数据库
if err := initMySQLDB(); err != nil {
log.Fatalf("initMySQLDB failed: %v\n", err)
}
defer db.Close() // 程序退出时关闭数据库连接
// 执行数据库操作示例
deleteRow(2)
}
// 删除数据
func deleteRow(userID int) {
sqlStr := "DELETE FROM person WHERE user_id = ?"
result, err := db.Exec(sqlStr, userID)
if err != nil {
log.Printf("Delete failed: %v\n", err)
return
}
rowsAffected, err := result.RowsAffected()
if err != nil {
log.Printf("Get RowsAffected failed: %v\n", err)
return
}
fmt.Printf("Delete success, %d rows affected.\n", rowsAffected)
}
select操作
- 单条记录查询
// Person 定义用户信息结构体
type Person struct {
UserID int `db:"user_id"`
Username string `db:"username"`
Sex string `db:"sex"`
Email string `db:"email"`
}
// main 主函数
func main() {
// 初始化数据库
if err := initMySQLDB(); err != nil {
log.Fatalf("initMySQLDB failed: %v\n", err)
}
defer db.Close() // 程序退出时关闭数据库连接
// 执行数据库操作示例
queryOneRow(3)
}
// 查询单条数据
func queryOneRow(userID int) {
sqlStr := "SELECT user_id, username, sex, email FROM person WHERE user_id = ?"
var person Person
err := db.QueryRow(sqlStr, userID).Scan(&person.UserID, &person.Username, &person.Sex, &person.Email)
if err != nil {
if err == sql.ErrNoRows {
log.Printf("No record found for userId: %d\n", userID)
return
}
log.Printf("Query failed: %v\n", err)
return
}
fmt.Printf("Query result: %+v\n", person)
}
Scan
方法,已经关闭rows释放持有的数据库链接,不需要我们自己关闭了:
运行结果:
- 多条记录查询:
// 查询多条数据
func queryMultipleRows() {
sqlStr := "SELECT userId, username, sex, email FROM person"
rows, err := db.Query(sqlStr)
if err != nil {
log.Printf("Query failed: %v\n", err)
return
}
defer rows.Close()
for rows.Next() {
var person Person
err := rows.Scan(&person.UserID, &person.Username, &person.Sex, &person.Email)
if err != nil {
log.Printf("Row scan failed: %v\n", err)
continue
}
fmt.Printf("Row: %+v\n", person)
}
// 检查遍历时是否遇到错误
if err := rows.Err(); err != nil {
log.Printf("Rows iteration error: %v\n", err)
}
}
小结
- 插入、更新和删除操作都使用
Exec
方法,传入sql语句,以及sql里面需要的参数:
result, err := db.Exec(sql, args...)
- 单条记录查询:
db.QueryRow(sql, args...).Scan()
,使用了scan方法,会自动关闭连接,不需要手动关闭。 - 多条记录查询:
rows, err := db.Query(sql)
,在使用rows.Next()
遍历所有记录前,需要defer rows.Close()
,防止遍历时中断,不能关闭所有链接。
SQL预处理
上面的示例中所有 SQL 参数均使用占位符 (?)
,避免了 SQL
注入的风险。
不正确的操作,sql注入示例:
// sql注入示例
func sqlInjectDemo(name string) {
sqlStr := fmt.Sprintf("select id, name, age from user where name='%s'", name)
fmt.Printf("SQL:%s\n", sqlStr)
var u user
err := db.QueryRow(sqlStr).Scan(&u.id, &u.name, &u.age)
if err != nil {
fmt.Printf("exec failed, err:%v\n", err)
return
}
fmt.Printf("user:%#v\n", u)
}
传入下面字段都会引发sql注入:
sqlInjectDemo("xxx' or 1=1#")
sqlInjectDemo("xxx' union select * from user #")
sqlInjectDemo("xxx' and (select count(*) from user) <10 #")
关于预防sql注入,除了采用直接在 db.Exec
或 db.Query
中使用占位符:
sqlStr := "SELECT * FROM person WHERE username = ? AND email = ?"
rows, err := db.Query(sqlStr, "Alice", "alice@example.com")
还可以采用db.Prepare
,它是为多次执行相似语句设计的,它可以提前编译和缓存 SQL 语句:
stmt, err := db.Prepare("INSERT INTO person(username, sex, email) VALUES (?, ?, ?)")
if err != nil {
log.Fatalf("Prepare failed: %v", err)
}
defer stmt.Close()
// 多次执行相似的 SQL 语句
_, err = stmt.Exec("Alice", "Female", "alice@example.com")
_, err = stmt.Exec("Bob", "Male", "bob@example.com")
关于这两种的选择:
单次操作: 如果只执行一次 SQL 语句,直接使用占位符(?)
更方便。
多次操作: 如果需要多次执行相似的 SQL 语句(如批量插入或更新),使用 Prepare
可以提高性能。
安全性: 无论使用哪种方式,参数绑定的本质是由数据库驱动安全地处理的,因此都可以防止 SQL 注入。
事务
在数据库操作中,事务(Transaction)是一组操作的集合,这些操作要么全部成功,要么全部失败,确保数据库处于一致性状态。
事务的特性 (ACID)
-
原子性 (Atomicity):
- 事务中的所有操作要么全部完成,要么全部不执行。
-
一致性 (Consistency):
- 事务完成后,数据库状态从一个一致状态转变为另一个一致状态。
-
隔离性 (Isolation):
- 一个事务的执行不能被其他事务干扰,事务之间互不影响。
-
持久性 (Durability):
- 事务完成后,其对数据库的更改是永久性的。
Go 中的事务操作
在 Go 的 database/sql
包中,可以通过以下方式使用事务:
- 开启事务:
db.Begin()
- 提交事务:
tx.Commit()
- 回滚事务:
tx.Rollback()
事务操作的基本流程
-
开启事务: 使用
db.Begin()
创建一个事务对象。 -
执行事务中的多条 SQL 操作: 使用事务对象
tx
的方法执行语句。 -
提交事务或回滚事务:
- 如果所有操作成功,调用
tx.Commit()
提交事务。 - 如果任何操作失败,调用
tx.Rollback()
回滚事务。
- 如果所有操作成功,调用
完整示例代码:
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/go-sql-driver/mysql"
)
// 定义一个全局对象 db
var db *sql.DB
// 初始化数据库连接
func initMySQLDB() error {
var err error
dsn := "user:password@tcp(127.0.0.1:3306)/bank?charset=utf8mb4&parseTime=True"
db, err = sql.Open("mysql", dsn)
if err != nil {
return fmt.Errorf("sql.Open failed: %v", err)
}
if err = db.Ping(); err != nil {
db.Close()
return fmt.Errorf("db.Ping failed: %v", err)
}
fmt.Println("Database connected successfully.")
return nil
}
// 转账操作
func transferMoney(fromAccountID, toAccountID int, amount float64) error {
// 开启事务
tx, err := db.Begin()
if err != nil {
return fmt.Errorf("begin transaction failed: %v", err)
}
// 执行操作:扣减转出账户余额
sqlStr1 := "UPDATE accounts SET balance = balance - ? WHERE id = ? AND balance >= ?"
_, err = tx.Exec(sqlStr1, amount, fromAccountID, amount)
if err != nil {
tx.Rollback() // 回滚事务
return fmt.Errorf("deduct from account failed: %v", err)
}
// 执行操作:增加转入账户余额
sqlStr2 := "UPDATE accounts SET balance = balance + ? WHERE id = ?"
_, err = tx.Exec(sqlStr2, amount, toAccountID)
if err != nil {
tx.Rollback() // 回滚事务
return fmt.Errorf("add to account failed: %v", err)
}
// 提交事务
err = tx.Commit()
if err != nil {
return fmt.Errorf("commit transaction failed: %v", err)
}
fmt.Println("Transfer successful.")
return nil
}
func main() {
// 初始化数据库
if err := initMySQLDB(); err != nil {
log.Fatalf("Database initialization failed: %v", err)
}
defer db.Close()
// 转账操作
err := transferMoney(1, 2, 100.00)
if err != nil {
log.Printf("Transfer failed: %v\n", err)
} else {
fmt.Println("Transfer completed.")
}
}
sqlx
sqlx
是 Go 社区流行的一个库,基于 Go 的标准库 database/sql
封装而成。sqlx
提供了许多增强功能,例如结构体绑定、便捷的查询操作等,使开发者更容易操作数据库。
sqlx
的优势
-
简化查询结果映射:
- 支持将查询结果直接映射到结构体,省去手动
rows.Scan
的繁琐操作。
- 支持将查询结果直接映射到结构体,省去手动
-
Named Queries:
- 支持命名参数,避免了大量占位符的混乱。
-
事务支持 :
- 简化事务处理,与
database/sql
一致。
- 简化事务处理,与
-
预加载/预处理:
- 提供便捷的批量插入和动态查询构建。
增删改成操作
可以通过go get github.com/jmoiron/sqlx
或者go mod tidy
进行下载。
示例代码:
package main
import (
"fmt"
"log"
_ "github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
)
var db *sqlx.DB
// 初始化数据库连接
func initMySQLDB() error {
dsn := "user:password@tcp(127.0.0.1:3306)/testdb?charset=utf8mb4&parseTime=True"
var err error
db, err = sqlx.Connect("mysql", dsn) // 使用 sqlx.Connect
if err != nil {
return fmt.Errorf("failed to connect to database: %w", err)
}
// 设置连接池参数
db.SetMaxOpenConns(10)
db.SetMaxIdleConns(5)
return nil
}
func main() {
err := initMySQLDB()
if err != nil {
log.Fatalf("Database connection failed: %v", err)
}
defer db.Close()
fmt.Println("Database connected successfully.")
}
在插入、更新、删除操作上,基本和前面的相同,都使用Exec
方法,传入sql语句,以及sql里面需要的参数:
func insertPerson(username, sex, email string) error {
_, err := db.Exec("INSERT INTO person (username, sex, email) VALUES (?, ?, ?)", username, sex, email)
if err != nil {
return fmt.Errorf("insert failed: %w", err)
}
return nil
}
func updatePersonEmail(id int, email string) error {
_, err := db.Exec("UPDATE person SET email = ? WHERE user_id = ?", email, id)
if err != nil {
return fmt.Errorf("update failed: %w", err)
}
return nil
}
func deletePerson(id int) error {
_, err := db.Exec("DELETE FROM person WHERE user_id = ?", id)
if err != nil {
return fmt.Errorf("delete failed: %w", err)
}
return nil
}
查询上面,可以直接通过结构体进行映射,无需自己通过Scan函数一个一个映射,与前面调用QueryRow
和Query
和不同的是,这里是Get
和 Select
方法:
type Person struct {
UserID int `db:"user_id"`
Username string `db:"username"`
Sex string `db:"sex"`
Email string `db:"email"`
}
// 单条记录
func getPersonByID(id int) (*Person, error) {
var person Person
err := db.Get(&person, "SELECT * FROM person WHERE user_id = ?", id)
if err != nil {
return nil, fmt.Errorf("query failed: %w", err)
}
return &person, nil
}
// 多条记录
func getAllPersons() ([]Person, error) {
var persons []Person
err := db.Select(&persons, "SELECT * FROM person")
if err != nil {
return nil, fmt.Errorf("query failed: %w", err)
}
return persons, nil
}
func main() {
person, err := getPersonByID(1)
if err != nil {
log.Fatalf("Failed to get person: %v", err)
}
fmt.Printf("Person: %+v\n", person)
persons, err2 := getAllPersons()
if err2 != nil {
log.Fatalf("Failed to get persons: %v", err2)
}
for _, pers := range persons {
fmt.Printf("Person: %+v\n", pers)
}
}
另外一个优势是:sqlx 支持命名参数(Named Queries),使代码更加可读:
(1)NamedExec
方法用来绑定SQL语句与结构体或map中的同名字段。
func insertPersonNamed(username, sex, email string) error {
sqlStr := "INSERT INTO person (username, sex, email) VALUES (:username, :sex, :email)"
_, err := db.NamedExec(sqlStr, map[string]interface{}{
"username": username,
"sex": sex,
"email": email,
})
if err != nil {
return fmt.Errorf("insert failed: %w", err)
}
return nil
}
(2)NamedQuery
可以将查询的参数以结构体或映射(map[string]interface{}
)的形式传递。
type Person struct {
UserID int `db:"user_id"`
Username string `db:"username"`
Sex string `db:"sex"`
Email string `db:"email"`
}
// 使用命名参数查询单条记录
func getPersonByUsername(username string) (*Person, error) {
sqlStr := "SELECT * FROM person WHERE username = :username"
rows, err := db.NamedQuery(sqlStr, map[string]interface{}{
"username": username,
})
if err != nil {
return nil, fmt.Errorf("query failed: %w", err)
}
defer rows.Close()
var person Person
if rows.Next() {
err = rows.StructScan(&person)
if err != nil {
return nil, fmt.Errorf("scan failed: %w", err)
}
}
return &person, nil
}
func main() {
person, err := getPersonByUsername("张三")
if err != nil {
log.Fatalf("Failed to get person: %v", err)
}
fmt.Printf("Person: %+v\n", person)
}
参数为结构体: 你也可以直接使用结构体传递参数(字段需要对应 SQL 中的命名参数):
func getPersonUsingStruct(p *Person) (*Person, error) {
sqlStr := "SELECT * FROM person WHERE username = :username AND email = :email"
rows, err := db.NamedQuery(sqlStr, p)
if err != nil {
return nil, fmt.Errorf("query failed: %w", err)
}
defer rows.Close()
var person Person
if rows.Next() {
err = rows.StructScan(&person)
if err != nil {
return nil, fmt.Errorf("scan failed: %w", err)
}
}
return &person, nil
}
func main() {
input := &Person{Username: "张三", Email: "zhangsan@example.com"}
person, err := getPersonUsingStruct(input)
if err != nil {
log.Fatalf("Failed to get person: %v", err)
}
fmt.Printf("Person: %+v\n", person)
}
事务
事务操作与 database/sql
基本一样:
func transferMoney(tx *sqlx.Tx, fromID, toID int, amount float64) error {
// 扣减账户余额
_, err := tx.Exec("UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, fromID)
if err != nil {
return fmt.Errorf("deduct failed: %w", err)
}
// 增加账户余额
_, err = tx.Exec("UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, toID)
if err != nil {
return fmt.Errorf("add failed: %w", err)
}
return nil
}
func main() {
tx, err := db.Beginx() // 开启事务
if err != nil {
log.Fatalf("Failed to begin transaction: %v", err)
}
err = transferMoney(tx, 1, 2, 100.00)
if err != nil {
tx.Rollback()
log.Fatalf("Transaction failed: %v", err)
}
err = tx.Commit() // 提交事务
if err != nil {
log.Fatalf("Failed to commit transaction: %v", err)
}
fmt.Println("Transaction completed successfully.")
}
sqlx.In 的使用
sqlx.In
是 sqlx
提供的一个工具函数,解决了 SQL 查询中 IN 子句传递动态参数的问题。标准库 database/sql
不支持直接传递切片作为 IN
子句的参数,而 sqlx.In
自动将切片展开为占位符并处理参数绑定。
(1)使用 sqlx.In
查询多条记录【需求: 查询多个用户的信息,用户 ID
动态传入】
func getPersonsByIDs(ids []int) ([]Person, error) {
// 使用 sqlx.In 构建 SQL 语句
query, args, err := sqlx.In("SELECT * FROM person WHERE user_id IN (?)", ids)
if err != nil {
return nil, fmt.Errorf("sqlx.In failed: %w", err)
}
// 将查询转换为数据库驱动支持的语句
query = db.Rebind(query)
var persons []Person
err = db.Select(&persons, query, args...)
if err != nil {
return nil, fmt.Errorf("query failed: %w", err)
}
return persons, nil
}
func main() {
ids := []int{1, 2, 3}
persons, err := getPersonsByIDs(ids)
if err != nil {
log.Fatalf("Failed to get persons: %v", err)
}
for _, person := range persons {
fmt.Printf("Person: %+v\n", person)
}
}
(2)sqlx.In
插入数据【需求: 批量插入用户数据,使用动态参数。】
func insertPersons(persons []Person) error {
// 构建批量插入语句和参数
values := []interface{}{}
for _, p := range persons {
values = append(values, p.Username, p.Sex, p.Email)
}
query, args, err := sqlx.In("INSERT INTO person (username, sex, email) VALUES (?, ?, ?)", values...)
if err != nil {
return fmt.Errorf("sqlx.In failed: %w", err)
}
// Rebind 将 SQL 语句转换为当前数据库驱动支持的语法
query = db.Rebind(query)
// 执行插入
_, err = db.Exec(query, args...)
if err != nil {
return fmt.Errorf("insert failed: %w", err)
}
return nil
}
func main() {
persons := []Person{
{Username: "Alice", Sex: "女", Email: "alice@example.com"},
{Username: "Bob", Sex: "男", Email: "bob@example.com"},
}
err := insertPersons(persons)
if err != nil {
log.Fatalf("Failed to insert persons: %v", err)
}
fmt.Println("Batch insert successful!")
}