设为首页 加入收藏

TOP

因势而变,因时而动,Go lang1.18入门精炼教程,由白丁入鸿儒,Go lang泛型(generic)的使用EP15(一)
2023-07-23 13:27:05 】 浏览:50
Tags:时而动 lang1.18 白丁入 lang 泛型 generic EP15

事实上,泛型才是Go lang1.18最具特色的所在,但为什么我们一定要拖到后面才去探讨泛型?类比的话,我们可以想象一下给小学一年级的学生讲王勃的千古名篇《滕王阁序》,小学生有多大的概率可以理解作者的青云之志以及壮志难酬的愤懑心情?恐怕很难罢,是的,如果对Go lang的强类型语法没有一段时间的体验期,就很难理解泛型这种“反”静态语言概念。

基本概念

什么是泛型?泛型泛型,顾名思义,泛用的类型,说白了,就是在静态类型语言环境使用动态类型语言的特性:



package main  
  
import (  
	"fmt"  
)  
  
func sum(a string, b string) string {  
  
	s := a + b  
	return s  
}  
  
func main() {  
  
	a := "1"  
	b := "2"  
  
	fmt.Println(sum(a, b))  
}  



比方说有一个函数可以实现两个字符串合并,参数声明了字符串,也就不支持其他的数据类型,但如果逻辑上差不多,需要两个整形求和的函数怎么办?那就得再写一个差不多的函数,这样就影响了代码逻辑的复用性。

相同逻辑下可以针对不同的数据类型进行泛用,这就是泛型的意义所在。

泛型声明

Go lang中的泛型使用 [] 来申明类型范围:

func sum[v int | float64 | string](a v, b v) v {  
  
	s := a + b  
  
	return s  
}

如果是多个数据类型,可以使用|分隔,这里定义了一个泛型变量v,可以是整形、浮点以及字符串:

package main  
  
import (  
	"fmt"  
)  
  
func sum[v int | float64 | string](a v, b v) v {  
  
	s := a + b  
  
	return s  
}  
  
func main() {  
  
	a := "1"  
	b := "2"  
  
	fmt.Println(sum(a, b))  
}

程序返回:

12

注意,由于参数的类型未定,所以返回值也必须是泛型类型,现在动态的把参数改为整形:

package main  
  
import (  
	"fmt"  
)  
  
func sum[v int | float64 | string](a v, b v) v {  
  
	s := a + b  
  
	return s  
}  
  
func main() {  
  
	a := 1  
	b := 2  
  
	fmt.Println(sum(a, b))  
}

返回值也因为参数类型的改变而改变:

3

藉此,我们就声明了一个可以“泛用”的函数。

高阶应用

事实上,泛型的出现并非可以丰富函数的声明和构建,更多的,是战略层面上的多样化选择,比如容器内的类型,进而言之,队列:

type Queue[T interface{}] struct {  
	elements []T  
}  
  
// 将数据放入队列尾部  
func (q *Queue[T]) Put(value T) {  
	q.elements = append(q.elements, value)  
}  
  
// 从队列头部取出并从头部删除对应数据  
func (q *Queue[T]) Pop() (T, bool) {  
	var value T  
	if len(q.elements) == 0 {  
		return value, true  
	}  
  
	value = q.elements[0]  
	q.elements = q.elements[1:]  
	return value, len(q.elements) == 0  
}

这里结构体的类型约束使用了空接口,代表的意思是所有类型都可以用来实例化泛型类型,同时基于泛型结构体,我们定义两个方法,分别是:入队和出队。

因为这个队列是泛型队列,所以队内元素的类型可以在实现结构体接口时进行定义:

package main  
  
import (  
	"fmt"  
)  
  
type Queue[T interface{}] struct {  
	elements []T  
}  
  
// 将数据放入队列尾部  
func (q *Queue[T]) Put(value T) {  
	q.elements = append(q.elements, value)  
}  
  
// 从队列头部取出并从头部删除对应数据  
func (q *Queue[T]) Pop() (T, bool) {  
	var value T  
	if len(q.elements) == 0 {  
		return value, true  
	}  
  
	value = q.elements[0]  
	q.elements = q.elements[1:]  
	return value, len(q.elements) == 0  
}  
  
func main() {  
  
	var q1 Queue[int] // 可存放int类型数据的队列  
	q1.Put(1)  
	q1.Put(2)  
	q1.Put(3)  
	fmt.Println(q1)  
  
	var q2 Queue[string] // 可存放string类型数据的队列  
	q2.Put("A")  
	q2.Put("B")  
	q2.Put("C")  
  
	fmt.Println(q2)  
}

程序返回:

{[1 2 3]}  
{[A B C]}

匿名函数和方法暂不支持泛型

Golang中,我们经常会使用匿名函数:

package main  
  
import (  
	"fmt"  
)  
  
func main() {  
  
	fn := func(a, b int) int {  
		return a + b  
	} // 定义了一个匿名函数并赋值给 fn  
  
	fmt.Println(fn(1, 2)) // 输出: 3  
}

程序返回:

3

大体上,和Python的lambda表达式类似,如果封装的逻辑相对简单或者和上下游逻辑连贯性较强,那么,在不影响代码可读性的前提下,我们就没必要单独声明一个函数,而是选择匿名函数。

但1.18版本中,匿名函数并不支持参数为泛型,因为匿名函数不能自己定义类型形参:

fnGeneric := func[T int | string](a, b T) T {  
        return a + b  
}

程序报错:

./hello.go:9:19: syntax error: function literal must have no type parameters

但匿名函数可以使用已经被合法定义的泛型类型:

package main  
  
import (  
	"fmt"  
)  
  
func test[T int | float32 | float64](a, b T) {  
  
	// 匿名函数可使用已经定义好的类型形参  
	fn2 := func(i T, j T) T {  
		return i + j  
	}  
  
	fmt.Println(fn2(a, b))  
}  
  
func main() {  
  
	test(1, 2)  
  
}

程序返回:

3

也就是说,匿名函数可以使用父级函数定义好的泛型类型参数,这意味着,在泛型函数内,我们可以通过匿名函数对逻辑进行二次封装。

同样地,1.18版本中的方法也不支持泛型:

type A struct {  
}  
  
// 不支持泛型方法  
func (receiver A) Add[T int | float32 | float64]
首页 上一页 1 2 下一页 尾页 1/2/2
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇我的设计模式之旅、05 装饰模式 下一篇我的设计模式之旅、08 状态模式

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目