何小豆儿在此 发表于 2025-4-4 01:54:55

【Golang】泛型与类型约束

一、环境

Go 1.20.2
二、没有泛型的Go

假设现在我们需要写一个函数,实现:
1)输入一个切片参数,切片类型可以是[]int或[]float64,然后将全部元素相加的“和”返回
2)假如是int切片,返回int类型;假如是float64切片,返回float64类型
当然,最简朴的方法是写两个函数SumSliceInt(s []int)、SumSliceFloat64(s []float64)来分别支持不同类型的切片,但是这样会导致大部门代码重复冗余,不是很优雅。那么有没有办法只写一个函数呢?
我们知道,在Go中全部的类型都实现了interface{}接口,所以假如想让一个变量支持多种数据类型,我们可以将这个变量声明为interface{}类型,比方var slice interface{},然后利用类型断言(.(type))来判断这个变量的类型。
interface{} + 类型断言:
// any是inerface{}的别名,两者是完全相同的:type any = interface{}
func SumSlice(slice any) (any, error) {
        switch s := slice.(type) {
        case []int:
                sum := 0
                for _, v := range s {
                        sum += v
                }
                return sum, nil
        case []float64:
                sum := float64(0)
                for _, v := range s {
                        sum += v
                }
                return sum, nil
        default:
                return nil, fmt.Errorf("unsupported slice type: %T", slice)
        }
}
从上述代码可见,固然利用interface{}类型可以实现在同一个函数内支持两种不同切片类型,但是每个case块内的代码仍然是高度相似和重复的,代码冗余的问题没有得到根本的办理。
三、泛型的优点

幸运的是,在Go 1.18之后开始支持了泛型(Generics),我们可以利用泛型来办理这个问题:
func SumSlice(slice []T) T {
        var sum T = 0
        for _, v := range slice {
                sum += v
        }
        return sum
}
是不是简洁了很多?而且,泛型相比interface{}还有以下上风:


[*]可复用性:进步了代码的可复用性,减少代码冗余。
[*]类型安全性:泛型在编译时就会进行类型安全检查,可以确保编译出来的代码就是类型安全的;而interface{}是在运行时才进行类型判断,假如编写的代码在类型判断上有bug或罅漏,就会导致Go在运行过程中报错。
[*]性能:不同类型的数据在赋值给interface{}变量时,会有一个隐式的装箱操作,从interface{}取数据时也会有一个隐式的拆箱操作,而泛型就不存在装箱拆箱过程,没有额外的性能开销。
四、理解泛型

(一)泛型函数(Generic function)

1)定义

编写一个函数,输入a、b两个泛型参数,返回它们的和:
// T的名字可以更改,改成K、V、MM之类的都可以,只是一般比较常用的是T
// 这是一个不完整的错误例子
func Sum(a, b T) T {
        return a + b
}
大写字母T的名字叫类型形参(Type parameter),代表a、b参数是泛型,可以继承多种类型,但具体可以继承哪些类型呢?在上面的定义中并没有给出这部门信息,要知道,并不是全部的类型都可以相加的,因此这里就引出了约束的概念,我们需要对T可以继承的类型范围作出约束:
// 正确例子
func Sum(a, b T) T {
        return a + b
}
中括号[]之间的空间用于定义类型形参,支持定义一个或多个


[*]T:类型形参的名字
[*]interface{ int | float64 }:对T的类型约束(Type Constraint),必须是一个接口,约束T只可以是int或float64
为了简化写法,类型约束中的interface{}在某些环境下是可以省略的,所以可以简写成:
func Sum(a, b T) T {
        return a + b
}
interface{}不能省略的一些环境:
// 当接口中包含方法时,不能省略
func Contains(num T) {
}
可以定义多个类型形参:
func Add(a T, b E) E {
        return E(a) + b
}
2)调用

