Golang Go语言中有一个泛型的问题咨询

发布于 1周前 作者 htzhanglong 来自 Go语言

为啥下面的 Golang 代码无法正常工作, 我理解两个 struct Test 和 TextNext 不是都有 V 这个变量么?

package main

import “fmt”

type Test struct { V string }

type TestNext struct { V string }

func handle[T Test | TestNext](a T) { fmt.Println(a.V) // 这里会报错. 提示 未解析的引用 ‘V’ }

func main() { handle(Test{V: “Hello”}) }


Golang Go语言中有一个泛型的问题咨询

更多关于Golang Go语言中有一个泛型的问题咨询的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html

40 回复

感觉应该是编译器不知道 a 是什么类型的吧

更多关于Golang Go语言中有一个泛型的问题咨询的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


AI 给出的是 非泛型的写法,有泛型的么?

访问具体的数据要用接口约束;

package main

import "fmt"

type Test struct {
V string
}

func (t *Test) GetValue() string {
return t.V
}

type TestNext struct {
V string
}

func (t *TestNext) GetValue() string {
return t.V
}

type Value interface {
GetValue() string
}

func handle[V Value](a V) {
fmt.Println(a.GetValue())
}

func main() {
handle(&Test{V: “Hello”})
}

我没用过 Go 的泛型,但是从实际使用的场景来看,Test 和 TestNext 是没有任何关系的,虽然都有一个成员 V 并且类型一致,但是编译器并不能直接将两者进行关联,所以重点是要让 Test 和 TestNext 产生关系,可以改造一下使这两个都实现同一个合同或者接口(如果 Go 支持其中之一的话)。

Go 只认接口

#2 ChatGPT 了解的 go 的最新版本是 1.16

g1.18 release notes 有表述
The Go compiler does not support accessing a struct field x.f where x is of type parameter type even if all types in the type parameter’s type set have a field f. We may remove this restriction in a future release.

看看 go 泛型的文档,写个接口。

感谢各位
最终我选择了如下的写法, 为什么转成 any 类型,不直接使用 any 作为参数 a 的类型?
因为我想只限制使用 Test | TestNext 这两个类型, 这样 handle 函数只能传指定的类型.
参考: https://stackoverflow.com/questions/73864711/get-type-parameter-from-a-generic-struct-using-reflection


<br>package main<br><br>import "fmt"<br><br>type Test struct {<br> V string<br>}<br><br>type TestNext struct {<br> V string<br>}<br><br>type Interface interface {<br> Test | TestNext<br>}<br><br>func handle[T Interface](a T) {<br> switch val := any(a).(type) {<br> case Test:<br> fmt.Println(val.V)<br> case TestNext:<br> fmt.Println(val.V)<br> }<br>}<br><br>func main() {<br> handle(Test{V: "Hello"})<br>}<br><br>

可以用复合接口满足你这个需求:

type MyType interface {
Test | TestNext // 类型约束
getV() string // 方法约束
}

就不用做 type assertion 了

我建议你按 4 楼的来,go 本来就是鸭子模型,范也得范鸭子的模型
10 楼你硬是写成的 php 动态类型验证,看似用了范型,实际上没用,你把[xxx]删了,T 改成 interface{}也行

package main

import "fmt"

type Test struct {
V string
}

type TestNext struct {
V string
}

func handle(a interface{}) {
switch val := any(a).(type) {
case Test:
fmt.Println(val.V)
case TestNext:
fmt.Println(val.V)
}
}

func main() {
handle(Test{V: “Hello”})
}


这个 issue 里提供了十几种写法,学到了!


用接口约束的话,那这里泛型好像可以去掉了,直接定义接口类型的参数不是更简单
func handle(a Value) {
fmt.Println(a.GetValue())
}

提问者想要的只是编译期类型检查


如果入参不是对应具体 interface{…}类型也是编译期就失败的。


个人感受是:go 的泛型适合运算符重载之类的基础类型场景,自定义类型通常用 interface{…}或者切面更加简洁清晰、硬要为了用泛型而去用泛型,可能会让代码更难看。

再根据我浅薄的 go 泛型使用经验(不超过 10 分钟),能用接口、切面解决的问题不要用泛型就是最优解。
——不一定对,仅供各位参考

泛型主要是用来实现数据结构与算法的. 有时候也需要在泛型方法里面转成 interface{}再断言

这个问题之前讨论过
/t/904511


type typeProxy interface {
Close() error
}

func CanCompile[T T1 | T2](t T) error {
return any(t).(typeProxy).Close()
}

CanCompile(T3{}) // 拒绝编译

用一个接口类型代理 method 访问,并不需要 switch type

这样子还不如直接用接口

#18
对比下 c++,因为 c++有运算符重载,所以模板这种泛型里类也可以直接加减乘除这些运算符、因此不需要使用方法如 a.Sum(b)。而 go 里没有运算符重载,很多算法数据结构里即使使用泛型,仍然需要以接口的形式由 struct 实现接口,然后用 a.Sum(b)之类的函数调用方式来处理。显而易见,既然这种地方已经使用接口方法了,这里的泛型就是多余的了。
多数时候是在使用基础类型为主的地方,使用泛型可以节约代码、不用为每种类型都去实现一个 /一组方法或者单个方法内再做类型转换。而本来接口、切面就能做的地方,泛型是画蛇添足。

所以有了我在#17 的观点。

自从支持了泛型以后,不少人为了用泛型而用泛型,让 go 代码变得更丑陋了,很不希望看到一些大厂“架构师”又来污染放毒,因为太多小白会跟风,最终劣币驱逐良币。

是的, 如果泛型的接口约束里面不涉及多种数据类型, 直接用接口更好

泛型声明里面限定了 int, float 之后自然可以做加减乘除, 更复杂的操作即使有重载操作符也不够用, 我认为泛型主要是给写库的人使用的.

不熟悉模板元编程,但模仿 cpp 的话无疑会让 go 变复杂许多

接口有个很大的问题是不能只实现部分,也就说接口不适合完成 feature trait 的目标(用接口函数组来定义 concept ,往接口类型里加入新方法后,旧的已实现原来方法的但没实现新方法的类型就无法被 trait 了),但泛型可以。不过这两种语言特性都是为了实现某种形式的多态而存在的,所以很多时候确实也可以相互取代,尤其是在 golang 里语言实现它们的方式还如此类似,基本可以把泛型看做接口的某种简化形式。



> 泛型声明里面限定了 int, float 之后自然可以做加减乘除, 更复杂的操作即使有重载操作符也不够用, 我认为泛型主要是给写库的人使用的

泛型也好,接口也好,或者其他语言的面向对象多态,以及 c++的模板静多态(编译期为每种实际类型生成代码性能更好),所有这些都是为了系统设计时的公共操作抽象做语法基础。
所以不管是泛型还是接口,都是需要这些“同类”obj 具有一组相同的操作(运算符或者方法)。

就 go 而言,除去基础类型能支持运算符,对于自定义类型的 struct ,只能通过方法来实现相同操作,而对于多个类型的同名同形式方法,以接口的方式作为其他方法的参数要比以泛型的方式来定义其他方法更简洁明了,而且版本兼容性也更好。

另外就是性能,截至目前,自定义类型的泛型性能应该是不够好的,可以搜一下 “泛型会让你的 Go 代码运行变慢” 这个帖子看看,或者其他关于范型性能的帖子。
所以对于写库的人来说,如果出于性能考虑,目前的泛型也是应该放弃,连接口都不要用而是把接口访问自己元类型这层中转的指针操作也省去、自己手撸不同类型的实现才是最好的选择,而对于写库的作者,其实用不用范型节省不了太多工作量,毕竟基础设施的领域不像 CURD 那样经常搞很多需求。
当然,如果后续版本的编译器能像 c++模板那样以静多态的方式来提高性能是最好了,然后如果性能敏感的场景来替代接口的方式,也可以不用手撸多种类型的实现替代泛型了

> 不熟悉模板元编程,但模仿 cpp 的话无疑会让 go 变复杂许多

并不是模仿 c++,而是目前 go 的泛型除去基础类型范畴,既没有性能提升也没有使代码更加优雅简洁。反倒是很多人为了泛型而泛型、本来不需要泛型的地方都想用泛型,反倒把 go 搞复杂了。

#25
> 接口有个很大的问题是不能只实现部分

我觉得这样说不太正确。接口与面向对象是不同的:
1. 面向对象是自顶向下的约束,非父类本身或者子孙类无法作为祖辈参数的实力传入;
2. 接口没有向下约束,而是以更自由松散的方式、按需定以按劳分配,完全可以根据需求、抽取需要实现的方法集合作为一个单独的接口定以去作为其他公共形式的使用。可能是由于大家平时一些自定义类型需要去实现标准库已经定义了的接口,才会有这种被约束的错觉。

比如 struct A 实现了 M() X() Y(), struct B 实现了 N() X() Y(),泛型需求是需要在泛型方法里调用 X() Y()。可以定义一个 interface XY{ X() Y() } 抽取他们公共的部分,以接口的方式替代泛型,当然,这个单独的定以是一点额外的成本,但并不算复杂。

标准库的很多接口定义,是根据多年的工程实践总结出来的一些通用抽象,这些是属于基础设施、系统编程范畴的。
而相比于标准库、我们基于其上构建的,基础库也好、业务需求也好,更多的是基于自己需要来进行定义,这些自定义的约束来自自己的设计、可以灵活处理,并不需要太被标准库约束。

#27 更正
我觉得这样说不太正确。接口与面向对象是不同的:
1. 面向对象是自顶向下的约束,非父类本身或者子孙类无法作为祖辈参数的实例入参传入;
2. 接口没有强制的向下约束,而是以更自由松散的方式、按需定以按劳分配,完全可以根据需求、抽取需要实现的方法集合作为一个单独的接口定以去作为其他公共形式的使用。可能是由于大家平时一些自定义类型需要去实现标准库已经定义了的接口,才会有这种被约束的错觉。

泛型本来就不能提高性能啊, 只是减少重复工作. 一个 Max 肯定比 MaxInt64, MaxInt32…优雅

#27 你说得对,抽取 公共部分用 interface 定义 trait ,这正是 go 泛型的低层逻辑。但在绝大多数语言里这都是做不到的。定义新接口 trait 要让所有实现类重新显式继承或扩展,更何况还有多继承的问题。所以在其它绝大多数语言里,trait 的目标依靠泛型解决。

go 的设计杂糅了两种方向

#29
> 泛型本来就不能提高性能啊, 只是减少重复工作. 一个 Max 肯定比 MaxInt64, MaxInt32…优雅

你这里所述的是基础类型,我 #17 所说的 “泛型适合运算符重载之类的基础类型场景” 就是支持这样做的,这种场景我对使用泛型没有异议。

“泛型本来就不能提高性能啊” 是不正确的,只是 go 目前的泛型实现方案对自定义结构体类型没带来性能提升,但是其他一些语言是做到了的。我觉得可能是由于 go 自带 runtime 面临着更多复杂性,所以目前阶段没有支持这种优化,或许随着 go 未来版本编译器的优化也能带来这些提升。

泛型"能够"性能提升的场景主要是自定义的结构体类型相关的。
你可以先找些资料来研究下,比如面向对象多态的函数表,比如 c++模板静多态,我前面提到的 “泛型会让你的 Go 代码运行变慢”,帖子里有讲比我更专业更深入的,引用一段落:
"从历史上看,C++、D 乃至 Rust 等系统语言一直采用单态化方法实现泛型。造成这一现实的原因很多,但总体来说就是想用更长的编译时间来换取结果代码的性能提升,并且只要我们能提前把泛型代码中的类型占位符替换成最终类型、再进行编译,就可以极大优化编译流程的性能表现。装箱方法就做不到这一点。另外,我们还可以对函数调用进行去虚拟化以回避 vtable ,甚至使用内联代码实现进一步优化。"

甚至不需要去研究这些,你可以站在编译器和运行时的角度自行想象一下比如:interfaceA 的一个实例在进行方法调用时,如何通过这个实例的指针实现方法调用?
首先你得知道这个实例的类型,否则A类型调用到B类型的方法上去就乱套了,go 的指针是胖指针,指向的内容一部分是类型元信息,一部分是指向值本身。要实现实例化调用,首先要通过这个 interfaceA 实例的指针去解引用取得类型信息、找到函数表,然后再把值和函数结合起来使用。这是由 runtime 来处理的,运行时才能搞定。
而静多态 /单态化,比如 c++模板,是编译期就生成了对应类型的代码,与上面说的 interfaceA 由运行时处理对比一下,静多态 /单态化可以把 runtime 先确定类型信息这一步省掉,虽然只是一些简单的指针操作,但量大积累起来,性能差距就拉开了。


对,go 是解放了设计阶段的一些束缚,避免了面向对象的鸭嘴兽难题,避免了面对新领域尚无成型的领域设计加之快速迭代需求时顶层设计尾大不掉重构成本过高的问题。
如果项目非常复杂,新人接手之类的,没有自顶向下的类型关系图,这种自下而上地去熟悉代码也是有点心累,要靠个人阅读搜索分析代码和业务理解能力了。但相比于面向对象,这点成本其实要轻松得多。

你想表达的应该是 golang 泛型是有开销的, 而不是泛型可以提高性能. 同样两份代码, 把参数换成泛型, 不可能会提高性能. 用快排实测了一下, 722725 => 792782 ns/op, 确实慢了点.

忽然发现我的快排非泛型实现比标准库还快 5%, 以前还以为不如标准库最新版 :)



