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

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

事实上,泛型才是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](a T, b T) T {  
    return a + b  
}

程序报错:

syntax error: method must have no type parameters

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

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)  
  
}

程序返回:

3

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

结语

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

相关文章
|
5天前
|
监控 算法 Go
Golang深入浅出之-Go语言中的服务熔断、降级与限流策略
【5月更文挑战第4天】本文探讨了分布式系统中保障稳定性的重要策略:服务熔断、降级和限流。服务熔断通过快速失败和暂停故障服务调用来保护系统;服务降级在压力大时提供有限功能以保持整体可用性;限流控制访问频率,防止过载。文中列举了常见问题、解决方案,并提供了Go语言实现示例。合理应用这些策略能增强系统韧性和可用性。
52 0
|
5天前
|
JavaScript 前端开发 Go
Go语言的入门学习
【4月更文挑战第7天】Go语言,通常称为Golang,是由Google设计并开发的一种编程语言,它于2009年公开发布。Go的设计团队主要包括Robert Griesemer、Rob Pike和Ken Thompson,这三位都是计算机科学和软件工程领域的杰出人物。
17 1
|
5天前
|
分布式计算 Java Go
Golang深入浅出之-Go语言中的分布式计算框架Apache Beam
【5月更文挑战第6天】Apache Beam是一个统一的编程模型,适用于批处理和流处理,主要支持Java和Python,但也提供实验性的Go SDK。Go SDK的基本概念包括`PTransform`、`PCollection`和`Pipeline`。在使用中,需注意类型转换、窗口和触发器配置、资源管理和错误处理。尽管Go SDK文档有限,生态系统尚不成熟,且性能可能不高,但它仍为分布式计算提供了可移植的解决方案。通过理解和掌握Beam模型,开发者能编写高效的数据处理程序。
141 1
|
5天前
|
缓存 测试技术 持续交付
Golang深入浅出之-Go语言中的持续集成与持续部署(CI/CD)
【5月更文挑战第5天】本文介绍了Go语言项目中的CI/CD实践,包括持续集成与持续部署的基础知识,常见问题及解决策略。测试覆盖不足、版本不一致和构建时间过长是主要问题,可通过全面测试、统一依赖管理和利用缓存优化。文中还提供了使用GitHub Actions进行自动化测试和部署的示例,强调了持续优化CI/CD流程以适应项目需求的重要性。
56 1
|
5天前
|
Kubernetes Cloud Native Go
Golang深入浅出之-Go语言中的云原生开发:Kubernetes与Docker
【5月更文挑战第5天】本文探讨了Go语言在云原生开发中的应用,特别是在Kubernetes和Docker中的使用。Docker利用Go语言的性能和跨平台能力编写Dockerfile和构建镜像。Kubernetes,主要由Go语言编写,提供了方便的客户端库与集群交互。文章列举了Dockerfile编写、Kubernetes资源定义和服务发现的常见问题及解决方案,并给出了Go语言构建Docker镜像和与Kubernetes交互的代码示例。通过掌握这些技巧,开发者能更高效地进行云原生应用开发。
58 1
|
5天前
|
负载均衡 监控 Go
Golang深入浅出之-Go语言中的服务网格(Service Mesh)原理与应用
【5月更文挑战第5天】服务网格是处理服务间通信的基础设施层,常由数据平面(代理,如Envoy)和控制平面(管理配置)组成。本文讨论了服务发现、负载均衡和追踪等常见问题及其解决方案,并展示了使用Go语言实现Envoy sidecar配置的例子,强调Go语言在构建服务网格中的优势。服务网格能提升微服务的管理和可观测性,正确应对问题能构建更健壮的分布式系统。
30 1
|
5天前
|
消息中间件 Go API
Golang深入浅出之-Go语言中的微服务架构设计与实践
【5月更文挑战第4天】本文探讨了Go语言在微服务架构中的应用,强调了单一职责、标准化API、服务自治和容错设计等原则。同时,指出了过度拆分、服务通信复杂性、数据一致性和部署复杂性等常见问题,并提出了DDD拆分、使用成熟框架、事件驱动和配置管理与CI/CD的解决方案。文中还提供了使用Gin构建HTTP服务和gRPC进行服务间通信的示例。
29 0
|
5天前
|
Prometheus 监控 Cloud Native
Golang深入浅出之-Go语言中的分布式追踪与监控系统集成
【5月更文挑战第4天】本文探讨了Go语言中分布式追踪与监控的重要性,包括追踪的三个核心组件和监控系统集成。常见问题有追踪数据丢失、性能开销和监控指标不当。解决策略涉及使用OpenTracing或OpenTelemetry协议、采样策略以及聚焦关键指标。文中提供了OpenTelemetry和Prometheus的Go代码示例,强调全面可观测性对微服务架构的意义,并提示选择合适工具和策略以确保系统稳定高效。
36 5
|
5天前
|
负载均衡 算法 Go
Golang深入浅出之-Go语言中的服务注册与发现机制
【5月更文挑战第4天】本文探讨了Go语言中服务注册与发现的关键原理和实践,包括服务注册、心跳机制、一致性问题和负载均衡策略。示例代码演示了使用Consul进行服务注册和客户端发现服务的实现。在实际应用中,需要解决心跳失效、注册信息一致性和服务负载均衡等问题,以确保微服务架构的稳定性和效率。
22 3
|
5天前
|
前端开发 Go
Golang深入浅出之-Go语言中的异步编程与Future/Promise模式
【5月更文挑战第3天】Go语言通过goroutines和channels实现异步编程,虽无内置Future/Promise,但可借助其特性模拟。本文探讨了如何使用channel实现Future模式,提供了异步获取URL内容长度的示例,并警示了Channel泄漏、错误处理和并发控制等常见问题。为避免这些问题,建议显式关闭channel、使用context.Context、并发控制机制及有效传播错误。理解并应用这些技巧能提升Go语言异步编程的效率和健壮性。
30 5
Golang深入浅出之-Go语言中的异步编程与Future/Promise模式