Golang中为什么这个泛型函数无法正常工作?

Golang中为什么这个泛型函数无法正常工作? A 行处的代码无法工作(使用 -G=3 编译的 go1.17.1) 错误信息是:t.Id undefined (type bound for T has no method Id)

package main
import "reflect"

type a struct {
  Id uint
}

type b struct {
  Id int
}

func generic[T any](t T) {
  typ := reflect.TypeOf(t)
  println(typ.String())
  for idx := 0 ; idx < typ.NumField() ; idx++ {
    println(idx,typ.Field(idx).Name)
  }
  println(t.Id) // A 这一行无法工作
}

func main() {
  generic(a{10})
  generic(b{10})
}

更多关于Golang中为什么这个泛型函数无法正常工作?的实战教程也可以访问 https://www.itying.com/category-94-b0.html

7 回复

在使用泛型时,使用反射也没有太大意义。泛型的目的之一就是为了避免使用反射,还有其他用例。即使在使用泛型时,接口(和契约)对于定义泛型类型的行为也会有很大帮助。

更多关于Golang中为什么这个泛型函数无法正常工作?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


关键在于 func generic 需要在编译时了解 t 的能力

根据我对泛型的理解,如果我们有这样一个泛型函数:

func generic[T any](t T) {
...
}

并且当我们用不同的类型调用它时,像这样:

generic(a{1})
generic(b{1})

编译器应该能从参数类型推断出 T,并用推断出的类型实例化一个 generic 的副本,所有这些都可以在编译时完成。这应该能正常工作。

你好 @oldoldman,

参数 t 可以是任何类型(因为 [T any]),因此无法对其真实类型做出任何假设。

为了让函数 generic 能够利用 ab 的具体类型和行为,你可以使用结构体方法和类型约束:

type a struct {
  id uint
}

func (a a) Id() uint {
	return a.id
}

...

// 这是类型约束
type Ider interface {
	Id() uint
}

// 将一个 Ider 传递给 func generic
func generic[T Ider](t T) {
...
  println(t.Id())
}
...

完整代码在此:

https://go2goplay.golang.org/p/k5SaFVEQjz_E

感谢回复。但如果我们引入 Ider 接口,generic 函数就无需使用泛型了,我们可以直接这样写:

func generic(t Ider) {
...
}

实际上,我有以下用例,我希望将 dump32/dump64 抽象为一个通用的 dump 函数:

package main
import "debug/pe"

func dump32(x *pe.OptionalHeader32) {
  // 相同的逻辑
}
func dump64(x *pe.OptionalHeader64) {
  // 相同的逻辑
}
func main() {
  file,err := pe.Open("test.exe")
  if err == nil {
    switch x:=file.OptionalHeader.(type) {
      case *pe.OptionalHeader64:
        dump64(x)
      case *pe.OptionalHeader32:
        dump32(x)
      default:
    }
  }
}

换个角度来看这个问题:

假设你的 func generic 在一个名为 genericfunc 的公共包中(导出为 func Generic)。其他人正在使用它。现在你决定更改函数内部的某些内容,例如将 println(t.Id) 这一行改为:

   fmt.Println("%20d\n", t.Id)

因为你希望 ID 能够右对齐。这个更改对于你的结构体 ab 来说效果很好。

与此同时,你包的其他用户编写了以下代码:

type c struct {
   Id string
}

func main() {
    genericfunc.Generic(c{Id: "y87Zo9pX6"}) 
}

这对于之前的 println(t.Id) 来说运行良好,但对于新的 fmt.Printf("%d",...) 就不太适用了。

一旦你的更改发布并被客户端导入,他们的代码将立即崩溃。

如果使用了类型约束,像这样的破坏性更改就不可能发生。

确实,仅使用一个 Ider 接口也能解决问题。

关键点:使用尽可能简单的解决方案。

如果你在泛型的上下文中寻找解决方案,要点在于 func generic 需要在编译时知道 t 的能力。T any 可以是任何类型,无论其是否包含 Id 字段,因此 t.Id 可能在运行时失败,所以是无效的。一个恰当的类型约束会告诉编译器 t 是一个内部包含 Id 的类型。

顺便说一下,我在之前的回复中犯了一个错误。我忽略了 ab 包含不同的类型。对于这种情况,intuint 的并集可以构建一个与 Ider 一起使用的类型约束,就像这段代码中一样。然而,正如 @hollowaykeanho 正确指出的那样,go2go playground 尚不支持并集语法,因此这段代码(目前?)无法运行。

话虽如此,我会选择非泛型、仅使用接口的解决方案。

编辑补充:对于你的特定用例,像 @hollowaykeanho 解决方案中那样使用类型开关似乎是可行的,或者考虑使用反射,类似于 go-spew 这样的转储工具所做的那样

问题出在泛型约束上。你使用了 T any 作为约束,这意味着 T 可以是任何类型,但编译器无法保证类型 T 具有 Id 字段。

要解决这个问题,你需要定义一个接口约束来要求类型必须包含 Id 字段。以下是修正后的代码:

package main

import "reflect"

type a struct {
    Id uint
}

type b struct {
    Id int
}

// 定义接口约束
type HasId interface {
    ~struct { Id uint } | ~struct { Id int }
}

func generic[T HasId](t T) {
    typ := reflect.TypeOf(t)
    println(typ.String())
    for idx := 0; idx < typ.NumField(); idx++ {
        println(idx, typ.Field(idx).Name)
    }
    println(t.Id) // 现在可以正常工作
}

func main() {
    generic(a{10})
    generic(b{10})
}

或者,如果你希望更灵活地处理不同类型的 Id 字段,可以使用类型断言:

package main

import "reflect"

type a struct {
    Id uint
}

type b struct {
    Id int
}

// 定义包含Id方法的接口
type IDer interface {
    GetID() interface{}
}

// 为a类型实现GetID方法
func (x a) GetID() interface{} {
    return x.Id
}

// 为b类型实现GetID方法
func (x b) GetID() interface{} {
    return x.Id
}

func generic[T IDer](t T) {
    typ := reflect.TypeOf(t)
    println(typ.String())
    for idx := 0; idx < typ.NumField(); idx++ {
        println(idx, typ.Field(idx).Name)
    }
    println(t.GetID()) // 通过接口方法访问Id
}

func main() {
    generic(a{10})
    generic(b{10})
}

在第一个修正方案中,HasId 约束使用类型集(type set)来指定 T 必须是包含特定 Id 字段的结构体。第二个方案则通过接口方法提供更通用的解决方案。

回到顶部