Go泛型介绍和使用—一文学会Go泛型

简介

​ Go 1.18 在2022年3月正式发布,支持了新特性泛型,语法特性变化非常大,这里介绍下Go的泛型以及如何使用。

初识Go泛型

形参和实参

函数形参和实参

​ 先从简单的函数形参和实参,看下目前我们常用的函数是如何支持传入不同的参数,得到不同的结果的。

​ 变量a,b是函数Add的形参,"a int, b int"这一串被称为形参列表

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

​ 调用函数Add时,需要传入实参。实参需要满足形参的约束

Add(1,2)
Add(100,200)

​ 函数形参,类似占位符,没有具体的值。实参是传入的具体值,只需要满足形参的约束即可。

​ 有个问题是如果想对int32进行相加,则需要定义新方法 func AddInt32(a, b int32) int32 {},因为int32的实参不满足int形参的约束。

类型形参和实参

​ 问题:类比函数形参和实参,能否定义类型的形参,然后传入的类型的实参满足形参的约束?

// 伪代码
T int string // T为类型形参,约束只能是int string的类型实参
func Add(a, b T) T {
  return a + b
}

// 使用,实参类型可以是int string
Add(1 int, 2 int)
Add("a" string, "b" string)

​ 引入类型形参和实参,让一个函数获得了处理多种不同类型数据的能力,这种编程方式称为泛型编程

Go的泛型

​ Go在1.18及以后引入了类型形参和实参,支持了泛型。

类型定义

​ 类型形参和实参用伪代码展示了如何定义,这里介绍下Go的定义。

​ 从简单的类型定义说起,下面定义了一个"[]int"类型的切片

type IntSlice []int

​ 定义了一个类型IntSlice,此类型为可以容纳int类型的切片。如果此时想定一个可以容纳int32类型的切片或者float32类型的切片,需要给每种类型定义新类型

type Int32Slice []int32
type Float32Slice []float32

​ 使用泛型后,只定义一个类型就能代表上面的所有类型

type MySlice[T int | int32 | float32] []T
  • MySlice名称后有中括号"[]"
  • T为类型形参,T的具体类型并不确定,类似占位符
  • int | int32 | float32被称为类型约束,类型T只能接收int、int32、float32这三种类型的类型实参
  • 中括号"[]"里的被称为类型形参列表,MySlice中只有一个T
  • 定义的泛型类型名称为MySlice[T]

​ 定义好泛型类型后,不能直接使用,需要将泛型实例化才能使用

var a MySlice[int] = []int{1, 2, 3}
var b MySlice[float32] = []int{1.0, 2.0}

函数定义

​ 从简单的两数相加,下面定义了可以进行int类型相加,返回int类型的函数

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

​ 如果想对int32或者float64相加,返回对应的类型,需要定义如下函数

func AddInt32(a, b int32) int32 {
  return a + b
}

func AddFloat64(a, b float64) float64 {
  return a + b
}

​ 使用泛型类型后只需要定义一个函数

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

​ 定义好泛型函数后,不能直接使用,需要将泛型实例化才能使用

var intA int = 1
var intB int = 2
var int32A int32 = 3
var int32B int32 = 4

Add(intA, intB) // 可以直接类型推断
Add[int32](int32A, int32B)
Add("a","b")

​ 同样的,我们在实际开发中有很多场景需要获取对应参数的指针,或者根据指针获得对应参数值,此时使用泛型我们可以定义如下两个函数

// 获取指针
func Ptr[T any](in T) *T {
	return &in
}
// 获取指针的值
func PtrValue[T any](in *T) T {
	return *in
}

// 使用测试
// 使用泛型
intPtr1 := Ptr[int](1)
fmt.Println(reflect.TypeOf(intPtr1)) // *int
intPtr2 := Ptr(1)                    // 类型推断
fmt.Println(reflect.TypeOf(intPtr2)) // *int

strPtr := Ptr("a")
fmt.Println(reflect.TypeOf(strPtr)) // *string
strValue2 := PtrValue(strPtr)
fmt.Println(reflect.TypeOf(strValue2)) // string

