前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >因势而变,因时而动,Go lang1.18入门精炼教程,由白丁入鸿儒,Go lang泛型(generic)的使用EP15

因势而变,因时而动,Go lang1.18入门精炼教程,由白丁入鸿儒,Go lang泛型(generic)的使用EP15

作者头像
用户9127725
发布2022-09-23 15:24:35
2830
发布2022-09-23 15:24:35
举报
文章被收录于专栏:刘悦的技术博客

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

    基本概念

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

代码语言:javascript
复制
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中的泛型使用 [] 来申明类型范围:

代码语言:javascript
复制
func sum[v int | float64 | string](a v, b v) v {

	s := a + b

	return s
}

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

代码语言:javascript
复制
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))
}

    程序返回:

代码语言:javascript
复制
12

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

代码语言:javascript
复制
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))
}

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

代码语言:javascript
复制
3

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

    高阶应用

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

代码语言:javascript
复制
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
}

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

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

代码语言:javascript
复制
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)
}

    程序返回:

代码语言:javascript
复制
{[1 2 3]}
{[A B C]}

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

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

代码语言:javascript
复制
package main

import (
	"fmt"
)

func main() {

	fn := func(a, b int) int {
		return a + b
	} // 定义了一个匿名函数并赋值给 fn

	fmt.Println(fn(1, 2)) // 输出: 3
}

    程序返回:

代码语言:javascript
复制
3

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

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

代码语言:javascript
复制
fnGeneric := func[T int | string](a, b T) T {
        return a + b
}

    程序报错:

代码语言:javascript
复制
./hello.go:9:19: syntax error: function literal must have no type parameters

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

代码语言:javascript
复制
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)

}

    程序返回:

代码语言:javascript
复制
3

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

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

代码语言:javascript
复制
type A struct {
}

// 不支持泛型方法
func (receiver A) Add[T int | float32 | float64](a T, b T) T {
    return a + b
}

    程序报错:

代码语言:javascript
复制
syntax error: method must have no type parameters

    但是和匿名函数类型,因为receiver支持泛型,所以我们可以声明结构体内receiver的参数为泛型类型:

代码语言:javascript
复制
package main

import "fmt"

type A[T int | float32 | float64] struct {
}

// 方法可以使用类型定义中的形参 T
func (receiver A[T]) Add(a T, b T) T {
	return a + b
}

func main() {

	var a A[int]
	res := a.Add(1, 2)

	fmt.Println(res)

}

    程序返回:

代码语言:javascript
复制
3

    因为receiver声明了泛型参数,我们为结构体A绑定的方法也就可以直接使用声明好的泛型类型,和匿名函数直接用父级泛型是一个意思。

    结语

    事实上,静态语言在设计上基本都有泛型的概念,这并不是自我矛盾,对应的,在动态语言Python中为函数声明形参时,我们其实也可以指定具体的参数类型或者返回值类型,正所谓无招胜有招,真正的高手,可以脱离语言类型的桎梏,达到一种无我无众生的境界,比如,在固有思维模式中,降龙十八掌是一种至刚至猛的武功,威力无穷,无坚不摧,但郭大侠后期再使用这门神功时,降龙十八掌的劲力忽强忽弱,忽吞忽吐,从至刚之中竟生出至柔的妙用,那已是洪七公当年所领悟不到的境界,所以,刚柔并济、虚中有实、实中有虚、虚实相生才是泛型使用的最高境界。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-08-21 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  •     基本概念
  •     泛型声明
  •     高阶应用
  •     匿名函数和方法暂不支持泛型    
  •     结语
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档