跟CodeX学golang(二)- 范型

我们换一种方式理解 Go 泛型。

不要一开始陷入 constraint~interface 这些概念。

先抓住一句话:

泛型就是:把"类型"变成一个变量,让一份代码可以适配多种类型。

普通变量:

go 复制代码
func add(a int, b int) int {
	return a + b
}

这里:

arduino 复制代码
a 的类型 = int
b 的类型 = int

类型写死了。

泛型:

css 复制代码
func add[T int | float64](a T, b T) T {
	return a + b
}

这里:

r 复制代码
T 是一个类型变量

调用时:

csharp 复制代码
add[int](1,2)

相当于:

go 复制代码
func add(a int,b int) int

调用:

scss 复制代码
add[float64](1.1,2.2)

相当于:

go 复制代码
func add(a float64,b float64) float64

一份代码,生成多个版本。


一、先看一个没有泛型的痛点

假设我们写一个最大值函数。

int 版本

go 复制代码
func MaxInt(a int,b int) int {

	if a>b {
		return a
	}

	return b
}

使用:

less 复制代码
fmt.Println(MaxInt(10,20))

输出:

复制代码
20

现在需要 float:

go 复制代码
func MaxFloat(a float64,b float64) float64 {

	if a>b {
		return a
	}

	return b
}

字符串:

go 复制代码
func MaxString(a string,b string) string {

	if a>b {
		return a
	}

	return b
}

三个函数:

复制代码
MaxInt
MaxFloat
MaxString

逻辑完全一样。

区别只有:

go 复制代码
int
float64
string

这就是泛型解决的问题。


二、第一个泛型例子

改成:

css 复制代码
func Max[T int | float64 | string](
	a T,
	b T,
) T {

	if a>b {
		return a
	}

	return b
}

这里:

r 复制代码
T

就是一个未知类型。

意思:

T 可以是 int、float64、string。

使用:

int

arduino 复制代码
fmt.Println(
	Max[int](10,20),
)

编译器理解:

go 复制代码
func Max(
	a int,
	b int,
) int

float

css 复制代码
fmt.Println(
	Max[float64](1.5,2.5),
)

理解:

go 复制代码
func Max(
	a float64,
	b float64,
) float64

string

c 复制代码
fmt.Println(
	Max[string](
		"hello",
		"world",
	),
)

三、为什么需要 [T]

你可能疑惑:

为什么写:

css 复制代码
Max[T]

而不是:

scss 复制代码
Max()

因为 Go 是静态类型语言。

编译器需要提前知道:

css 复制代码
a>b

是否合法。

比如:

r 复制代码
func Max[T any](a T,b T)T{

	return a
}

这里:

r 复制代码
T 可以是任何东西

例如:

arduino 复制代码
struct{}

那么:

css 复制代码
a>b

就非法。

所以需要告诉编译器:

go 复制代码
T int | float64 | string

这些类型支持比较。


四、理解 T:它不是变量,是类型变量

普通变量:

go 复制代码
name := "张三"

保存数据。

泛型 T:

go 复制代码
func Test[T any]()

保存类型。

例如:

调用:

scss 复制代码
Test[int]()

那么:

ini 复制代码
T = int

调用:

scss 复制代码
Test[string]()

那么:

ini 复制代码
T = string

类似:

ini 复制代码
普通变量:

x = 10


泛型:

T = int

五、泛型最简单用途:写通用工具

例如判断数组是否包含某个元素。

以前:

整数:

go 复制代码
func ContainsInt(
	arr []int,
	target int,
)bool{

	for _,v:=range arr{

		if v==target {
			return true
		}
	}

	return false
}

字符串:

go 复制代码
func ContainsString(
	arr []string,
	target string,
)bool

重复。

泛型:

go 复制代码
func Contains[T comparable](
	arr []T,
	target T,
)bool{

	for _,v:=range arr{

		if v==target {
			return true
		}

	}

	return false
}

使用:

整数:

scss 复制代码
Contains(
	[]int{1,2,3},
	2,
)

字符串:

c 复制代码
Contains(
	[]string{
		"Go",
		"Java",
	},
	"Go",
)

六、comparable 是什么?

这里:

r 复制代码
T comparable

表示:

T 必须支持:

diff 复制代码
==
!=

例如:

支持:

c 复制代码
int

string

bool

不支持:

python 复制代码
slice

map

因为:

arduino 复制代码
[]int{1} == []int{1}

Go 不允许。


七、泛型结构体

不仅函数可以泛型。

比如:

普通队列

只能存 int:

go 复制代码
type Queue struct {

	data []int

}

如果想存:

c 复制代码
int
string
User
Device

怎么办?

以前:

复制:

复制代码
QueueInt

QueueString

QueueUser

泛型:

r 复制代码
type Queue[T any] struct {

	data []T

}

现在:

整数队列:

css 复制代码
var q Queue[int]

等价:

arduino 复制代码
data []int

字符串队列:

css 复制代码
var q Queue[string]

等价:

c 复制代码
data []string

用户队列:

go 复制代码
type User struct {
	Name string
}


var q Queue[User]

等价:

css 复制代码
data []User

八、完整泛型 Queue 示例

go 复制代码
package main

import "fmt"


type Queue[T any] struct {

	items []T

}


func (q *Queue[T]) Push(
	value T,
){

	q.items =
		append(
			q.items,
			value,
		)
}



func (q *Queue[T]) Pop() T {

	value :=
		q.items[0]


	q.items =
		q.items[1:]


	return value
}



func main(){


	intQueue :=
		Queue[int]{}


	intQueue.Push(100)
	intQueue.Push(200)


	fmt.Println(
		intQueue.Pop(),
	)



	stringQueue :=
		Queue[string]{}


	stringQueue.Push("hello")


	fmt.Println(
		stringQueue.Pop(),
	)

}