泛型实现简单说明

​ 泛型的引入提高了Go的编码效率,但 编码效率编译速度运行速度 三者是无法同时提高的。Go引入泛型后,会提高编码效率,但编译速度和运行速度会有略微降低。可以通过了解Go的泛型实现,从而了解为什么会降低编译速度和运行速度。

​ 实现泛型的两种常见方式为**“虚拟方法表”“单态化”**。

虚拟方法表(Virtual Method Table)

​ 泛型函数,在编译时都会被编译器改成只接受指针作为参数,因为指针都是一样的,所以可以只编译一份代码。这些指针的值都被分配到堆上,调用泛型函数时会将指针传递给泛型函数。

​ 当传入的对象,且要调用对象的方法时,由于只有指向对象的指针,不知道方法在哪,所以需要一个查询方法的内存地址的表格**“Virtual Method Table”**,根据table查询到对应的方法地址。指针值的推导和调用虚拟函数,会比直接调用函数慢。

单态化(Monomorphization)

​ 编译器为每个被调用传入的数据类型,生成一个泛型函数的副本。对应的编译速度会慢,但运行时性能会更快,跟不使用泛型一致。

Go的实现

​ go采用了两者结合的实现。

  • 对于值类型,比如int、int32、struct{} 等,会分别生成一份对应的代码副本。且int和type MyInt int,其底层类型都是int,只会生成同一个副本
  • 对于指针类型和接口类型的调用,会统一生成一份泛型方法的副本。调用到方法时根据方法表动态查找方法地址

​ 综上可以理解为什么Go泛型引入后,编译速度和运行速度都会略微下降了。

Go泛型使用

基础

​ 上述Go的泛型-类型定义、函数定义是泛型的基础。

​ 且Go 1.18因为泛型,引入了一些新的变化。

interface{}、any、comparable

​ 这里的interface不是方法集的interface,而是指空接口interface{},从Go 1.18后,空接口interface{}的定义发生了变更,它可以表示所有类型的集合

  • 所有类型的集合,并非空集
  • 类型约束中的空接口interface{},表示包含了所有类型的类型集,并不是限定只能传入空接口interface{}
type Slice[T interface{}] []T

var s1 Slice[int]
var s2 Slice[string]
var s3 Slice[interface{}]

​ 由于空接口interface{}的定义发生了变更,不能直接体现实际语义,所以Go 1.18提供了新的等价关键词any。且Go官方推荐所有的使用空接口的地方都使用any替换。

type any = interface{}

// 上述的Slice可以改写成
type Slice[T any] []T

​ Go 1.18内置comparable约束,表示所有可以用 != 和 == 对比的类型。

// 可以用comparable来做泛型Map的key
type Map[K comparable, V any] map[K]V

符号“~”

​ 符号“~”,指底层类型约束,举个例子。

type Slice[T int | int32] []T
type MyInt int
var a Slice[MyInt] // 错误

​ MyInt底层类型是int,但其本身并不是int,所以不能用于Slice[T int | int32]的实例化。

​ 可以使用 ~int 的写法,表示所有以int为底层类型的类型都可以用于实例化。

type Slice[T ~int | ~int32] []T
type MyInt int
var a Slice[MyInt] // 正确

泛型类型

下面讲下如何定义泛型类型,以及可以定义哪些泛型类型。

类型形参不能单独使用

​ 下面这种是无法定义泛型Generic[T]的。

type Generic[T int | int32] T

泛型数据结构

​ 类型形参不能单独使用,需要跟其他数据结构组合起来一起定义泛型。可以组合的有ptr、slice、array、struct、map、channel、interface。

type Int interface {
	~int | ~int32 | ~int64
}

// ptr、slice、struct、map、channel、interface
type Ptr[T int | int32] *T // ptr

type SliceInt[T Int] []T // slice

type MapInt[K Int, V any] map[K]V // map

type ChannelInt[T Int] chan T // chan