> 你想表达的应该是 golang 泛型是有开销的, 而不是泛型可以提高性能.

基础类型和非基础类型的性能影响是不一样的

> 同样两份代码, 把参数换成泛型, 不可能会提高性能

你的测试没有提供代码,我没办法做测试结论对应的代码的因果分析


我前面的描述是有说明了场景条件的,但你可能还没仔细看所以没聊到一个点上,大概总结下
1. 主要限定于自定义 struct 类型接口调用,其他语言可能是 class 多态,所以请不要用基础类型来比较性能是否提升
2. 范型本身的实现方案,有办法提高性能的策略,但要看具体语言编译器的实现,go 的目前应该是没有提高,但 c++ rust 那些应该是提高了
3. 编译器优化能够做到范型静多态 /单态化相比于接口或面向对象 class 多态提高性能不是说 go 现在已经提高了,为什么能提高,请看下 #31

如果你熟悉一些 c++,可以简单看下《深度探索 c++对象模型》,对实际业务用处不大,但是对语言这块的理解会有些帮助。编译器理论过于深入我也不懂,其他讲编程语言的书鲜有涉及这块,所以推荐这本书

把 T 换成 int ,性能提升了一点点;对于结构体数组自定义排序,结果应该是一致的,执行回调函数的时候一般来说比较的还是基础类型.