以上面的Sum泛型函数为例,完整的调用写法为:
Sum(1, 2)
Sum(1.1, 2.2)
[]之间的内容称为类型实参(Type argument),是函数定义中的类型形参T的实际值,比方传int过去,那么T的实际值就是int。
类型形参确定为具体类型的过程称为实例化(Instantiations),可以简朴理解为将函数定义中的T更换为具体类型:
https://i-blog.csdnimg.cn/direct/2559bd3c83e642c78ba7ec2c9cfbd798.png
泛型函数实例化后,就可以像普通函数那样调用了。
但大多数时间,编译器都可以自动推导出该具体类型,无需我们主动告知,这个功能叫函数实参类型推导(Function argument type inference)。所以可以简写成:
// 简写,跟调用普通函数一样的写法
Sum(1, 2)
Sum(1.1, 2.2)
需要注意的是,在调用这个函数时,a、b两个参数的类型必须同等,要么两个都是int,要么都是float64,不能一个是int一个是float64:
Sum(1, 2.3) // 编译会报错
什么时间不能简写?
// 当类型形参T仅用在返回值,没有用在函数参数列表时
func Foo() T {
        return 1
}
Foo() // 报错:cannot infer T
Foo() // OK
Foo() // OK
(二)类型约束(Type constraint)

1)接口与约束

Go 利用interface定义类型约束。我们知道,在引入泛型之前,interface中只可以声明一组未实现的方法,大概内嵌其它interface,比方:
// 普通接口
type Driver interface {
        SetName(name string) (int, error)
        GetName() string
}

// 内嵌接口
type ReaderStringer interface {
    io.Reader
    fmt.Stringer
}
接口里的全部方法称之为方法集(Method set)。
引入泛型之后,interface内里可以声明的元素丰富了很多,可以是任何 Go 类型,除了方法、接口以外,还可以是根本类型,甚至struct布局体都可以,接口里的这些元素称为类型集(Type set):
// 基本类型约束
type MyInt interface {
        int
}

// 结构体类型约束
type Point interface {
        struct{ X, Y int }
}

// 内嵌其它约束
type MyNumber interface {
        MyInt
}

// 联合(Unions)类型约束,不同类型元素之间是“或”的关系
// 如果元素是一个接口,这个接口不能包含任何方法!
type MyFloat interface {
        float32 | float64
}
有了丰富的类型集支持,我们就可以更加方便的利用接口对类型形参T的类型作出约束,既可以约束为根本类型(int、float32、string…),也可以约束它必须实现一组方法,机动性大大增加。
因此前面的Sum函数还可以改写成:
// 原始例子:
// func Sum(a, b T) T {
//       return a + b
// }

type MyNumber interface {
        int | float64
}

func Sum(a, b T) T {
        return a + b
}
2)布局体类型约束

Go 还答应我们利用复合类型字面量来定义约束。比方,我们可以定义一个约束,类型元素是一个具有特定布局的struct:
type Point interface {
        struct{ X, Y int }
}
然而,需要注意的是,固然我们可以编写受此类布局体类型约束的泛型函数,但在当前版本的 Go 中,函数无法访问布局体的字段,比方:
func GetX(p T) int {
        return p.X// p.X undefined (type T has no field or method X)
}
3)类型近似(Type approximations)

我们知道,在Go中可以创建新的类型,比方:
type MyString string
MyString是一个新的类型,底层类型是string。
在类型约束中,偶然候我们大概并不关心上层类型,只要底层类型符合要求就可以,这时间就可以利用类型近似符号:~。
// 创建新类型type MyString string
// 定义类型约束type AnyStr interface {        ~string}// 定义泛型函数func Foo(param T) T {        return param}func main() {        var p1 string = "aaa"        var p2 MyString = "bbb"        Foo(p1)        Foo(p2) // 固然p2是MyString类型,但也可以通过泛型函数的类型约束检查} 需要注意的是,类型近似中的类型,必须是底层类型,而且不能是接口类型:
type MyInt int

type I0 interface {
        ~MyInt // 错误! MyInt不是底层类型, int才是
        ~error // 错误! error是接口
}
(三)泛型类型(Generic type)

1)泛型切片

假设现在有一个IntSlice类型:
type IntSlice []int