// 只要满足有方法 Val() T,T的约束为 ~int | ~int32 | ~int64的都为这个接口的实现
type InterfaceInt[T Int] interface { // interface
	Val() T
}

// 例子1,InterfaceInt的实现
type StructInt[T Int] struct { // struct
	Data T
}
func (s *StructInt[T]) Val() T {
	return s.Data
}

// 例子2,InterfaceInt的实现
type InterfaceIntImpl1 struct{}
func (i *InterfaceIntImpl1) Val() int {
	return 1
}

// 例子3,InterfaceInt的实现
type InterfaceIntImpl2 int32
func (i InterfaceIntImpl2) Val() int32 {
	return int32(i)
}

// 测试
func main() {
	var interfaceInt InterfaceInt[int]

	interfaceInt = &InterfaceIntImpl1{}
	val := interfaceInt.Val()
	fmt.Println(val)
	fmt.Println(reflect.TypeOf(val)) // int

	interfaceInt = &StructInt[int]{Data: 2}
	val2 := interfaceInt.Val()
	fmt.Println(val2)
	fmt.Println(reflect.TypeOf(val2)) // int

	var interfaceInt32 InterfaceInt[int32] = InterfaceIntImpl2(int32(2))
	valInt32 := interfaceInt32.Val()
	fmt.Println(valInt32)
	fmt.Println(reflect.TypeOf(valInt32)) // int32
}

指针类型约束

​ 指针类型泛型定义约束,不能直接定义,需消除歧义。

// type Test1 [T * int] []T // error 会当做 T 乘 int
// 可以用逗号消除歧义
type Test2[T *int,] []T
type Test3[T *int | *int32,] []T

// 推荐写法
type Test4[T interface{ *int | *int32 }] []T

获取具体类型方式

​ 在使用泛型时,获取泛型具体的类型,可以通过使用反射或者any转换获取。

func GetType[T int | string](t T) {
	// t.(int) // error,泛型类型定义的变量不能使用类型断言

	// 1. 反射
	v := reflect.ValueOf(t)
	switch v.Kind() {
	case reflect.Int:
	default:
	}

	// 2. 转换
	var i any = t
	switch i.(type) {
	case int:
	default:
	}
}

​ 实际开发中如果用到这种,需要慎重考虑是否需要使用泛型。泛型的出现是为了屏蔽具体类型或者避免使用反射,现在又在泛型中使用反射或者any转换获得具体类型,这种做法是不合适的。

泛型函数

​ 带类型形参的函数,被称为泛型函数。

简单泛型函数

​ 比较常用的泛型函数形式如下。

func Add[T int | int32](a, b T) T {
  return a + b
}

// 实例化使用
Add[int](1,1) // 声明实例化类型为int
Add(1,1) // 类型推断

匿名函数

​ 匿名函数无法自己定义类型形参,但可以使用定义好的类型形参。

// 1. 匿名函数不能自己定义类型形参
//func test() {
//	fn1 := func[T int | int32](a, b T) T {
//		return a + b
//	}
//}

// 2. 匿名函数可以使用定义好的类型形参
func test[T int | int32](a, b T) T {
	result := func(a, b T) T {
		return a + b
	}(a, b)
	return result
}

函数闭包

​ 可以使用函数闭包实现一些高级功能,比如下面泛型Filter、Map、Reduce的实现。

func Filter[T any](src []T, f func(T) bool) []T {
	res := make([]T, 0)
	for _, t := range src {
		if f(t) {
			res = append(res, t)
		}
	}
	return res
}

func Map[S, T any](src []S, f func(S) T) []T {
	res := make([]T, 0)
	for _, s := range src {
		t := f(s)
		res = append(res, t)
	}
	return res
}

func Reduce[T any](src []T, f func(T, T) T) T {
	if len(src) == 1 {
		return src[0]
	}

	return f(src[0], Reduce(src[1:], f))
}

