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