简单介绍
database/sql
database/sql 是一个标准库软件包,负责与数据库(主要是 SQL 关系数据库)的连接和交互。
它为类 SQL 交互提供泛型接口、类型和方法。database/sql 在创建时将简单易用纳入考量,配置为支持与类 SQL 数据库交互所需的最基本必要功能。
为了与数据库管理系统交互,数据库软件包需要适当的驱动程序。 目前,database/sql 支持超过 50 种数据库驱动程序,涵盖 SQLite、MySQL/MariaDB、PostgreSQL、Oracle 和 MS SQL Server 等最流行的 DBMS,能够增强适配性,增强移植性。
此软件包还支持基本的 CRUD 操作、数据库事务、命名形参、返回多个结果集、可取消查询、SQL 类型支持、连接池管理、形参化查询和预备语句等功能。
尽管它支持许多基本的现代数据库功能,例如事务和预备语句,但它也有一些局限性。 例如,它存在类型限制,无法将大的 uint64 值作为形参传递给语句。
它的主要功能和特性可以总结为以下几点:
- 统一的编程接口:database/sql包通过提供一组统一的API,如Prepare(), Exec(), Query()等,使得开发人员能够以相同的方式操作不同的数据库。这大大提高了代码的可移植性和灵活性。
- 驱动支持:database/sql包本身并不直接与数据库通信,而是依赖于第三方数据库驱动程序。这些驱动程序需要实现database/sql/driver包中定义的Driver接口,并在程序初始化阶段通过sql.Register()方法注册到database/sql中。常见的关系型数据库如MySQL、PostgreSQL、Oracle、Gbase8s等都有对应的Go语言驱动程序。比如oracle的驱动mattn/go-oci8: Oracle driver for Go using database/sql (github.com)
- 连接池管理:database/sql维护了一个数据库连接池,用于管理数据库连接。当通过sql.Open()打开一个数据库连接时,database/sql会在合适的时机调用注册的驱动来创建一个具体的连接,并将其添加到连接池中。连接池会负责连接的复用、管理和维护工作,并且这是并发安全的。
- 事务支持:database/sql包还支持事务处理,可以通过Tx类型的方法如Begin(), Commit(), Rollback()等来进行事务的管理。
- 安全性:为了防止SQL注入攻击,database/sql包推荐使用预编译语句和参数化查询。这样可以确保所有的SQL语句在执行前都会被预先分析和编译,从而避免了潜在的安全问题。
sqlx
创建 sqlx 是为了扩展标准库数据库软件包的功能。 由于它依赖 database/sql 软件包,后者提供的所有功能也可用,包括对同一组数据库技术和驱动程序的支持。
除了这些核心功能之外,sqlx 还具有以下优点:
- 带有命名形参的预备语句 – 这使您能够使用结构字段的名称和映射键绑定预备语句或查询中的变量。
- 结构扫描 – 这允许您将查询结果直接扫描到单行的结构中,不必像 database/sql 那样单独扫描每个字段或列。 它还支持扫描到嵌入式结构。
- Select 和 Get – 这些是用于处理预期将多个记录或单个记录分别返回到结构的切片或单个结构的查询的便捷方法。 不需要循环结果集!
- 对 IN 查询的支持 – 这允许您将值的切片作为单个形参绑定到 IN 查询。 与将切片作为单个值处理的 database/sql 相比,切片在预期位置上以相同数量的 bindvars 展开。
命名查询 – 这会将结构字段的名称绑定到列名称,避免在向 bindvars 赋值时对列名称的位置引用。 - 无错误结果集:结果集不返回错误,允许对返回结果进行链式操作,例如将结果直接扫描到结构中。 如以下代码段所示:
var p Place
err := db.QueryRowx("SELECT city, telcode FROM place LIMIT 1").StructScan(&p)
这些只是 sqlx 软件包众多功能中的几个例子,这些功能确保了比 database/sql 更好的工作体验。
sqlc
sqlc 是一个捆绑为可执行二进制文件的 SQL 编译器,可以为原始 SQL 架构和查询生成类型安全代码。 因此,除了实际的 SQL 语句之外,您不必编写任何样板代码。
根据文档,它可以为 PostgreSQL、MySQL/MariaDB 和 SQLite 生成代码。 然而,生成的代码也适用于标准库的 SQL 软件包;因此,它可以使用所有支持的驱动程序,但不一定使用支持的数据库。 除了支持的数据库和驱动程序之外,以下是它的一些其他功能:
- 查询注解 – 这些注解允许您为每个查询定义函数的名称以及预期的查询结果类型。 它们在代码生成期间用于确定函数的名称和签名。
- JSON 标记 – sqlc 支持为将被编组并作为 JSON 发送给客户端的结构或类型生成 JSON 标记。
- 架构修改 – sqlc 支持读取各种格式的迁移文件以修改架构并生成代码来反映这些更改。
- 结构命名 – sqlc 提供用于从表名称生成结构名称的命名方案的选项。
如果您擅长 SQL 语句,并且不喜欢使用太多代码执行数据库操作和处理数据,那么这个软件包绝对适合您。
database/sql
首先需要连接与defer关闭数据库
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql" // 导入MySQL驱动
"log"
)
func main() {
// 打开数据库连接
db, err := sql.Open("mysql", "username:password@tcp(localhost:3306)/dbname")
if err != nil {
log.Fatal(err) //or panic
}
defer db.Close()
// 执行你想要的操作
}
后续可单独封装出db对象
需要关闭数据库!
Go的database/sql包在设置了连接池后,仍然需要关闭连接。尽管连接池的设计目的是为了复用数据库连接,提高性能并避免频繁地建立和断开连接,但在使用完数据库连接后,应该显式地关闭它。这是因为关闭连接并不是真正意义上的断开与数据库的TCP连接,而是将连接返回到连接池中,以便其他请求可以复用。如果不关闭连接,连接将会一直被占用,可能导致连接池中的连接被耗尽
测连接是否能ping通
err = db.Ping()
if err != nil {
panic(err)
}
连接池设置
在正常情况下,database/sql的连接池会维护一定数量的活跃和空闲连接。
SetMaxIdleConns()
用于设置连接池中空闲连接的最大数量。
SetMaxOpenConns()
用于设置到数据库的同时最大打开连接数。
SetConnMaxLifetime()
可以设置连接的最大生命周期,超过这个时间,连接将被关闭并从池中移除。
插入、删除、更新—— Exec
以下代码段演示了如何使用带有 MySQL 驱动程序的 database/sql 软件包插入记录:
func addStudent(s Student) (int64, error){
query := "insert into students (fname, lname, date_of_birth, email, gender, address) values (?, ?, ?, ?, ?, ?);"
result, err := db.Exec(query, s.Fname,s.Lname, s.DateOfBirth, s.Email, s.Gender, s.Address)
if err != nil {
return 0, fmt.Errorf("addStudent Error: %v", err)
}
id, err := result.LastInsertId()
if err != nil {
return 0, fmt.Errorf("addStudent Error: %v", err)
}
return id, nil
}
正如您所看到的,插入操作与编写直接 SQL 语句非常相似。 您还将需要分别输入每个字段及其关联值。 然而,随着时间推移,在大型结构或复杂类型中维护代码会变得很麻烦,增加引入错误的机会,而这些错误可能只能在运行时被捕获。
查询 Query与QueryRow
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql" // 导入MySQL驱动
"log"
)
func main() {
// 打开数据库连接
db, err := sql.Open("mysql", "username:password@tcp(localhost:3306)/dbname")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// 执行查询
rows, err := db.Query("SELECT id, name FROM users WHERE active = ? and deleted= ?", 1,0)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
// 遍历查询结果
for rows.Next() {
var this_is_id int
var this_is_name string
//Scan会按照顺序将select中的值赋值给括号中的变量
if err := rows.Scan(&this_is_id, &this_is_name); err != nil {
log.Fatal(err)
}
//打印出来
fmt.Printf("ID: %d, Name: %s\n", this_is_id, this_is_name)
}
// 检查遍历是否出现错误
if err := rows.Err(); err != nil {
log.Fatal(err)
}
}
在这个示例中,我们使用了Go的database/sql标准库和MySQL驱动。首先,我们使用sql.Open()建立了与数据库的连接。然后,我们使用db.Query()执行了一个查询,查询活跃用户的id和name。最后,我们通过rows.Next()遍历结果集,并通过rows.Scan()将结果存入变量。如果在处理过程中发生错误,我们记录日志并退出程序。
或者还可以将其保存在数组、map或者结构体数组等中(通过append或者直接赋值等方法) 比如
func fetchStudents() ([]Student, error) {
var students []Student
rows, err := db.Query("SELECT * FROM students")
if err != nil {
return nil, fmt.Errorf("fetchStudents %v", err)
}
defer rows.Close()
for rows.Next() {
var s Student
if err := rows.Scan(&s.ID, &s.Fname, &s.Lname, &s.DateOfBirth, &s.Email, &s.Address, &s.Gender ); err != nil {
return nil, fmt.Errorf("fetchStudents %v", err)
}
students = append(students, s)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("fetchStudents %v", err)
}
return students, nil
}
在上面的代码段中,获取记录后对其循环,