// 测试函数闭包
func main() {
	// filter test
	filterTest := Filter[int]([]int{1, 2, 3, 4, 5}, func(i int) bool {
		if i > 3 {
			return true
		}
		return false
	})
	fmt.Println(filterTest)

	// map test
	mapTest := Map[int, string]([]int{1, 2, 3}, func(i int) string {
		return "str" + strconv.Itoa(i)
	})
	fmt.Println(mapTest)

	// reduce test
	reduceTest := Reduce([]int{1, 2, 3}, func(a int, b int) int {
		return a + b
	})
	fmt.Println(reduceTest)
}

泛型结构体

结构体

​ 泛型结构体是上面泛型数据结构的一种,这里单讲一下。

​ 定义一个支持Map的结构体,key为comparable,value为any。

type Map[K comparable, V any] struct {
	Data map[K]V
}

func NewMap[K comparable, V any]() *Map[K, V] {
	return &Map[K, V]{
		Data: make(map[K]V),
	}
}

func (m *Map[K, V]) Set(key K, value V) {
	m.Data[key] = value
}
func (m *Map[K, V]) Get(key K) V {
	return m.Data[key]
}
func (m *Map[K, V]) Exist(key K) bool {
	_, ok := m.Data[key]
	return ok
}
func (m *Map[K, V]) PrintAll() {
	for k, v := range m.Data {
		fmt.Println("key: ", k, ", val: ", v)
	}
}

// 使用
intStringMap := NewMap[int, string]()
intStringMap.Set(1, "a")
intStringMap.Set(2, "b")
intStringMap.PrintAll()

s1 := &Student{
	Num:  1,
	Name: "a",
}
s2 := &Student{
	Num:  2,
	Name: "b",
}
numStudentMap := NewMap[int, *Student]()
numStudentMap.Set(s1.Num, s1)
numStudentMap.Set(s2.Num, s2)
numStudentMap.PrintAll()

​ 定义的struct map,可以兼容所有使用到map的场景,且提供统一的方法。比如PrintAll()方法,无需跟之前一样每个不同的map都要自己写一遍。

泛型方法

​ 泛型方法无法定义类型形参,只能通过receiver使用类型形参。

// 不支持泛型方法
//	func (m *Map[K, V]) TestGeneric[T int | string](a, b T) T { // error
//		return a + b
//	}
//

// 只能通过receiver使用类型形参
func (m *Map[K, V]) Equal(a, b K) bool {
	return a == b
}

匿名结构体

​ 匿名结构体是不支持泛型的。

	// 匿名结构体不支持泛型
	//testCase := struct [T int | string] { // error
	//	a T
	//}[int] {
	//	a: 1
	//}

接口

​ 接口比较复杂,在go 1.18以后,go的接口分为了两种,分别是基本接口一般接口

基本接口

​ 只包含方法,是方法的集合。基本接口可以定义变量

非泛型基本接口

​ 不包含泛型的基本接口。举例

type BasicInterface interface {
	Name() string
	Age() int
}

​ 可以定义变量。

// 可以定义变量
var a BasicInterface

​ 基本接口本身也代表一个类型集,可以用在类型约束中。

// 基本接口也代表一个类型集,可以用在类型约束中
type ATest[T BasicInterface] []T

// 可以当做类型集,用在泛型方法
func BasicInterfaceFunc1[T BasicInterface](b T) {
	b.Name()
	b.Age()
}

// 可以跟go1.18之前写法一致
func BasicInterfaceFunc2(b BasicInterface) {
	b.Name()
	b.Age()
}
泛型基本接口

​ 包含泛型的基本接口。举例

type BasicInterface2[T int | int32 | string] interface {
   Func1(in T) (out T)
   Func2() T
}

​ 可以定义变量。

var b1 BasicInterface2[int]
var b2 BasicInterface2[string]

​ 也代表一个类型集,可以用在类型约束中。

// 可以用在类型约束
type BTest1[T BasicInterface2[int]] []T
type BTest2[T int] BasicInterface2[T]

// 可以当做类型集,用在泛型方法
func BTestFunc1[T BasicInterface2[int]](t T) {
	t.Func2()
}