https://github.com/lxzan/dao/blob/main/algorithm/sort.go

go 泛型被人诟病缺乏优化也不少一天两天了🌚
5%以内的性能损失我还能接受,换取便利. 编译原理没深入学习过,头秃


看了下简单的基础类型泛型汇编

golang<br>// main.go<br>package main<br><br>func SumInt8(a, b int8) int8 {<br> return a + b<br>}<br><br>func SumInt32(a, b int32) int32 {<br> return a + b<br>}<br><br>func SumFloat32(a, b float32) float32 {<br> return a + b<br>}<br><br>func SumFloat64(a, b float64) float64 {<br> return a + b<br>}<br><br>func SumGenerics[T int8 | int32 | float32 | float64](a, b T) T {<br> return a + b<br>}<br><br>func main() {<br> SumInt8(1, 2)<br> SumInt32(1, 2)<br> SumFloat32(1.0, 2.0)<br> SumFloat64(1.0, 2.0)<br> SumGenerics[int8](1, 2)<br> SumGenerics[int32](1, 2)<br> SumGenerics[float32](1.0, 2.0)<br> SumGenerics(1.0, 2.0)<br>}<br>


sh<br>ubuntuubuntu:~/generics$ go tool compile -S main.go <br>ubuntuubuntu:~/generics$ go tool objdump main.o <br>

