Go 标准库之 time

database/sql 包提供了保证SQL或类SQL数据库的泛用接口,常用的数据库驱动有 github.com/go-sql-driver/mysql

https://pkg.go.dev/database/sql

1、初始化数据库连接

首先得初始化一个数据库连接,我们可以通过 sql.Open 来连接数据库:

1
2
3
4
5
DB, err := sql.Open("mysql", "root:123456@tcp(127.0.0.1:3306)/test")
if err != nil {
fmt.Println("Error opening DB:", err)
return
}

"root:123456@tcp(127.0.0.1:3306)/test" 包含了数据库用户名、密码、MySQL 服务器地址、端口以及要连接的数据库名称。注意,如果你的密码是空的,那记得跳过密码部分。

但这里只是打开了一个数据库连接。为了让连接更加高效,我们要配置一些连接池的参数,比如最大连接数、最大空闲连接数和连接的最大生命周期:

1
2
3
DB.SetConnMaxLifetime(time.Minute * 5)  // 连接最大生命周期  
DB.SetMaxOpenConns(50) // 最大连接数
DB.SetMaxIdleConns(10) // 最大空闲连接数

我个人建议设置合理的连接池大小,避免连接过多导致 MySQL 服务器压力过大。连接池的大小根据你的系统负载来调节,不要让数据库连接成为瓶颈。

最后,检查一下是否能成功连接到数据库:

1
2
3
4
5
if err := DB.Ping(); err != nil {  
fmt.Println("Connection failed:", err)
return
}
fmt.Println("Connected successfully")

Ping() 方法会尝试与数据库建立连接,如果失败就直接报错。至此,数据库连接初始化完成,接下来就可以进行数据查询和操作了。

2、查询

Go 提供了 DB.QueryDB.QueryRow 两种方法来查询数据。

DB.Query

DB.Query 用于查询多行数据,返回的是一个 *Rows 类型的对象,你需要通过 rows.Next() 来循环遍历每一行数据,然后使用 rows.Scan 将当前行的数据扫描到相应的变量中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
rows, err := DB.Query("SELECT id, name FROM users WHERE age > ?", 18)  
if err != nil {
fmt.Println("Query failed:", err)
return
}
defer rows.Close() // 确保在使用完毕后关闭资源

for rows.Next() {
var id int
var name string
if err := rows.Scan(&id, &name); err != nil {
fmt.Println("Scan failed:", err)
return
}
fmt.Printf("ID: %d, Name: %s\n", id, name)
}

这里的 rows.Scan 就是把查询结果扫描到变量中,注意传递的是变量的地址。如果你在这步出现了 Scan 错误,通常是查询字段的类型和数据库字段不匹配,或者是查询结果为 NULL

DB.QueryRow

DB.QueryRow 用于查询单行数据,返回的是一个单独的 Row 对象。和 Query 不同,它只返回一行结果,适合用于查询根据条件唯一的数据。

如果没有找到结果,它不会返回 nil,而是返回一个实现了 Row 接口的非空值。

在调用 QueryRow() 后,通常会使用 Scan() 方法从结果中提取数据。Scan() 方法负责将结果的列映射到相应的变量,并在需要时触发底层查询的执行。在调用 Scan() 之前,QueryRow() 不会执行实际的查询。

1
2
3
4
5
6
7
var user User  
err := DB.QueryRow("SELECT id, name FROM users WHERE id = ?", 1).Scan(&user.ID, &user.Name)
if err != nil {
fmt.Println("QueryRow failed:", err)
return
}
fmt.Printf("User: %+v\n", user)

3、增、删、改

Go 提供了 Exec 方法来执行增、删、改等操作。这些操作不会返回行数据,而是返回一个 Result 类型

你可以通过 RowsAffected 来查看受影响的行数,如果是新增,可以使用 LastInsertId() 获取最后插入的 ID,通常在表中有一个自增的主键时使用

有两种方式

  • 一种是直接使用 db.Exec()
  • 一种是使用预编译的方式
    • 把SQL语句分成两部分,命令部分与数据部分。
    • 先把命令部分发送给MySQL服务端,MySQL服务端进行SQL预处理。
    • 然后把数据部分发送给MySQL服务端,MySQL服务端对SQL语句进行占位符替换。
    • MySQL 服务端执行完整的SQL语句并将结果返回给客户端。