// 可以跟go1.18之前写法一致
func BTestFunc2(t BasicInterface2[int]) {
	t.Func2()
}

​ 如何实现泛型基本接口?对于BasicInterface2来说,需要满足以下条件。

  • 有方法 Func1(in T) (out T),方法 Func2() T。
  • T满足约束为 int | int32 | string。
  • 方法的T,同时只能为一种类型。比如Func1中的T为int,Func2中的T也只能为int。
// 举例1-是BasicInterface2的实现
type BasicInterface2Impl struct{}
func (b BasicInterface2Impl) Func1(in int) (out int) {
	panic("implement me")
}
func (b BasicInterface2Impl) Func2() int {
	panic("implement me")
}

// 举例2-不是BasicInterface2的实现
type BasicInterface2Impl2 struct{}
func (b BasicInterface2Impl2) Func1(in string) (out string) {
	panic("implement me")
}
func (b BasicInterface2Impl2) Func2() int {
	panic("implement me")
}

// 举例3-不是BasicInterface2的实现
type BasicInterface2Impl3 struct{}
func (b BasicInterface2Impl3) Func1(in float32) (out float32) {
	panic("implement me")
}
func (b BasicInterface2Impl3) Func2() float32 {
	panic("implement me")
}

一般接口

​ 包含类型约束的接口都被称为一般接口,无论是否包含方法。一般接口无法定义变量

简单约束和集合操作

​ 一般常用的是将类型约束定义在接口中,且多个类型约束接口可以进行集合操作。

type CommonInterface interface {
	int | int8 | float32 | string
}

// 不能用来定义变量
//var commonInterface CommonInterface // error

type Int interface {
	int | int8 | int32 | int64
}
type Float interface {
	float32 | float64
}

// 并集操作
type IntAndFloat interface {
	Int | Float
}

// 交集操作
type IntExceptInt8 interface {
	Int
	int8
}

// 空集,无实际意义
type Null interface {
	int
	int32
}
复杂类型约束

​ 上述为比较简单的类型约束,一般接口也可以包含方法,也可以包含泛型。

复杂约束1:包含方法的一般接口
type CommonInterface2 interface {
	~int | ~int8 | ~struct {
		Data string
	}

	Func1() string
}

​ 不能用来定义变量

//var c CommonInterface2 // error

​ 如何实现或者实例化复杂接口?对上述CommonInterface2来说,需要满足以下条件。

  • 底层类型为int|int8|struct{Data string}
  • 有方法Func1() string
// 举例1 是CommonInterface2的实例化
type CommonInterface2_1 int
func (c CommonInterface2_1) Func1() string {
	return "CommonInterface2_1"
}

// 举例2 是CommonInterface2的实例化
type CommonInterface2_2 struct {
	Data string
}
func (c CommonInterface2_2) Func1() string {
	return c.Data
}

// 举例3 不是CommonInterface2的实例化
type CommonInterface2_3 int32
func (c CommonInterface2_3) Func1() string {
	panic("CommonInterface2_3")
}

// 针对CommonInterface2的泛型方法
func DoCommonInterface2[T CommonInterface2](t T) {
	fmt.Println(t.Func1())
}

​ 测试例子

commonInterface2_1 := CommonInterface2_1(1)
DoCommonInterface2[CommonInterface2_1](commonInterface2_1)
DoCommonInterface2(commonInterface2_1) // 类型推断
commonInterface2_2 := CommonInterface2_2{}
DoCommonInterface2(commonInterface2_2)
//commonInterface2_3 := CommonInterface2_3(1)
//DoCommonInterface2(commonInterface2_3) // error, commonInterface2_3不是CommonInterface2的实例化
复杂约束2:包含方法,且包含泛型的一般接口
type CommonInterface3[T string | float32] interface {
	~int | ~int8 | ~struct {
		Data T
	}

	Func2() T
}

​ 不能用来定义变量。

//var a CommonInterface3[string]

