【Go学习】-01-6-数据库泛型新特性

发布于:2025-02-11 ⋅ 阅读:(47) ⋅ 点赞:(0)


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事务特性:

  1. 原子性
  2. 一致性
  3. 隔离性
  4. 持久性
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 非泛型函数

两个 不同类型的映射:一种用于存储值,一种用于存储值。int64float64

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 中列出它们的路径。

假设你有两个模块 moduleAmoduleB,它们位于不同的文件夹中,你可以在根目录下创建一个 go.work 文件,将这两个模块包括在内:

go 1.23

use (
    ./moduleA
    ./moduleB
)

这将指示 Go 使用 moduleAmoduleB 两个模块进行构建。

一旦设置了 go.work 文件,Go 命令会自动识别工作空间并处理模块之间的依赖关系。例如,你可以使用 go buildgo 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 类型来定义。

基本步骤:

  1. 创建模糊测试函数:在测试代码中定义一个模糊测试函数,函数的参数是一个类型为 testing.F 的对象,代表模糊测试框架。
  2. 编写测试逻辑:在模糊测试函数内部,编写逻辑来处理模糊输入并验证输出。
  3. 运行测试:使用 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
    

网站公告

今日签到

点亮在社区的每一天
去签到