asm<br>TEXT "".SumInt8(SB) gofile../home/ubuntu/generics/main.go<br> main.go:4 0x2cb7 01d8 ADDL BX, AX <br> main.go:4 0x2cb9 c3 RET <br><br>TEXT "".SumInt32(SB) gofile../home/ubuntu/generics/main.go<br> main.go:8 0x2cba 01d8 ADDL BX, AX <br> main.go:8 0x2cbc c3 RET <br><br>TEXT "".SumFloat32(SB) gofile../home/ubuntu/generics/main.go<br> main.go:12 0x2cbd f30f58c1 ADDSS X1, X0 <br> main.go:12 0x2cc1 c3 RET <br><br>TEXT "".SumFloat64(SB) gofile../home/ubuntu/generics/main.go<br> main.go:16 0x2cc2 f20f58c1 ADDSD X1, X0 <br> main.go:16 0x2cc6 c3 RET <br><br>TEXT "".main(SB) gofile../home/ubuntu/generics/main.go<br> main.go:32 0x2cc7 c3 RET <br><br>TEXT "".SumGenerics[go.shape.int8_0](SB) gofile../home/ubuntu/generics/main.go<br> main.go:20 0x2ed6 8d040b LEAL 0(BX)(CX*1), AX <br> main.go:20 0x2ed9 c3 RET <br><br>TEXT "".SumGenerics[go.shape.int32_0](SB) gofile../home/ubuntu/generics/main.go<br> main.go:20 0x2eda 8d040b LEAL 0(BX)(CX*1), AX <br> main.go:20 0x2edd c3 RET <br><br>TEXT "".SumGenerics[go.shape.float32_0](SB) gofile../home/ubuntu/generics/main.go<br> main.go:20 0x2ede f30f58c1 ADDSS X1, X0 <br> main.go:20 0x2ee2 c3 RET <br><br>TEXT "".SumGenerics[go.shape.float64_0](SB) gofile../home/ubuntu/generics/main.go<br> main.go:20 0x2ee3 f20f58c1 ADDSD X1, X0 <br> main.go:20 0x2ee7 c3 RET <br>