如何实现或者实例化复杂接口?对上述CommonInterface3来说,需要满足以下条件。

  • 底层类型为int|int8|struct{Data T} ,T的约束为string | float32
  • 有方法Func2() T, T的约束为string | float32
  • 如果底层类型定义的没有T, 则Func2() T的约束可以为string | float32
  • 如果底层类型定义的有T, 比如struct{Data T}, 此时Func2() T,两个T需要同时为string或者同时为float32
// 举例1 CommonInterface3的实例化
type CommonInterface3Impl1 int
func (c CommonInterface3Impl1) Func2() string {
	return strconv.Itoa(int(c))
}

// 举例2 CommonInterface3的实例化
type CommonInterface3Impl2 int
func (c CommonInterface3Impl2) Func2() float32 {
	return float32(c)
}

// 举例3 CommonInterface3的实例化
type CommonInterface3Impl3[T string | float32] struct {
	Data T
}
func (c CommonInterface3Impl3[T]) Func2() T {
	return c.Data
}

// 针对CommonInterface3[string]的泛型函数
func DoCommonInterface3_1[T CommonInterface3[string]](t T) {
	fmt.Println(reflect.TypeOf(t.Func2()))
}

// 针对CommonInterface3[float32]的泛型函数
func DoCommonInterface3_2[T CommonInterface3[float32]](t T) {
	fmt.Println(reflect.TypeOf(t.Func2()))
}

// 针对CommonInterface3[T]的泛型函数
// 新增一个泛型D, 用来表示CommonInterface3里的泛型
func DoCommonInterface3[D string | float32, T CommonInterface3[D]](t T) {
	fmt.Println(reflect.TypeOf(t.Func2()))
}

​ 测试例子

commonInterface3_1 := CommonInterface3Impl1(1)
DoCommonInterface3_1(commonInterface3_1)
DoCommonInterface3[string](commonInterface3_1)

commonInterface3_2 := CommonInterface3Impl2(1)
DoCommonInterface3_2(commonInterface3_2)
DoCommonInterface3[float32](commonInterface3_2)

commonInterface3_3 := CommonInterface3Impl3[string]{
	Data: "data",
}
DoCommonInterface3_1(commonInterface3_3)
DoCommonInterface3[string](commonInterface3_3)

总结

  • 针对不同类型书写相同的逻辑,可以考虑用泛型简化代码,提高开发效率
  • 不要滥用泛型,一些场景使用接口、反射或许更合适。

上述代码示例都在github仓库:https://github.com/PeileiWang/generic_demo

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Golang自V1.18版本开始正式支持泛型泛型Golang中可用于变量、函数、方法、接口、通道等多个语法实体。泛型使用方法可以分为以下几个方面: 1. 泛型变量类型:泛型变量类型是针对类型变量的,可以用于声明包含不同类型的变量。例如,可以声明一个泛型切片变量,使其可以存储不同类型的元素。 2. 自定义泛型类型:在Golang中,可以通过声明自定义的泛型类型来扩展泛型使用。这意味着可以定义适用于不同类型的自定义数据结构。 3. 调用带泛型的函数:通过使用泛型类型作为函数参数或返回值,可以调用带有泛型的函数。这样可以实现在不同类型上通用的函数逻辑。 4. 泛型与结构体:泛型可以与结构体一同使用,结构体中的字段和方法可以是泛型类型,从而实现更灵活和通用的数据结构和操作。 然而,需要注意的是,Golang泛型还存在一些限制和缺陷。例如,无法直接与switch语句配合使用。这是因为在泛型中无法判断具体的类型,而switch语句需要根据具体类型进行分支判断。 总的来说,Golang泛型提供了一种通用的类型机制,使得代码更具灵活性和可复用性。但是需要熟悉泛型的语法和使用限制,以避免在实际使用中遇到问题。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [全面解读!Golang泛型使用](https://blog.csdn.net/QcloudCommunity/article/details/125401490)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *3* [go泛型使用方法](https://blog.csdn.net/qq_42062052/article/details/123840525)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值