【Go学习】-01-6-数据库泛型新特性
- 1 数据库操作
- 1.1 操作mysql
- 1.1.1 Insert
- 1.1.2 Select
- 1.1.3 Update
- 1.1.4 Delete
- 1.1.5 sql事务
- 1.2 go操作Redis
- 2 泛型
- 2.1 非泛型函数
- 2.2 泛型函数
- 2.3 泛型类型
- 2.3.1 泛型结构体
- 2.3.2 泛型接口
- 2.4 泛型约束
- 2.5 泛型切片和映射
- 2.5.1 泛型切片
- 2.5.2 泛型映射
- 2.6 泛型实际应用
- 3 workspace
- 3.1 概念
- 3.2 workspace案例
- 4 模糊测试
- 4.1概念
- 4.2 如何使用模糊测试
- 4.3 模糊测试常见用法
- 4.4 自定义输入生成
1 数据库操作
1.1 操作mysql
创建go_learn数据库后创建user表
CREATE TABLE `user` (
`user_id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(255) DEFAULT NULL,
`sex` varchar(255) DEFAULT NULL,
`email` varchar(255) DEFAULT NULL,
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
mysql的前置知识,我们这里就不讲了,可自行去学习mysql教程
1.1.1 Insert
首先,需要引入mysql驱动
通过go get github.com/go-sql-driver/mysql@v1.6.0引入依赖
_ "github.com/go-sql-driver/mysql"
我们的数据库地址是192.168.101.68:3306
用户名:root
密码:mysql
插入一条记录:名字bblb,性别man,邮箱bblb123456789@qq.com
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
"log"
"time"
)
var DB *sql.DB
func init() {
db, err := sql.Open("mysql", "root:mysql@tcp(192.168.101.68:3306)/go_learn")
if err != nil {
panic(err)
}
//最大空闲连接数,默认不配置,是2个最大空闲连接
db.SetMaxIdleConns(5)
//最大连接数,默认不配置,是不限制最大连接数
db.SetMaxOpenConns(100)
// 连接最大存活时间
db.SetConnMaxLifetime(time.Minute * 3)
//空闲连接最大存活时间
db.SetConnMaxIdleTime(time.Minute * 1)
err = db.Ping()
if err != nil {
log.Println("数据库连接失败")
db.Close()
panic(err)
}
DB = db
}
func save() {
r, err := DB.Exec("insert into user (username,sex,email) values(?,?,?)", "bblb", "man", "bblb123456789@qq.com")
if err != nil {
log.Println("执行sql语句出错")
panic(err)
}
id, err := r.LastInsertId()
if err != nil {
panic(err)
}
fmt.Println("插入成功:", id)
}
func main() {
defer DB.Close()
save()
}
查看数据库
1.1.2 Select
type User struct {
UserId int `db:"user_id"`
Username string `db:"username"`
Sex string `db:"sex"`
Email string `db:"email"`
}
func query(id int) (*User,error) {
rows, err := DB.Query("select * from user where user_id=? limit 1", id)
if err != nil{
log.Println("查询出现错误:",err)
return nil,errors.New(err.Error())
}
user := new(User)
for rows.Next() {
if err := rows.Scan(&user.UserId,&user.Username,&user.Sex,&user.Email); err != nil{
log.Println("scan error:",err)
return nil,errors.New(err.Error())
}
}
return user,nil
}
func main() {
defer DB.Close()
//save()
user,err := query(2)
if err != nil{
log.Println("查询出现错误:",err)
return
}
fmt.Printf("查询成功:%+v\n",user)
}
可以看到我们刚刚插入的id是2所以查2
查询成功:&{UserId:2 Username:bblb Sex:man Email:bblb123456789@qq.com}
1.1.3 Update
func update(username string, id int) {
ret, err := DB.Exec("update user set username=? where user_id=?", username, id)
if err != nil {
log.Println("更新出现问题:",err)
return
}
affected, _ := ret.RowsAffected()
fmt.Println("更新成功的行数:",affected)
}
1.1.4 Delete
func delete(id int) {
ret, err := DB.Exec("delete from user where user_id=?", id)
if err != nil {
log.Println("删除出现问题:",err)
return
}
affected, _ := ret.RowsAffected()
fmt.Println("删除成功的行数:",affected)
}
1.1.5 sql事务
mysql事务特性:
- 原子性
- 一致性
- 隔离性
- 持久性
func insertTx(username string) {
tx, err := DB.Begin()
if err != nil {
log.Println("开启事务错误:",err)
return
}
ret, err := tx.Exec("insert into user (username,sex,email) values (?,?,?)", username, "man", "test@test.com")
if err != nil {
log.Println("事务sql执行出错:",err)
return
}
id, _ := ret.LastInsertId()
fmt.Println("插入成功:",id)
if username == "lisi" {
fmt.Println("回滚...")
_ = tx.Rollback()
}else {
_ = tx.Commit()
}
}
1.2 go操作Redis
redis不另行介绍,默认会,如果不了解,先去学习redis教程
安装:go get github.com/go-redis/redis/v8
package main
import (
"context"
"fmt"
"github.com/go-redis/redis/v8"
)
func main() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "192.168.101.68:6379",
Password: "redis", // no password set
DB: 0, // use default DB
})
err := rdb.Set(ctx, "key", "value", 0).Err()
if err != nil {
panic(err)
}
val, err := rdb.Get(ctx, "key").Result()
if err != nil {
panic(err)
}
fmt.Println("key", val)
val2, err := rdb.Get(ctx, "key2").Result()
if err == redis.Nil {
fmt.Println("key2 does not exist")
} else if err != nil {
panic(err)
} else {
fmt.Println("key2", val2)
}
}
2 泛型
2.1 非泛型函数
两个 不同类型的映射:一种用于存储值,一种用于存储值。int64
和float64
package main
import "fmt"
// SumInts adds together the values of m.
func SumInts(m map[string]int64) int64 {
var s int64
for _, v := range m {
s += v
}
return s
}
// SumFloats adds together the values of m.
func SumFloats(m map[string]float64) float64 {
var s float64
for _, v := range m {
s += v
}
return s
}
func main() {
// Initialize a map for the integer values
ints := map[string]int64{
"first": 34,
"second": 12,
}
// Initialize a map for the float values
floats := map[string]float64{
"first": 35.98,
"second": 26.99,
}
fmt.Printf("Non-Generic Sums: %v and %v\n",
SumInts(ints),
SumFloats(floats))
}
针对不同的类型我们都需要写对应的函数来进行求和,这是非常麻烦的
2.2 泛型函数
Go 1.18 引入了泛型(Generics),这是 Go 语言的一项重大更新。通过泛型,Go 开发者可以编写更通用、更可复用的代码,而不需要手动编写多个类型的重复代码。
为了支持这一点,将编写一个函数,在添加到其普通函数参数中。这些类型参数使 function generic,使其能够处理不同类型的参数。将使用类型参数和普通函数参数调用函数。
package main
import "fmt"
// 定义一个泛型函数,接受一个类型参数 T
func Print[T any](value T) {
fmt.Println(value)
}
func main() {
Print(123) // 输出: 123
Print("hello") // 输出: hello
}
T
是类型参数,any
是 Go 1.18 中的类型约束,表示可以是任何类型。
在函数 Print[T any](value T)
中,T
是类型参数,表示 value
参数的类型。
可以为类型参数指定约束,使其只能是某些特定的类型。例如,限制类型参数只能是整数类型:
package main
import "fmt"
// 定义一个泛型函数,限制 T 类型为整型(int, int32, int64)
func Sum[T int | int32 | int64](a, b T) T {
return a + b
}
func main() {
fmt.Println(Sum(1, 2)) // 输出: 3
fmt.Println(Sum(int32(3), int32(4))) // 输出: 7
}
2.3 泛型类型
2.3.1 泛型结构体
泛型不仅可以用于函数,也可以用于结构体和接口。
package main
import "fmt"
// 定义一个泛型结构体,类型参数 T
type Pair[T any] struct {
First T
Second T
}
func main() {
// 使用泛型结构体,传入 int 类型
pair1 := Pair[int]{First: 1, Second: 2}
fmt.Println(pair1) // 输出: {1 2}
// 使用泛型结构体,传入 string 类型
pair2 := Pair[string]{First: "hello", Second: "world"}
fmt.Println(pair2) // 输出: {hello world}
}
在这个例子中,Pair[T any]
是一个泛型结构体,T
代表结构体字段的类型。
2.3.2 泛型接口
package main
import "fmt"
// 定义一个泛型接口,支持多种数值类型(int, float64)
type Adder[T int | float64] interface {
Add(a, b T) T
}
// 泛型类型:支持任意数值类型
type NumberAdder[T int | float64] struct{}
func (na NumberAdder[T]) Add(a, b T) T {
return a + b
}
func main() {
// 使用 NumberAdder 支持 int 类型
intAdder := NumberAdder[int]{}
fmt.Println(intAdder.Add(3, 4)) // 输出: 7
// 使用 NumberAdder 支持 float64 类型
floatAdder := NumberAdder[float64]{}
fmt.Println(floatAdder.Add(3.0, 4.0)) // 输出: 7.0
}
在这里,Adder[T any]
是一个泛型接口,NumberAdder
实现了这个接口。
2.4 泛型约束
泛型支持类型约束,用于指定类型参数的合法类型范围。类型约束通过 interface{}
或更具体的接口来实现。
Go 1.18 提供了一些内置的类型约束,如 any
(表示任何类型)和 comparable
(表示可以进行比较的类型)。
package main
import "fmt"
// 定义一个泛型函数,约束 T 为可比较类型
func Compare[T comparable](a, b T) bool {
return a == b
}
func main() {
fmt.Println(Compare(1, 1)) // 输出: true
fmt.Println(Compare("a", "b")) // 输出: false
// fmt.Println(Compare([]int{1}, []int{1})) // 编译错误: slices are not comparable
}
在这个例子中,T comparable
限制了类型参数 T
必须是可以进行比较的类型。
2.5 泛型切片和映射
泛型在 Go 中也支持切片(slices)和映射(maps)等常见数据结构。
2.5.1 泛型切片
package main
import "fmt"
func PrintSlice[T any](s []T) {
for _, v := range s {
fmt.Println(v)
}
}
func main() {
PrintSlice([]int{1, 2, 3}) // 输出: 1 2 3
PrintSlice([]string{"a", "b"}) // 输出: a b
}
2.5.2 泛型映射
package main
import "fmt"
// 泛型映射,支持任意类型作为键和值
func PrintMap[K comparable, V any](m map[K]V) {
for k, v := range m {
fmt.Println(k, v)
}
}
func main() {
m1 := map[string]int{"a": 1, "b": 2}
PrintMap(m1) // 输出: a 1 b 2
m2 := map[int]string{1: "one", 2: "two"}
PrintMap(m2) // 输出: 1 one 2 two
}
2.6 泛型实际应用
Go 中可以用于实现许多常见的算法和数据结构,如链表、栈、队列等。
package main
import "fmt"
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(item T) {
s.items = append(s.items, item)
}
func (s *Stack[T]) Pop() T {
if len(s.items) == 0 {
panic("stack is empty")
}
item := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return item
}
func main() {
stack := &Stack[int]{}
stack.Push(1)
stack.Push(2)
fmt.Println(stack.Pop()) // 输出: 2
fmt.Println(stack.Pop()) // 输出: 1
}
3 workspace
go 1.18 引入了 workspace
功能,旨在简化多个模块(module)的管理和开发。workspace
允许你在一个工作空间中同时管理多个 Go 模块,这对于开发大型项目或依赖多个模块时非常有用。
3.1 概念
工作空间(workspace)是 Go 1.18 引入的一个新概念,它可以包含多个 Go 模块。这样,你可以在同一个目录下处理多个模块(module),而无需通过 $GOPATH
来管理它们。
在 Go 1.18 版本中,你需要通过创建一个名为 go.work
的文件来启用工作空间。这个文件定义了工作空间内的 Go 模块及其路径。
3.2 workspace案例
工作空间文件 go.work
用来指定工作空间中包含的模块。例如,如果你有多个模块在不同的目录中,可以在 go.work
中列出它们的路径。
假设你有两个模块 moduleA
和 moduleB
,它们位于不同的文件夹中,你可以在根目录下创建一个 go.work
文件,将这两个模块包括在内:
go 1.23
use (
./moduleA
./moduleB
)
这将指示 Go 使用 moduleA
和 moduleB
两个模块进行构建。
一旦设置了 go.work
文件,Go 命令会自动识别工作空间并处理模块之间的依赖关系。例如,你可以使用 go build
或 go run
命令时,Go 会自动处理跨模块依赖。
例如,运行 go run .
时,Go 会处理 go.work
文件并且可以跨模块找到所需的依赖,而不需要单独执行每个模块的命令。
假设你有以下文件结构:
/workspace
go.work
/moduleA
go.mod
main.go
/moduleB
go.mod
main.go
go.work
文件:
go 1.23
use (
./moduleA
./moduleB
)
moduleA/go.mod
文件:
module moduleA
go 1.23
moduleB/go.mod
文件:
module moduleB
go 1.23
moduleB/utilsB
文件:
package moduleB
import "fmt"
func HelloB() {
fmt.Println("HelloB")
}
moduleA/main
文件:
package main
import "moduleB"
func main() {
moduleB.HelloB()
}
在根目录 /workspace
下运行 go run
或其他 Go 命令。
Go 会处理 go.work
中列出的模块路径,并根据需求解析、构建或运行所有模块。
Go 1.18 引入的工作空间功能使得多模块管理变得更为简单,适用于大型项目或需要协调多个模块的开发场景。通过创建 go.work
文件,可以让 Go 项目更好地管理跨模块依赖,同时提高开发效率和可维护性。
4 模糊测试
Go 1.18 引入了 模糊测试(Fuzzing)功能,它是一种自动化的测试技术,用于发现程序中的潜在缺陷和安全漏洞。模糊测试通过自动生成大量的随机输入数据来测试程序的健壮性,帮助开发者发现代码中的异常行为、崩溃、内存泄漏等问题。
在 Go 1.18 中,模糊测试被集成到了标准库中,你可以直接在 Go 中进行模糊测试,而不需要额外的工具或库。
4.1概念
模糊测试是一种通过向程序输入大量随机、无意义的(或者故意设计的异常的)数据来检测程序潜在漏洞的技术。它主要用于:
- 发现代码中的边界情况、崩溃、未处理的异常等。
- 测试程序对异常输入的处理能力,增强程序的健壮性。
- 通过大量随机的输入数据测试算法、输入验证、错误处理等方面。
4.2 如何使用模糊测试
Go 1.18 引入了对模糊测试的内置支持,允许通过 testing
包来编写模糊测试函数。模糊测试的函数以 Fuzz
开头,使用 testing
包中的 Fuzz
类型来定义。
基本步骤:
- 创建模糊测试函数:在测试代码中定义一个模糊测试函数,函数的参数是一个类型为
testing.F
的对象,代表模糊测试框架。 - 编写测试逻辑:在模糊测试函数内部,编写逻辑来处理模糊输入并验证输出。
- 运行测试:使用
go test
命令来运行模糊测试。
示例:
假设我们有一个函数 Add
,它简单地将两个整数相加。
package main
import "fmt"
func Add(a, b int) int {
return a + b
}
我们可以编写一个模糊测试函数来测试 Add
函数。
定义模糊测试函数:
package main
import (
"testing"
)
func FuzzAdd(f *testing.F) {
// 预设一些初始的模糊测试用例
f.Add(1, 2)
f.Add(3, 4)
// 模糊测试逻辑
f.Fuzz(func(t *testing.T, a, b int) {
result := Add(a, b)
// 简单验证:返回的结果是否为预期
if result != a+b {
t.Errorf("Add(%d, %d) = %d; want %d", a, b, result, a+b)
}
})
}
在这个示例中,我们使用 f.Add
来添加初始的输入值,之后通过 f.Fuzz
来生成随机的输入对,并使用 t.Errorf
来报告结果是否符合预期。
运行模糊测试:
你可以通过 go test
来运行模糊测试。
go test -fuzz=FuzzAdd
此命令会开始执行模糊测试并生成随机的测试输入来执行 Add
函数。Go 会根据生成的输入验证函数行为是否正确。
模糊测试参数
f.Add
: 用于为模糊测试提供初始输入。这些输入会在测试过程中作为种子,基于这些种子数据,Go 会生成更多的随机数据。f.Fuzz
: 用于定义实际的模糊测试逻辑。这里你可以编写逻辑来处理模糊输入并进行断言。
4.3 模糊测试常见用法
模糊测试特别适用于以下几种场景:
- 函数边界情况测试:例如,测试字符串处理函数是否能正确处理空字符串、特殊字符、非常长的字符串等。
- 错误处理验证:验证程序在接收到不合法输入时是否会崩溃或产生错误。
- 性能和压力测试:通过给定极限的输入来检查程序在边界条件下的表现。
4.4 自定义输入生成
Go 的模糊测试功能允许开发者对生成的输入进行更细粒度的控制。例如,你可以为模糊测试提供自定义的输入生成器,来专门生成特定类型的测试数据。
示例:自定义输入生成
func FuzzCustomInput(f *testing.F) {
f.Add([]byte("initial input"))
f.Fuzz(func(t *testing.T, data []byte) {
if len(data) > 100 {
t.Errorf("input data too long: %v", data)
}
})
}
运行模糊测试的其他选项
-
-fuzztime
: 设置运行模糊测试的时间限制。默认情况下,模糊测试会一直运行直到手动停止,可以通过-fuzztime
参数来控制运行时长。go test -fuzz=FuzzAdd -fuzztime=30s
这将限制模糊测试的运行时间为 30 秒。
-
-fuzzminimize
: 尝试最小化产生错误的测试用例,这样可以帮助开发者快速定位问题。go test -fuzz=FuzzAdd -fuzzminimize