十年磨一剑 go 1.18泛型

本文详细介绍了Go语言中的泛型,包括什么是泛型、为什么需要泛型,以及在Go 1.18之前如何实现"泛型"。重点讲述了Go 1.18引入的泛型,包括泛型类型、泛型函数、接口、类型约束、any和comparable等。文章还讨论了泛型的一些设计决策,并总结了泛型的使用注意事项。
摘要由CSDN通过智能技术生成

什么是泛型

泛型程序设计(generic programming)是程序设计语言的一种风格或范式。泛型允许程序员在强类型程序设计语言中编写代码时使用一些以后才指定的类型,在实例化时作为参数指明这些类型。各种程序设计语言和其编译器、运行环境对泛型的支持均不一样。Java和C# 称之为泛型(generics)ML、Scala 和 Haskell 称之为参数多态(parametric polymorphism);C++ 和 D称之为模板(template)。具有广泛影响的1994年版的《Design Patterns》一书称之为参数化类型(parameterized type)。

为什么需要泛型

考虑这么一个需求,实现一个函数,这个函数接受2个int的入参,返回两者中数值较小的。需求是非常简单的,我们可以不假思索的写下如下的代码:

func Min(a,b int) int {
    if a < b {
        return a
    }
    return b
}

看起来很美好,但是这个函数有局限性,入参只能用int类型,如果需求做了拓展,需要支持对两个float64的入参做判断,返回两者中较小的。

众所周知,go是一个强类型的语言,且不像c那样在算术表达式里有隐式的类型转换(例如隐式的int转bool,float转int),所以上述这个函数就不能满足需求场景的,不过要支持这个拓展的需求也是很简单的,改成如下的代码然后使用MinFloat64即可:

func Min(a,b int) int {
    if a < b {
        return a
    }
    return b
}
func MinFloat64(a,b float64) float64 {
    if a < b {
        return a
    }
    return b
}

但是如果需求又做了拓展,需要支持对两个int64类型的。同理也很简单,如下:

func Min(a,b int) int {
    if a < b {
        return a
    }
    return b
}
func MinFloat64(a,b float64) float64 {
    if a < b {
        return a
    }
    return b
}
func MinInt64(a,b int64) int64 {
    if a < b {
        return a
    }
    return b
}

但是如果需求又做了拓展......然后我们就一直加哇加哇,然后最终就变的像下图一样了(ps:go离泛型就差一个sublime...)

 

不知道大家有没有发现,一旦需求做了拓展,我们都需要也跟着做一些变更,一直做着重复事情,而且通过看函数原型,我们发现只有类型声明这里不一致,当然函数名也是不一致,因为golang也是不支持函数重载(function overloading)的,如果golang支持了函数重载,我们这里不一致的也就只剩下类型了(ps:函数重载其实也是泛型的一种实现,在编译时通过将类型参数信息加入函数符号里,就实现了编码时的调用同名函数,但是在运行时因为类型信息也不会有二义性)。

 

那么有没有一种手段可以减少我们重复的工作量呢?在需求做了拓展后,也能在不改动原有代码的基础上做支持,也就是提高代码的可复用性,而这就是泛型的使命。

before go1.18 泛型

在没有泛型前,开发者们是如何实现"泛型的"。

  1. copy & paste

这是我们最容易想到的方式,也是我们在前文介绍的方式,看起来是一种很笨的方式,但是结合实际情况,大多数情况下你可能只需要两三个类型的实现,过早的去优化,可能会带来更多的问题,go proverbs里有一句就很符合这个场景。

“A little copying is better than a little dependency.”(一点复制好过一点依赖)

优点:无需额外的依赖,代码逻辑简单。

缺点:代码会有一些臃肿,且灵活性有缺失。

  1. interface

比较符合OOP的思路,面向接口编程则容易想到这种途径,不过像我们上述的取两数min场景就不能用interface去满足了,可应用的场景比较单一,考虑有下边这样一个接口。

type Inputer interface {
    Input() string
}

对于Inputer接口,我们可以定义有多种实现,比如

type MouseInput struct{}

func (MouseInput) Input() string {
    return "MouseInput"
}

type KeyboardInput struct{}
func (KeyboardInput) Input() string {
    return "KeyboardInput"
}

这样我们在调用时,也就可以用不同的类型定义相同的接口,通过interface来调用相同的函数了。不过本质上interface和generic是两种设计思路,应用的场景也不太一样,这里只是举了一个共通的例子。

优点:无需额外的依赖,代码逻辑简单。

缺点:代码会有一些臃肿,且应用的场景较单一。

  1. reflect

reflect(反射)在运行时动态获取类型,golang runtime将使用到的类型都做了存储,对于用户层golang则提供了非常强大的反射包,牺牲了性能,但是提供更多的便捷性,帮助程序员在可以在静态语言里使用一些动态的特性,本质上reflect和generic是两种截然不同的设计思路,反射在运行时发挥作用,而泛型则在编译时发挥作用,runtime无须感知到泛型的存在,像gorm框架就大量用到了反射。reflect包就内置了DeepEqual的实现,用来判断了两个入参是否相等。

func DeepEqual(x, y any) bool {
   if x == nil || y == nil {
      return x == y
   }
   v1 := ValueOf(x)
   v2 := ValueOf(y)
   if v1.Type() != v2.Type() {
      return false
   }
   return d
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值