跟CodeX学golang(二)- 范型

0 阅读5分钟

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

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

先抓住一句话:

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

普通变量:

func add(a int, b int) int {
	return a + b
}

这里:

a 的类型 = int
b 的类型 = int

类型写死了。

泛型:

func add[T int | float64](a T, b T) T {
	return a + b
}

这里:

T 是一个类型变量

调用时:

add[int](1,2)

相当于:

func add(a int,b int) int

调用:

add[float64](1.1,2.2)

相当于:

func add(a float64,b float64) float64

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


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

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

int 版本

func MaxInt(a int,b int) int {

	if a>b {
		return a
	}

	return b
}

使用:

fmt.Println(MaxInt(10,20))

输出:

20

现在需要 float:

func MaxFloat(a float64,b float64) float64 {

	if a>b {
		return a
	}

	return b
}

字符串:

func MaxString(a string,b string) string {

	if a>b {
		return a
	}

	return b
}

三个函数:

MaxInt
MaxFloat
MaxString

逻辑完全一样。

区别只有:

int
float64
string

这就是泛型解决的问题。


二、第一个泛型例子

改成:

func Max[T int | float64 | string](
	a T,
	b T,
) T {

	if a>b {
		return a
	}

	return b
}

这里:

T

就是一个未知类型。

意思:

T 可以是 int、float64、string。

使用:

int

fmt.Println(
	Max[int](10,20),
)

编译器理解:

func Max(
	a int,
	b int,
) int

float

fmt.Println(
	Max[float64](1.5,2.5),
)

理解:

func Max(
	a float64,
	b float64,
) float64

string

fmt.Println(
	Max[string](
		"hello",
		"world",
	),
)

三、为什么需要 [T]

你可能疑惑:

为什么写:

Max[T]

而不是:

Max()

因为 Go 是静态类型语言。

编译器需要提前知道:

a>b

是否合法。

比如:

func Max[T any](a T,b T)T{

	return a
}

这里:

T 可以是任何东西

例如:

struct{}

那么:

a>b

就非法。

所以需要告诉编译器:

T int | float64 | string

这些类型支持比较。


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

普通变量:

name := "张三"

保存数据。

泛型 T:

func Test[T any]()

保存类型。

例如:

调用:

Test[int]()

那么:

T = int

调用:

Test[string]()

那么:

T = string

类似:

普通变量:

x = 10


泛型:

T = int

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

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

以前:

整数:

func ContainsInt(
	arr []int,
	target int,
)bool{

	for _,v:=range arr{

		if v==target {
			return true
		}
	}

	return false
}

字符串:

func ContainsString(
	arr []string,
	target string,
)bool

重复。

泛型:

func Contains[T comparable](
	arr []T,
	target T,
)bool{

	for _,v:=range arr{

		if v==target {
			return true
		}

	}

	return false
}

使用:

整数:

Contains(
	[]int{1,2,3},
	2,
)

字符串:

Contains(
	[]string{
		"Go",
		"Java",
	},
	"Go",
)

六、comparable 是什么?

这里:

T comparable

表示:

T 必须支持:

==
!=

例如:

支持:

int

string

bool

不支持:

slice

map

因为:

[]int{1} == []int{1}

Go 不允许。


七、泛型结构体

不仅函数可以泛型。

比如:

普通队列

只能存 int:

type Queue struct {

	data []int

}

如果想存:

int
string
User
Device

怎么办?

以前:

复制:

QueueInt

QueueString

QueueUser

泛型:

type Queue[T any] struct {

	data []T

}

现在:

整数队列:

var q Queue[int]

等价:

data []int

字符串队列:

var q Queue[string]

等价:

data []string

用户队列:

type User struct {
	Name string
}


var q Queue[User]

等价:

data []User

八、完整泛型 Queue 示例

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 项目理解泛型

你现在项目:

User
Device
Company
MonitorPoint
EmergencyPlan

都有类似代码。

例如:

查询用户:

func GetUser(id uint)(*User,error)

查询设备:

func GetDevice(id uint)(*Device,error)

查询公司:

func GetCompany(id uint)(*Company,error)

代码:

db.First(&data,id)

完全一样。

泛型:

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
}

使用:

用户:

userRepo :=
	Repository[User]{
		db:db,
	}

设备:

deviceRepo :=
	Repository[Device]{
		db:db,
	}

公司:

companyRepo :=
	Repository[Company]{
		db:db,
	}

一份 Repository。


十、泛型分页

你的项目里面分页很多:

以前:

type UserPage struct {

	List []User

	Total int64

}

设备:

type DevicePage struct {

	List []Device

	Total int64

}

公司:

type CompanyPage struct {

	List []Company

	Total int64

}

其实结构一样。

泛型:

type PageResult[T any] struct {

	List []T

	Total int64

}

于是:

用户:

PageResult[User]

设备:

PageResult[Device]

公司:

PageResult[Company]

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

很多 Go 开发者容易混淆。

interface

表示:

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

例如:

type Animal interface {

	Speak()

}

狗:

type Dog struct{}


func(Dog) Speak(){

	fmt.Println("汪")

}

猫:

type Cat struct{}


func(Cat) Speak(){

	fmt.Println("喵")

}

它们都是 Animal。


泛型

表示:

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

例如:

func Print[T any](value T)

它关心的是:

类型

不是:

行为

简单记:

interface
解决:
不同对象相同能力


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

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

不是所有地方都适合。

比如:

业务代码:

func CreateEmergencyPlan()

不要改:

func Create[T any]()

没有意义。

因为:

业务逻辑不同:

创建用户
创建设备
创建预案

本来就不同。

泛型适合:

重复结构
重复算法
重复基础设施

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

结合你的 Go 后端:

非常适合:

1. 统一返回

type Response[T any] struct {

	Code int

	Message string

	Data T

}

返回:

Response[UserDTO]

Response[[]DeviceDTO]

2. 分页

PageResult[T]

3. Repository

Repository[T]

4. 缓存

例如:

Cache[T]

缓存:

Cache[User]

Cache[Device]

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

建议你真正写代码练:

例1

通用最大值:

Max[T]

理解:

T 是类型变量

例2

泛型 Stack:

Stack[T]

理解:

结构体泛型

例3

GORM Repository:

Repository[T]

理解:

企业项目应用

最后用一句话总结

Go 泛型:

以前:

代码 + 固定类型


现在:

代码 + 类型参数

运行时:

T 被替换成真实类型

例如:

func Save[T any](data T)

调用:

Save(User{})

编译器生成:

Save(User)

调用:

Save(Device{})

生成:

Save(Device)

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

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

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

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