V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
aapeli
V2EX  ›  Go 编程语言

有一个 Golang 泛型的问题咨询

  •  
  •   aapeli · 2023-02-14 17:31:22 +08:00 · 2799 次点击
    这是一个创建于 673 天前的主题,其中的信息可能已经有所发展或是发生改变。

    为啥下面的 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"})
    }
    
    
    39 条回复    2023-02-15 22:32:56 +08:00
    yangheng4922
        1
    yangheng4922  
       2023-02-14 17:37:36 +08:00
    感觉应该是编译器不知道 a 是什么类型的吧
    yangheng4922
        2
    yangheng4922  
       2023-02-14 17:41:54 +08:00
    ![Snipaste_2023-02-14_17-40-35.jpg]( https://img1.imgtp.com/2023/02/14/DMsFW93P.jpg)
    aapeli
        3
    aapeli  
    OP
       2023-02-14 17:44:57 +08:00
    @yangheng4922 AI 给出的是 非泛型的写法,有泛型的么?
    Nazz
        4
    Nazz  
       2023-02-14 17:51:28 +08:00   ❤️ 2
    访问具体的数据要用接口约束;

    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"})
    }
    timethinker
        5
    timethinker  
       2023-02-14 17:56:20 +08:00
    我没用过 Go 的泛型,但是从实际使用的场景来看,Test 和 TestNext 是没有任何关系的,虽然都有一个成员 V 并且类型一致,但是编译器并不能直接将两者进行关联,所以重点是要让 Test 和 TestNext 产生关系,可以改造一下使这两个都实现同一个合同或者接口(如果 Go 支持其中之一的话)。
    Mexion
        6
    Mexion  
       2023-02-14 18:08:01 +08:00
    Go 只认接口
    GogoGo666
        7
    GogoGo666  
       2023-02-14 18:11:54 +08:00
    @yangheng4922 #2 ChatGPT 了解的 go 的最新版本是 1.16
    lawlielt
        8
    lawlielt  
       2023-02-14 18:15:14 +08:00
    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.
    DefoliationM
        9
    DefoliationM  
       2023-02-14 18:47:49 +08:00 via Android
    看看 go 泛型的文档,写个接口。
    aapeli
        10
    aapeli  
    OP
       2023-02-14 18:52:56 +08:00
    感谢各位 @DefoliationM @Mexlon @timethinker @yangheng4922
    最终我选择了如下的写法, 为什么转成 any 类型,不直接使用 any 作为参数 a 的类型?
    因为我想只限制使用 Test | TestNext 这两个类型, 这样 handle 函数只能传指定的类型.
    参考: https://stackoverflow.com/questions/73864711/get-type-parameter-from-a-generic-struct-using-reflection


    ```
    package main

    import "fmt"

    type Test struct {
    V string
    }

    type TestNext struct {
    V string
    }

    type Interface interface {
    Test | TestNext
    }

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

    func main() {
    handle(Test{V: "Hello"})
    }

    ```
    rrfeng
        11
    rrfeng  
       2023-02-14 19:28:32 +08:00
    可以用复合接口满足你这个需求:

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

    就不用做 type assertion 了
    liuxu
        12
    liuxu  
       2023-02-14 19:34:23 +08:00
    我建议你按 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"})
    }
    hahadaxigua834
        13
    hahadaxigua834  
       2023-02-14 19:58:52 +08:00
    go 现在不支持这种写法,https://github.com/golang/go/issues/48522
    rrfeng
        14
    rrfeng  
       2023-02-14 20:07:30 +08:00
    @hahadaxigua834
    这个 issue 里提供了十几种写法,学到了!
    none
        15
    none  
       2023-02-14 20:27:10 +08:00
    @Nazz
    用接口约束的话,那这里泛型好像可以去掉了,直接定义接口类型的参数不是更简单
    func handle(a Value) {
    fmt.Println(a.GetValue())
    }
    Nazz
        16
    Nazz  
       2023-02-14 21:12:52 +08:00 via Android
    @none 提问者想要的只是编译期类型检查
    lesismal
        17
    lesismal  
       2023-02-14 22:57:00 +08:00
    @Nazz
    如果入参不是对应具体 interface{...}类型也是编译期就失败的。

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

    再根据我浅薄的 go 泛型使用经验(不超过 10 分钟),能用接口、切面解决的问题不要用泛型就是最优解。
    ——不一定对,仅供各位参考
    Nazz
        18
    Nazz  
       2023-02-14 23:18:39 +08:00 via Android
    @lesismal 泛型主要是用来实现数据结构与算法的. 有时候也需要在泛型方法里面转成 interface{}再断言
    GeruzoniAnsasu
        19
    GeruzoniAnsasu  
       2023-02-15 02:37:44 +08:00   ❤️ 2
    这个问题之前讨论过
    /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
    lysS
        20
    lysS  
       2023-02-15 09:59:08 +08:00
    @Nazz 这样子还不如直接用接口
    lesismal
        21
    lesismal  
       2023-02-15 11:26:54 +08:00
    @Nazz #18
    对比下 c++,因为 c++有运算符重载,所以模板这种泛型里类也可以直接加减乘除这些运算符、因此不需要使用方法如 a.Sum(b)。而 go 里没有运算符重载,很多算法数据结构里即使使用泛型,仍然需要以接口的形式由 struct 实现接口,然后用 a.Sum(b)之类的函数调用方式来处理。显而易见,既然这种地方已经使用接口方法了,这里的泛型就是多余的了。
    多数时候是在使用基础类型为主的地方,使用泛型可以节约代码、不用为每种类型都去实现一个 /一组方法或者单个方法内再做类型转换。而本来接口、切面就能做的地方,泛型是画蛇添足。

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

    自从支持了泛型以后,不少人为了用泛型而用泛型,让 go 代码变得更丑陋了,很不希望看到一些大厂“架构师”又来污染放毒,因为太多小白会跟风,最终劣币驱逐良币。
    Nazz
        22
    Nazz  
       2023-02-15 11:54:30 +08:00
    @lysS 是的, 如果泛型的接口约束里面不涉及多种数据类型, 直接用接口更好
    Nazz
        23
    Nazz  
       2023-02-15 12:01:10 +08:00
    @lesismal 泛型声明里面限定了 int, float 之后自然可以做加减乘除, 更复杂的操作即使有重载操作符也不够用, 我认为泛型主要是给写库的人使用的.
    Nazz
        24
    Nazz  
       2023-02-15 12:15:10 +08:00 via Android
    @lesismal 不熟悉模板元编程,但模仿 cpp 的话无疑会让 go 变复杂许多
    GeruzoniAnsasu
        25
    GeruzoniAnsasu  
       2023-02-15 13:06:08 +08:00
    @lesismal 接口有个很大的问题是不能只实现部分,也就说接口不适合完成 feature trait 的目标(用接口函数组来定义 concept ,往接口类型里加入新方法后,旧的已实现原来方法的但没实现新方法的类型就无法被 trait 了),但泛型可以。不过这两种语言特性都是为了实现某种形式的多态而存在的,所以很多时候确实也可以相互取代,尤其是在 golang 里语言实现它们的方式还如此类似,基本可以把泛型看做接口的某种简化形式。
    lesismal
        26
    lesismal  
       2023-02-15 13:10:15 +08:00
    @Nazz

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

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

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

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

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

    并不是模仿 c++,而是目前 go 的泛型除去基础类型范畴,既没有性能提升也没有使代码更加优雅简洁。反倒是很多人为了泛型而泛型、本来不需要泛型的地方都想用泛型,反倒把 go 搞复杂了。
    lesismal
        27
    lesismal  
       2023-02-15 13:26:10 +08:00
    @GeruzoniAnsasu #25
    > 接口有个很大的问题是不能只实现部分

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

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

    标准库的很多接口定义,是根据多年的工程实践总结出来的一些通用抽象,这些是属于基础设施、系统编程范畴的。
    而相比于标准库、我们基于其上构建的,基础库也好、业务需求也好,更多的是基于自己需要来进行定义,这些自定义的约束来自自己的设计、可以灵活处理,并不需要太被标准库约束。
    lesismal
        28
    lesismal  
       2023-02-15 13:28:02 +08:00
    @lesismal #27 更正
    我觉得这样说不太正确。接口与面向对象是不同的:
    1. 面向对象是自顶向下的约束,非父类本身或者子孙类无法作为祖辈参数的实例入参传入;
    2. 接口没有强制的向下约束,而是以更自由松散的方式、按需定以按劳分配,完全可以根据需求、抽取需要实现的方法集合作为一个单独的接口定以去作为其他公共形式的使用。可能是由于大家平时一些自定义类型需要去实现标准库已经定义了的接口,才会有这种被约束的错觉。
    Nazz
        29
    Nazz  
       2023-02-15 14:56:36 +08:00
    @lesismal 泛型本来就不能提高性能啊, 只是减少重复工作. 一个 Max 肯定比 MaxInt64, MaxInt32...优雅
    GeruzoniAnsasu
        30
    GeruzoniAnsasu  
       2023-02-15 15:44:42 +08:00
    @lesismal #27 你说得对,抽取 公共部分用 interface 定义 trait ,这正是 go 泛型的低层逻辑。但在绝大多数语言里这都是做不到的。定义新接口 trait 要让所有实现类重新显式继承或扩展,更何况还有多继承的问题。所以在其它绝大多数语言里,trait 的目标依靠泛型解决。

    go 的设计杂糅了两种方向
    lesismal
        31
    lesismal  
       2023-02-15 15:54:02 +08:00
    @Nazz #29
    > 泛型本来就不能提高性能啊, 只是减少重复工作. 一个 Max 肯定比 MaxInt64, MaxInt32...优雅

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

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

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

    甚至不需要去研究这些,你可以站在编译器和运行时的角度自行想象一下比如:interfaceA 的一个实例在进行方法调用时,如何通过这个实例的指针实现方法调用?
    首先你得知道这个实例的类型,否则A类型调用到B类型的方法上去就乱套了,go 的指针是胖指针,指向的内容一部分是类型元信息,一部分是指向值本身。要实现实例化调用,首先要通过这个 interfaceA 实例的指针去解引用取得类型信息、找到函数表,然后再把值和函数结合起来使用。这是由 runtime 来处理的,运行时才能搞定。
    而静多态 /单态化,比如 c++模板,是编译期就生成了对应类型的代码,与上面说的 interfaceA 由运行时处理对比一下,静多态 /单态化可以把 runtime 先确定类型信息这一步省掉,虽然只是一些简单的指针操作,但量大积累起来,性能差距就拉开了。
    lesismal
        32
    lesismal  
       2023-02-15 16:05:56 +08:00
    @GeruzoniAnsasu
    对,go 是解放了设计阶段的一些束缚,避免了面向对象的鸭嘴兽难题,避免了面对新领域尚无成型的领域设计加之快速迭代需求时顶层设计尾大不掉重构成本过高的问题。
    如果项目非常复杂,新人接手之类的,没有自顶向下的类型关系图,这种自下而上地去熟悉代码也是有点心累,要靠个人阅读搜索分析代码和业务理解能力了。但相比于面向对象,这点成本其实要轻松得多。
    Nazz
        33
    Nazz  
       2023-02-15 17:34:12 +08:00
    @lesismal 你想表达的应该是 golang 泛型是有开销的, 而不是泛型可以提高性能. 同样两份代码, 把参数换成泛型, 不可能会提高性能. 用快排实测了一下, 722725 => 792782 ns/op, 确实慢了点.
    Nazz
        34
    Nazz  
       2023-02-15 17:37:37 +08:00
    @Nazz 忽然发现我的快排非泛型实现比标准库还快 5%, 以前还以为不如标准库最新版 :)
    lesismal
        35
    lesismal  
       2023-02-15 18:59:20 +08:00   ❤️ 1
    @Nazz

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

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

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

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


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

    如果你熟悉一些 c++,可以简单看下《深度探索 c++对象模型》,对实际业务用处不大,但是对语言这块的理解会有些帮助。编译器理论过于深入我也不懂,其他讲编程语言的书鲜有涉及这块,所以推荐这本书
    Nazz
        36
    Nazz  
       2023-02-15 19:18:29 +08:00 via Android
    @lesismal 把 T 换成 int ,性能提升了一点点;对于结构体数组自定义排序,结果应该是一致的,执行回调函数的时候一般来说比较的还是基础类型.

    https://github.com/lxzan/dao/blob/main/algorithm/sort.go
    Nazz
        37
    Nazz  
       2023-02-15 19:24:01 +08:00 via Android
    @lesismal go 泛型被人诟病缺乏优化也不少一天两天了🌚
    5%以内的性能损失我还能接受,换取便利. 编译原理没深入学习过,头秃
    lesismal
        38
    lesismal  
       2023-02-15 22:27:44 +08:00   ❤️ 1
    @Nazz
    看了下简单的基础类型泛型汇编

    ```golang
    // main.go
    package main

    func SumInt8(a, b int8) int8 {
    return a + b
    }

    func SumInt32(a, b int32) int32 {
    return a + b
    }

    func SumFloat32(a, b float32) float32 {
    return a + b
    }

    func SumFloat64(a, b float64) float64 {
    return a + b
    }

    func SumGenerics[T int8 | int32 | float32 | float64](a, b T) T {
    return a + b
    }

    func main() {
    SumInt8(1, 2)
    SumInt32(1, 2)
    SumFloat32(1.0, 2.0)
    SumFloat64(1.0, 2.0)
    SumGenerics[int8](1, 2)
    SumGenerics[int32](1, 2)
    SumGenerics[float32](1.0, 2.0)
    SumGenerics(1.0, 2.0)
    }
    ```


    ```sh
    ubuntu@ubuntu:~/generics$ go tool compile -S main.go
    ubuntu@ubuntu:~/generics$ go tool objdump main.o
    ```

    ```asm
    TEXT "".SumInt8(SB) gofile../home/ubuntu/generics/main.go
    main.go:4 0x2cb7 01d8 ADDL BX, AX
    main.go:4 0x2cb9 c3 RET

    TEXT "".SumInt32(SB) gofile../home/ubuntu/generics/main.go
    main.go:8 0x2cba 01d8 ADDL BX, AX
    main.go:8 0x2cbc c3 RET

    TEXT "".SumFloat32(SB) gofile../home/ubuntu/generics/main.go
    main.go:12 0x2cbd f30f58c1 ADDSS X1, X0
    main.go:12 0x2cc1 c3 RET

    TEXT "".SumFloat64(SB) gofile../home/ubuntu/generics/main.go
    main.go:16 0x2cc2 f20f58c1 ADDSD X1, X0
    main.go:16 0x2cc6 c3 RET

    TEXT "".main(SB) gofile../home/ubuntu/generics/main.go
    main.go:32 0x2cc7 c3 RET

    TEXT "".SumGenerics[go.shape.int8_0](SB) gofile../home/ubuntu/generics/main.go
    main.go:20 0x2ed6 8d040b LEAL 0(BX)(CX*1), AX
    main.go:20 0x2ed9 c3 RET

    TEXT "".SumGenerics[go.shape.int32_0](SB) gofile../home/ubuntu/generics/main.go
    main.go:20 0x2eda 8d040b LEAL 0(BX)(CX*1), AX
    main.go:20 0x2edd c3 RET

    TEXT "".SumGenerics[go.shape.float32_0](SB) gofile../home/ubuntu/generics/main.go
    main.go:20 0x2ede f30f58c1 ADDSS X1, X0
    main.go:20 0x2ee2 c3 RET

    TEXT "".SumGenerics[go.shape.float64_0](SB) gofile../home/ubuntu/generics/main.go
    main.go:20 0x2ee3 f20f58c1 ADDSD X1, X0
    main.go:20 0x2ee7 c3 RET
    ```

    int8/int32 的泛型生成的汇编是用的 LEAL 指令,非泛型的是 ADDL 指令,虽然都是一条指令但是 LEAL 内存加载应该比 ADDL 慢一点,这应该是你说的换成 int 会快一点的原因吧;
    另外,泛型 float32/float64 生成的汇编与非泛型的汇编是一致的,都是 ADDSS/ADDSD 指令,所以性能应该没差别。
    lesismal
        39
    lesismal  
       2023-02-15 22:32:56 +08:00
    @Nazz
    v 站回复的 markdown fmt 感人,看这里吧:
    https://gist.github.com/lesismal/0cee2bb8d76c7f907a8e2a8401b4fd41
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1124 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 31ms · UTC 18:23 · PVG 02:23 · LAX 10:23 · JFK 13:23
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.