输出:

复制代码
100
hello

九、结合你的 GORM 项目理解泛型

你现在项目:

sql 复制代码
User
Device
Company
MonitorPoint
EmergencyPlan

都有类似代码。

例如:

查询用户:

go 复制代码
func GetUser(id uint)(*User,error)

查询设备:

go 复制代码
func GetDevice(id uint)(*Device,error)

查询公司:

go 复制代码
func GetCompany(id uint)(*Company,error)

代码:

bash 复制代码
db.First(&data,id)

完全一样。

泛型:

go 复制代码
type Repository[T any] struct {

	db *gorm.DB

}


func (r *Repository[T])
FindByID(
	id uint,
)(
	*T,
	error,
){

	var data T


	err :=
		r.db.
		First(&data,id).
		Error


	return &data,err
}

使用:

用户:

css 复制代码
userRepo :=
	Repository[User]{
		db:db,
	}

设备:

css 复制代码
deviceRepo :=
	Repository[Device]{
		db:db,
	}

公司:

css 复制代码
companyRepo :=
	Repository[Company]{
		db:db,
	}

一份 Repository。


十、泛型分页

你的项目里面分页很多:

以前:

go 复制代码
type UserPage struct {

	List []User

	Total int64

}

设备:

go 复制代码
type DevicePage struct {

	List []Device

	Total int64

}

公司:

go 复制代码
type CompanyPage struct {

	List []Company

	Total int64

}

其实结构一样。

泛型:

go 复制代码
type PageResult[T any] struct {

	List []T

	Total int64

}

于是:

用户:

css 复制代码
PageResult[User]

设备:

css 复制代码
PageResult[Device]

公司:

css 复制代码
PageResult[Company]

十一、泛型和 interface 的区别(非常重要)

很多 Go 开发者容易混淆。

interface

表示:

我不关心你是什么,只关心你有什么行为。

例如:

go 复制代码
type Animal interface {

	Speak()

}

狗:

go 复制代码
type Dog struct{}


func(Dog) Speak(){

	fmt.Println("汪")

}

猫:

go 复制代码
type Cat struct{}


func(Cat) Speak(){

	fmt.Println("喵")

}

它们都是 Animal。


泛型

表示:

我想写一份代码,支持很多类型。

例如:

r 复制代码
func Print[T any](value T)

它关心的是:

复制代码
类型

不是:

复制代码
行为

简单记:

csharp 复制代码
interface
解决:
不同对象相同能力


泛型
解决:
相同代码不同类型

十二、泛型什么时候不要用?

不是所有地方都适合。

比如:

业务代码:

go 复制代码
func CreateEmergencyPlan()

不要改:

go 复制代码
func Create[T any]()

没有意义。

因为:

业务逻辑不同:

复制代码
创建用户
创建设备
创建预案

本来就不同。

泛型适合:

复制代码
重复结构
重复算法
重复基础设施

十三、实际项目推荐使用位置

结合你的 Go 后端:

非常适合:

1. 统一返回

go 复制代码
type Response[T any] struct {

	Code int

	Message string

	Data T

}

返回:

css 复制代码
Response[UserDTO]

Response[[]DeviceDTO]

2. 分页

css 复制代码
PageResult[T]

3. Repository

css 复制代码
Repository[T]

4. 缓存

例如:

css 复制代码
Cache[T]

缓存:

css 复制代码
Cache[User]

Cache[Device]

十四、学习泛型最重要的三个例子

建议你真正写代码练:

例1

通用最大值:

css 复制代码
Max[T]

理解:

r 复制代码
T 是类型变量

例2

泛型 Stack:

css 复制代码
Stack[T]

理解:

复制代码
结构体泛型

例3

GORM Repository:

css 复制代码
Repository[T]

理解:

复制代码
企业项目应用

最后用一句话总结

Go 泛型:

r 复制代码
以前:

代码 + 固定类型


现在:

代码 + 类型参数

运行时:

T 被替换成真实类型

例如:

r 复制代码
func Save[T any](data T)

调用:

scss 复制代码
Save(User{})

编译器生成:

scss 复制代码
Save(User)

调用:

scss 复制代码
Save(Device{})

生成:

scss 复制代码
Save(Device)

泛型不是让 Go 变成动态语言,而是在编译阶段自动生成安全的重复代码。

对于你目前的 Go + Gin + GORM + 物联网平台开发,最值得掌握的是:

  1. Response[T]
  2. PageResult[T]
  3. Repository[T]
  4. Cache[T]
  5. Event[T]

这几个基本覆盖企业项目 80% 的泛型使用场景。

相关推荐
赵欢8271 小时前
跟Codex学习go(一)Context
go
他们叫我秃子2 小时前
前端开发转 Go 全栈(二):从 Hello Go 到类型转换,我开始习惯 Go 的“较真”
go
小满zs1 天前
Go语言第五章(数组和切片)
后端·go
chenzhou__1 天前
独立游戏开发日志 ①:从信号博弈到涂色对战——一次玩法重构始
笔记·后端·学习·unity·go·独立游戏
名字还没想好☜2 天前
Go 用 bufio.Scanner 读大文件踩坑:默认 64KB 行上限、Buffer 扩容与按 Token 切分
开发语言·后端·golang·go
microrain2 天前
从协议孤岛到统一接入:SagooIoT多协议接入层的架构演进
物联网·开源·go·sagooiot
microrain2 天前
从云端到边缘:SagooIoT边缘计算架构的落地实践
物联网·开源·go·sagooiot
TunerT_TQ3 天前
Valhalla 静态工程审阅 #007|AgentENV 源码证据驱动评测【大厂开源基础设施特辑】
rust·开源·go·github·sandbox·分布式系统·ai基础设施