直接使用 db.Exec()`

插入

1
2
3
4
sqlStr := "insert into user(name,age) values (?,?)"
ret, err := db.Exec(sqlStr, "王五", 18)
theID, err := ret.LastInsertId()
fmt.Println("insert success,the id is", theID)

更新

1
2
3
4
sqlStr := "update user set age=? where id=?"
ret, err := db.Exec(sqlStr, 100, 5)
n, err := ret.RowsAffected() // 操作影响的行数
fmt.Printf("update success, affected rows:%d\n", n)

删除

1
2
3
4
sqlStr := "delete from user where id=?"
ret, err := db.Exec(sqlStr, 5)
n, err := ret.RowsAffected()
fmt.Printf("delete success,affected rows:%d\n", n)

使用预编译

插入

1
2
3
4
5
6
7
8
9
10
11
12
stmt, err := DB.Prepare("INSERT INTO users(name, age) VALUES(?, ?)")  
if err != nil {
fmt.Println("Prepare failed:", err)
return
}
res, err := stmt.Exec("John Doe", 30)
if err != nil {
fmt.Println("Exec failed:", err)
return
}
lastID, _ := res.LastInsertId() // 使用 ret.LastInsertId() 获取最后插入的 ID,通常在表中有一个自增的主键时使用
fmt.Printf("Inserted user with ID: %d\n", lastID)

更新

1
2
3
4
5
6
7
8
9
10
11
12
stmt, err := DB.Prepare("UPDATE users SET age=?")  
if err != nil {
fmt.Println("Prepare failed:", err)
return
}
res, err := stmt.Exec(30)
if err != nil {
fmt.Println("Exec failed:", err)
return
}
lastID, _ := res.RowsAffected() // 操作影响的行数
fmt.Printf("update success, affected rows:%d\n", n)

1
2
3
4
5
6
7
8
9
10
11
12
stmt, err := DB.Prepare("DELETE FROM users WHERE id = ?")  
if err != nil {
fmt.Println("Prepare failed:", err)
return
}
res, err := stmt.Exec(1) // 删除ID为1的用户
if err != nil {
fmt.Println("Exec failed:", err)
return
}
affected, _ := res.RowsAffected()
fmt.Printf("Deleted %d rows\n", affected)

4、事务处理

在执行多个数据库操作时,事务处理是保证数据一致性的重要手段。Go 的 sql.DBsql.Tx 都提供了非常方便的事务支持。常见的操作是:Begin 开始事务,Commit 提交事务,Rollback 回滚事务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
tx, err := DB.Begin()  
if err != nil {
fmt.Println("Begin transaction failed:", err)
return
}
_, err = tx.Exec("UPDATE users SET age = ? WHERE id = ?", 35, 1)
if err != nil {
tx.Rollback() // 如果发生错误,回滚事务
fmt.Println("Exec failed:", err)
return
}
err = tx.Commit() // 提交事务
if err != nil {
fmt.Println("Commit failed:", err)
return
}
fmt.Println("Transaction committed successfully")

5、完整示例

通过之前的拆解,我们已经掌握了如何优雅地与数据库交互。现在来个完整的示例,看看我们如何在一个简单的应用中完成初始化、查询、插入、删除操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
func main() {  
// 初始化数据库连接
DB, err := sql.Open("mysql", "root:123456@tcp(127.0.0.1:3306)/test")
if err != nil {
fmt.Println("Error opening DB:", err)
return
}
defer DB.Close()

DB.SetConnMaxLifetime(time.Minute * 5)
DB.SetMaxOpenConns(50)
DB.SetMaxIdleConns(10)

// 测试数据库连接
if err := DB.Ping(); err != nil {
fmt.Println("Connection failed:", err)
return
}
fmt.Println("Connected successfully")

// 查询数据
rows, _ := DB.Query("SELECT id, name FROM users")
defer rows.Close()

for rows.Next() {
var id int
var name string
rows.Scan(&id, &name)
fmt.Printf("ID: %d, Name: %s\n", id, name)
}

// 插入数据
stmt, _ := DB.Prepare("INSERT INTO users(name, age) VALUES(?, ?)")
stmt.Exec("Jane Doe", 28)

// 删除数据
stmt, _ = DB.Prepare("DELETE FROM users WHERE id = ?")
stmt.Exec(1)
}

Go 标准库之 time
https://flepeng.github.io/021-Go-32-Go-标准库-Go-标准库之-database-sql/
作者
Lepeng
发布于
2024年12月3日
许可协议