泛型在go的使用建议

举报
码乐 发表于 2025/07/23 06:27:59 2025/07/23
【摘要】 1 简介本章将讨论对类型参数值的哪些操作 在泛型函数体中是有效的,哪些是无效的。在泛型函数体中, 对类型参数值的操作仅在以下情况下有效: 对 Type 参数约束的 Type 集中的每种类型的值都有效。 在当前的自定义通用设计和实现(Go 1.22)中, 反之亦然。 必须满足一些额外的要求才能使操作有效。目前,有许多这样的限制。其中一些是暂时的 并且可能会从未来的 Go 版本中删除,有些是永...

1 简介

本章将讨论对类型参数值的哪些操作 在泛型函数体中是有效的,哪些是无效的。

在泛型函数体中, 对类型参数值的操作仅在以下情况下有效: 对 Type 参数约束的 Type 集中的每种类型的值都有效。 在当前的自定义通用设计和实现(Go 1.22)中, 反之亦然。 必须满足一些额外的要求才能使操作有效。

目前,有许多这样的限制。其中一些是暂时的 并且可能会从未来的 Go 版本中删除,有些是永久性的。 临时的主要是实现工作量造成的, 因此,它们最终需要一些时间和努力才能被移除。 永久性的是由自定义泛型设计原则引起的。

2 对类型参数类型的值的操作

以下内容将列出这些限制。 还将列出一些事实和相关概念。

每个类型化值必须具有指定的类型,泛型函数中使用的类型是相同的
如上一章所述,从 Go 1.18 开始, Go 中的值类型可以分为两类:

  • 类型参数类型:类型参数列表中声明的类型。

普通类型:类型参数列表中未声明的值类型。 在 Go 1.18 之前,只有普通类型。
Go 自定义泛型不是作为简单的代码文本模板实现的。

这是与代码生成的根本区别。 go编程中有一个原则规则: 每个类型化表达式都必须具有指定的类型, 它可以是普通类型,也可以是类型参数。

例如,在以下代码片段中,只有函数dot不编译。 其他的编译正常。 原因很简单:

在函数中,类型为 ,是类型参数。 当然,在函数的使用中,可以实例化为 或 , 但这并不能改变这样一个事实,即从编译器的角度来看, foox 其类型是类型参数。
在函数中,bar 和 x[i]x[y]E的类型都是类型参数。
在函数中,win和 x[1]x[y]int的类型都是指定的普通类型。
在函数中,dot和 x[1]x[y]intstring的类型可能是 or(两种不同的普通类型),尽管它们总是相同的。

    func foo[T int | string](x T) {
        var _ interface{} = x // okay
    }

    func bar[T []E, E any](x T, i, j int) () {
        x[i] = x[j] // okay
    }

    func win[T ~[2]int | ~[8]int](x T, i, j int) {
        x[i] = x[j] // okay
    }

    func dot[T [2]int | [2]string](x T, i, j int) {
        x[i] = x[j]      // error: invalid operation
        var _ any = x[i] // error: invalid operation
    }

字符串的元素类型被视为 ,因此以下代码编译:byte

    func ele[ByteSeq ~string|~[]byte](x ByteSeq, n int) {
        _ = x[n] // okay
    }

出于同样的原因(原则规则),在以下代码片段中, 函数和两者都编译正常, 但该函数没有。nopjammud

    func nop[T *Base, Base int32|int64](x T) {
        *x = *x + 1 // okay
    }

    func jam[T int32|int64](x *T) {
        *x = *x + 1 // okay
    }

    func mud[T *int32|*int64](x T) {
        *x = *x + 1 // error: invalid operation
    }

同样,在下面的代码片段中,只有函数编译失败, 另外两个都可以编译。box

    func box[T chan int | chan byte](c T) {
        _ = <-c // error
    }


    func sed[T chan E, E int | byte](c T) {
        _ = <-c // okay
    }

    type Ch <-chan int
    func cat[T chan int | Ch](c T) {
        _ = <-c // okay
    }

3 类型参数可以被类型断言

由于类型参数是指定的类型,因此可以断言到类型。 即使存在重复的类型表达式,也会编译以下代码 在casetype-switchwua函数内的代码块中的运行时。

    import "fmt"

    func nel[T int | string](x any) {
        if v, ok := x.(T); ok {
            fmt.Printf("x is a %T\n", v)
        } else {
            fmt.Printf("x is not a %T\n", v)
        }
    }

    func wua[T int | string](x any) {
        switch v := x.(type) {
        case T:
            fmt.Println(v)
        case int:
            fmt.Println("int")
        case string:
            fmt.Println("string")
        }
    }

类型参数不能用作(本地)命名常量的类型
这意味着类型参数的值都是非常量。

例如,以下函数无法编译。

  func f[P int]() {
      const y P = 5 // error: invalid constant type P
  }

这个事实永远不会改变。

因此,将常量转换为类型参数会产生 传递给 Type 参数的参数的非常量值。 例如,在下面的代码中,该函数编译: 但该函数没有。hg

    const N = 5

    func g[P int]() {
        const _ = P(N) // error: P(N) is not constant
    }

    func h[P int]() {
        var _ = P(N) // okay
    }

由于转换规则,两者的返回结果 函数mud和tex 是不同的。

			package main

			const S = "Go"

			func mud() byte {
				return 64 << len(string(S)) >> len(string(S))
			}

			func tex[T string]() byte {
				return 64 << len(T(S)) >> len(T(S))
			}

			func main() {
				println(mud()) // 64
				println(tex()) // 0
			}

			# 

			func Output[T any]() {
				var t T
				fmt.Printf("%#v\n", t)
			}
			 
			type A struct {
				a,b,c,d,e,f,g int64
				h,i,j string
				k []string
				l, m, n map[string]uint64
			}
			 
			type B A
			 
			func main() {
				Output[string]()
				Output[int]()
				Output[uint]()
				Output[int64]()
				Output[uint64]() // 上面每个都underlying type都不同,尽管int64和uint64大小一样,所以生成5份不同的代码
				Output[*string]()
				Output[*int]()
				Output[*uint]()
				Output[*A]() // 所有指针都是同一个shape,所以共用一份代码
				Output[A]()
				Output[*B]()
				Output[B]() // B的underlying tyoe和A一样,所以和A共用代码
			}

4小结

实现泛型有很多种方法,常见的主流的是下面这些:

以c++为代表的,类型参数就是个占位符,最后实际上会替换成实际类型,然后以此为模板生成实际的代码,生成多份代码,每份的类型都不一样
以TypeScript和Java为代表的类型擦除,把类型参数泛化成一个满足类型约束的类型(Object或者某个interface),只生成一份代码
以c#为代表,代码里表现的像类型擦除,但运行的时候实际上和c++一样采用模板实例化对每个不同的类型都生成一份代码

golang有自己的gcshape。 简单说,所有拥有相同undelyring type的类型都算同一种shape,所有的指针都算一种shape,除此之外就算两个类型大小相同甚至字段的类型相同也不算同一个shape。

使用建议

  明确使用 *T,而不是让T代表指针类型
  明确使用 []T和 map[T1]T2,而不是让T代表slice或map
  少写泛型函数,可以多用泛型struct
  类型约束的core type直接影响被约束的类型可以执行哪些操作
【版权声明】本文为华为云社区用户翻译文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容, 举报邮箱:cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。