int8/int32 的泛型生成的汇编是用的 LEAL 指令,非泛型的是 ADDL 指令,虽然都是一条指令但是 LEAL 内存加载应该比 ADDL 慢一点,这应该是你说的换成 int 会快一点的原因吧;
另外,泛型 float32/float64 生成的汇编与非泛型的汇编是一致的,都是 ADDSS/ADDSD 指令,所以性能应该没差别。

当然,我很乐意帮你回答关于Go语言中泛型的问题。

Go语言在1.18版本中引入了泛型,这是一个非常强大的特性,它允许你编写更加通用和可重用的代码。泛型主要通过类型参数(type parameters)和类型集(type sets)来实现。

在使用泛型时,你可能会遇到一些常见的问题。比如,如何定义一个泛型函数或泛型类型?如何限制泛型类型的范围,以确保类型安全?以及如何处理泛型类型在运行时的一些特殊行为?

对于这些问题,以下是一些基本的指导原则:

  1. 定义泛型函数或类型时,使用类型参数列表(例如[T any])来指定泛型类型。

  2. 使用类型约束(constraints)来限制泛型类型的范围。例如,你可以使用内置的any类型作为无约束的类型参数,或者使用接口类型(如interface{ ... })来定义更具体的约束。

  3. 泛型代码在编译时进行类型检查,因此你可以获得与具体类型相同的类型安全性。但是,请注意,泛型类型在运行时会被擦除(erased),因此你不能在运行时检查泛型类型的具体类型。

  4. 在处理泛型类型时,你可能需要使用类型断言(type assertion)或类型选择(type switch)来将泛型类型转换为具体类型,以便进行进一步的操作。

如果你有更具体的问题或示例代码需要解释,请提供详细信息,我会尽力给出更具体的帮助。

回到顶部