Go - 泛型的使用

泛型的语法

泛型为Go语言添加了三个新的重要特性:

  1. 函数和类型的类型参数
  2. 将接口类型定义为类型集,包括没有方法的类型。
  3. 类型推断,它允许在调用函数时在许多情况下省略类型参数。

类型参数

类型参数的使用

除了函数中支持类型参数列表 外,类型也支持类型参数列表

单个类型参数
go 复制代码
type Slice[T int | string] []T 
多个类型参数
go 复制代码
type Map[K int | string, V float32 | float64] map[K]V

不同泛型参数之间使用,隔开。

泛型类型使用方法
go 复制代码
type Tree[T interface{}] struct {
	left, right *Tree[T]
	value       T
}

// 同时,泛型类型可以使用用*方法*。
func (t *Tree[T]) Lookup(x T) *Tree[T] { ... }

在上述泛型中,TKV 都属于类型形参 ,类型形参后面是类型约束

需要注意 的是:

要使用泛型类型,必须先对其实例化

go 复制代码
var stringTree Tree[string]

类型实例化

定义一个适用于一组类型的min函数

go 复制代码
func min(T int | float64)(a,b T) T {
	if a<b{
		return a
	}
	return b
}

进行类型实例化

go 复制代码
// 1. int
m1 := min[int](1,2)
// 相当于
// m1 := min[int]
// mi := m1(1,2)


// 2. float
m2 := min[float64](-0.1,-0.2)
// 相当于
// 同上

根据上面的例子,

在定义函数时:
(T int | float64) 就是形参

在调用函数时:
m1 := min[int] 是在进行类型实例化mi := m1(1,2) 则是在调用。

类型实例化分为两步:

  1. 首先,编译器在整个泛型函数或类型中将所有类型形参(type parameters)替换为它们各自的类型实参(type arguments)。
  2. 其次,编译器验证每个类型参数是否满足相应的约束。

即先"替换",再"验证"。

类型约束

Go语言中的类型约束是接口类型

类型参数列表中每个类型参数都有一个类型约束 ,它定义了一个类型集 ,只有在这个类型集中的类型才能用作类型实参。

类型约束常见有两种方式:

  1. 类型约束接口直接在类型参数列表中使用
go 复制代码
// 类型约束字面量,通常外层interface{}可省略
func min[T interface{ int | float64 }](a, b T) T {
	if a <= b {
		return a
	}
	return b
}
  1. 作为类型约束使用的接口类型可以事先定义并支持复用
go 复制代码
// 事先定义好的类型约束类型
type Value interface {
	int | float64
}
func min[T Value](a, b T) T {
	if a <= b {
		return a
	}
	return b
}

若省略外层interface{}会引起歧义,则不能省略。

go 复制代码
type IntPtrSlice [T *int] []T  // T*int ?

type IntPtrSlice[T *int,] []T  // 只有一个类型约束时可以添加`,`
type IntPtrSlice[T interface{ *int }] []T // 使用interface{}包裹

类型集

**Go1.18开始接口类型的定义也发生了改变,由过去的接口类型定义方法集(method set)变成了接口类型定义类型集(type set)。**也就是说,接口类型现在可以用作值的类型,也可以用作类型约束。

把接口类型当做类型集相较于方法集有一个优势: 我们可以显式地向集合添加类型,从而以新的方式控制类型集。

Go语言扩展了接口类型的语法,让我们能够向接口中添加类型。例如

go 复制代码
type V interface {
	int | string | bool
}

上面的代码就定义了一个包含 intstringbool 类型的类型集。

从 Go 1.18 开始,一个接口不仅可以嵌入其他接口,还可以嵌入任何类型、类型的联合或共享相同底层类型的无限类型集合。

当用作类型约束时,由接口定义的类型集精确地指定允许作为相应类型参数的类型。

  • |符号
    T1 | T2表示类型约束为T1和T2这两个类型的并集
go 复制代码
type Integer interface {
	Signed | Unsigned
}
  • ~符号
    ~T表示所以底层类型是T的类型,例如~string表示所有底层类型是string的类型集合。
go 复制代码
type MyString string // MyString的底层类型是string

通过[T ~string] 就可以限制泛型参数的底层类型必须为string

注意: ~符号后面只能是基本类型。

any接口

空接口在类型参数列表中很常见,在Go 1.18引入了一个新的预声明标识符,作为空接口类型的别名。

go 复制代码
// src/builtin/builtin.go

type any = interface{}

由此,我们可以使用如下代码:

go 复制代码
func foo[S ~[]E, E any]() {
	// ...
}

类型判断

函数参数类型判断

go 复制代码
func min[T int | float64](a, b T) T {
	if a <= b {
		return a
	}
	return b
}

类型形参T用于指定ab的类型。我们可以使用显式类型实参调用它:

go 复制代码
var a, b, m float64
m = min[float64](a, b) // 显式指定类型实参

在许多情况下,编译器可以从普通参数推断 T 的类型实参。这使得代码更短,同时保持清晰。

go 复制代码
var a, b, m float64

m = min(a, b) // 无需指定类型实参

这种从实参的类型推断出函数的类型实参的推断称为函数实参类型推断

其只适用于函数参数中使用的类型参数,而不适用于仅在函数结果中或仅在函数体中使用的类型参数。

例如,它不适用于像 MakeT [ T any ]() T 这样的函数,因为它只使用 T 表示结果。

类型约束推断

Go 语言支持另一种类型推断,即约束类型推断

go 复制代码
// Scale 返回切片中每个元素都乘c的副本切片
func Scale[E constraints.Integer](s []E, c E) []E {
    r := make([]E, len(s))
    for i, v := range s {
        r[i] = v * c
    }
    return r
}

这是一个泛型函数适用于任何整数类型的切片。

现在假设我们有一个多维坐标的 Point 类型,其中每个 Point 只是一个给出点坐标的整数列表。这种类型通常会实现一些业务方法,这里假设它有一个String方法。

go 复制代码
type Point []int32

func (p Point) String() string {
    b, _ := json.Marshal(p)
    return string(b)
}

由于一个Point其实就是一个整数切片,我们可以使用前面编写的Scale函数:

go 复制代码
func ScaleAndPrint(p Point) {
    r := Scale(p, 2)
    fmt.Println(r.String()) // 编译失败
}

不幸的是,这代码会编译失败,输出r.String undefined (type []int32 has no field or method String的错误。

问题是Scale函数返回类型为[]E的值,其中E是参数切片的元素类型。当我们使用Point类型的值调用Scale(其基础类型为[]int32)时,我们返回的是[]int32类型的值,而不是Point类型。这源于泛型代码的编写方式,但这不是我们想要的。

为了解决这个问题,我们必须更改 Scale 函数,以便为切片类型使用类型参数。

go 复制代码
func Scale[S ~[]E, E constraints.Integer](s S, c E) S {
    r := make(S, len(s))
    for i, v := range s {
        r[i] = v * c
    }
    return r
}

现在这个Scale函数,不仅支持传入普通整数切片参数,也支持传入Point类型参数。

编译器推断 E 的类型参数是切片的元素类型的过程称为约束类型推断

约束类型推断从类型参数约束推导类型参数。当一个类型参数具有根据另一个类型参数定义的约束时使用。当其中一个类型参数的类型参数已知时,约束用于推断另一个类型参数的类型参数。

总结

总之,如果你发现自己多次编写完全相同的代码,而这些代码之间的唯一区别就是使用的类型不同,这个时候你就应该考虑是否可以使用类型参数。

泛型和接口类型之间并不是替代关系,而是相辅相成的关系。泛型的引入是为了配合接口的使用,让我们能够编写更加类型安全的Go代码,并能有效地减少重复代码。

参考文章:
https://www.liwenzhou.com/posts/Go/generics/#c-0-3-1

相关推荐
Eugene__Chen5 分钟前
java常见面试01
java·开发语言·面试
灏瀚星空7 分钟前
用Python+Flask打造可视化武侠人物关系图生成器:从零到一的实战全记录
开发语言·人工智能·经验分享·笔记·python·flask
java1234_小锋8 分钟前
一周学会Flask3 Python Web开发-Flask3之表单处理WTForms安装与定义WTForms表单类
开发语言·前端·python
大G哥30 分钟前
jenkins集成docker发布java项目
java·运维·开发语言·docker·jenkins
鱼不如渔32 分钟前
《C++ primer》第二章
开发语言·c++
_GR1 小时前
Qt开发⑪Qt网络+Qt音视频_使用实操
开发语言·c++·qt
艾斯比的日常1 小时前
深入解析Java虚拟机(JVM)的核心组成
java·开发语言·jvm
2302_799525741 小时前
【go语言】——方法集
开发语言·后端·golang
非 白2 小时前
【Java 后端】Restful API 接口
java·开发语言·restful
rider1892 小时前
Java多线程及线程变量学习:从熟悉到实战(下)
java·开发语言·学习