var s1 IntSlice = []int{1, 2, 3} // 正常
var s2 IntSlice = []string{"a", "b", "c"} // 报错,因为IntSlice底层类型是[]int,字符串无法赋值
很显然,因为类型不同等,s2是无法赋值的,假如想要支持其它类型,需要定义新类型:
type StringSlice []string
type Float32Slice []float32
type Float64Slice []float64
// ...
但是这样做的问题也显而易见,它们布局都是一样的,只是元素类型不同就需要重新定义这么多新类型,导致代码复杂度增加。
这时间就可以用泛型类型来办理这个问题:
// 只需定义一种新类型,就可以同时支持[]int/[]string/[]float32多种切片类型
// 新类型的名字叫 MySlice
type MySlice []T
   类型定义中带 类型形参 的类型,称之为泛型类型(Generic type)
泛型切片的初始化:
var s1 MySlice = MySlice{1, 2, 3}
var s2 MySlice = MySlice{"a", "b", "c"}
s3 := MySlice{"a", "b", "c"} // 简写
其它一些例子:
// 泛型Map
type MyMap mapV

var m1 MyMap = MyMap{"a": 1, "b": 2} // 完整写法
m2 := MyMap{1: "a", 2: "b"} // 简写

// 泛型通道
type MyChan chan T

var c1 MyChan = make(MyChan) // 完整写法
c2 := make(MyChan) // 简写
2)泛型布局体

假设现在要创建一个struct布局体,内里含有一个data泛型属性,类型是一个int或float64的切片:
type List struct {
        data []T
}
给这个布局体增加一个Sum方法,用于对切片求和:
func (l *List) Sum() T {
        var sum T
        for _, v := range l.data {
                sum += v
        }
        return sum
}
实例化布局体,并调用Sum方法:
// var list *List = &List{data: []int{1, 2, 3}} // 完整写法
list := &List{data: []int{1, 2, 3}}
sum := list.Sum()
fmt.Println(sum) // 输出:6
3)泛型接口

泛型也可以用在接口上:
type Human interface {
        GetWeight() T
}
假设现在有两个布局体,它们都有GetWeight()方法,哪个布局体实现了上面Human接口?
// 结构体1
type Person1 struct {
        Name string
}
func (p Person1) GetWeight() float32 {
        return 66.6
}

// 结构体2
type Person2 struct {
        Name string
}
func (p Person2) GetWeight() int {
        return 66
}
注意观察两个GetWeight()方法的返回值类型,因为我们在Human接口中约束了T的类型只能是float32,而只有Person1布局体的返回值类型符合约束,所以实际上只有Person1布局体实现了Human接口。
p1 := Person1{Name: "Tim"}
var iface1 Human = p1 // 正常,因为Person1实现了接口,所以可以赋值成功

p2 := Person2{Name: "Tim"}
var iface2 Human = p2 // 报错,因为Person2没有实现接口
(五)一些错误示例

下面列出一些错误利用泛型的例子。
1)联合约束中的类型元素限定

联合约束中的类型元素不能是包含方法的接口:
// 错误
type ReaderStringer interface {
        io.Reader | fmt.Stringer // 错误,io.Reader和fmt.Stringer是包含方法的接口
}

// 正确
type MyInt interface {
        int
}
type MyFloat interface {
        float32
}
type MyNumber interface {
        MyInt | MyFloat // 正确,MyInt和MyFloat接口里面没有包含方法
}
联合约束中的类型元素不能含有comparable接口:
type Number interface {
        comparable | int // 含有comparable,报错
}
2)一样平常接口只能用于泛型的类型约束

先解释下相关概念,引入泛型后,Go的接口分为两种类型:


[*]根本接口(Basic interface)
只包含方法的接口,称为根本接口,实在就是引入泛型之前的那种传统接口。
[*]一样平常接口(General interface)
由于引入泛型后,接口可以定义的元素大大丰富,假如一个接口里含有除了方法以外的元素,那么这个接口就称为一样平常接口。
一样平常接口只能用于泛型的类型约束,不能用于变量、函数参数、返回值的类型声明,而根本接口则没有此限定:
type NoMethods interface {
        int
}

// 错误,不能用于函数参数列表、返回值
func Foo(param NoMethods) NoMethods {
        return param
}

// 错误,不能用来声明变量的类型
var param NoMethods

// 正确
func Foo(param T) T {
        return param
}
五、参阅



[*]Go泛型全面讲解:一篇讲清泛型的全部
[*]Golang泛型
[*]An Introduction To Generics

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 【Golang】泛型与类型约束