我们换一种方式理解 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 + 物联网平台开发,最值得掌握的是:
Response[T]PageResult[T]Repository[T]Cache[T]Event[T]
这几个基本覆盖企业项目 80% 的泛型使用场景。