在具有基础字段的任何结构数组上调用方法

Call method on any array of structs that have underlying field

假设我有一堆结构(大约 10 个)。

type A struct {
    ID int64
    ... other A-specific fields
}

type B struct {
    ID int64
    ... other B-specific fields
}

type C struct {
    ID int64
    ... other C-specific fields
}

如果我在任何给定时间都有这些结构的数组([]A[]B[]C),我该如何编写一个提取 ID 的函数从结构数组中不写 3(或者在我的例子中,10)这样的单独函数:

type AList []A
type BList []B
type CList []C

func (list *AList) GetIDs() []int64 { ... }
func (list *BList) GetIDs() []int64 { ... }
func (list *CList) GetIDs() []int64 { ... }

泛型方法需要使用接口和反射。

据我所知,没有简单的方法。

您可能想使用 embedding,但我不确定是否有任何方法可以使这项特定任务变得更容易。嵌入感觉就像子类化,但它并没有给你多态性的力量。

Go 中的多态性仅限于方法和接口,而不是字段,因此您不能跨多个 类.

按名称访问给定字段

您可以使用 reflection 通过名称(或标签)查找和访问您感兴趣的字段,但这样做会降低性能,并且会使您的代码变得复杂且难以理解。反射并不是真正打算替代多态或泛型。

我认为你最好的解决方案是使用 Go 为你提供的多态性,并创建一个接口:

type IDable interface {
    GetId() int64
}

并为每个 类 创建一个 GetId 方法。 Full example.

使用切片本身的通用方法

如果您定义一个通用接口来访问切片的第i元素的ID,您可以使它更简单一些:

type HasIDs interface {
    GetID(i int) int64
}

并且您为这些提供实现:

func (x AList) GetID(i int) int64 { return x[i].ID }
func (x BList) GetID(i int) int64 { return x[i].ID }
func (x CList) GetID(i int) int64 { return x[i].ID }

然后一个GetID()函数就够了:

func GetIDs(s HasIDs) (ids []int64) {
    ids = make([]int64, reflect.ValueOf(s).Len())
    for i := range ids {
        ids[i] = s.GetID(i)
    }
    return
}

注意:切片的长度可能是GetIDs()的参数,也可能是HasIDs接口的一部分。两者都比获取切片长度的微小反射调用更复杂,所以请耐心等待。

使用它:

as := AList{A{1}, A{2}}
fmt.Println(GetIDs(as))

bs := BList{B{3}, B{4}}
fmt.Println(GetIDs(bs))

cs := []C{C{5}, C{6}}
fmt.Println(GetIDs(CList(cs)))

输出(在 Go Playground 上尝试):

[1 2]
[3 4]
[5 6]

请注意,我们可以使用 AListBList 等类型的切片,我们不需要使用 interface{}[]SomeIface。另请注意,我们也可以使用例如a []C,传给GetIDs()时,我们用了一个简单的类型conversion.

这很简单。如果你想消除切片的 GetID() 方法,那么你真的需要更深入地研究反射(reflect 包),它会更慢。上面提供的解决方案与 "hard-coded" 版本的性能大致相同。

完全反射

如果你希望它完全"generic",你可以使用反射来完成,然后你绝对不需要任何额外的方法。

不检查错误,这里是解决方案:

func GetIDs(s interface{}) (ids []int64) {
    v := reflect.ValueOf(s)
    ids = make([]int64, v.Len())
    for i := range ids {
        ids[i] = v.Index(i).FieldByName("ID").Int()
    }
    return
}

测试和输出(几乎)相同。请注意,由于此处 GetIDs() 的参数类型为 interface{},因此您无需转换为 CList 即可传递类型 []C 的值。在 Go Playground.

上试用

有嵌入和反射

通过将其名称指定为 string 来获取字段非常脆弱(例如考虑重命名/重构)。如果我们 "outsource" ID 字段和一个访问器方法到一个单独的 struct,我们可以提高可维护性、安全性和反射的性能,我们将嵌入它,并捕获访问器通过界面:

type IDWrapper struct {
    ID int64
}

func (i IDWrapper) GetID() int64 { return i.ID }

type HasID interface {
    GetID() int64
}

所有类型都嵌入 IDWrapper:

type A struct {
    IDWrapper
}

type B struct {
    IDWrapper
}

type C struct {
    IDWrapper
}

通过嵌入,所有嵌入器类型(ABC)都将提升 GetID() 方法,因此它们都会自动实现 HasID.我们可以在 GetIDs() 函数中利用这一点:

func GetIDs(s interface{}) (ids []int64) {
    v := reflect.ValueOf(s)
    ids = make([]int64, v.Len())
    for i := range ids {
        ids[i] = v.Index(i).Interface().(HasID).GetID()
    }
    return
}

正在测试:

as := AList{A{IDWrapper{1}}, A{IDWrapper{2}}}
fmt.Println(GetIDs(as))

bs := BList{B{IDWrapper{3}}, B{IDWrapper{4}}}
fmt.Println(GetIDs(bs))

cs := []C{C{IDWrapper{5}}, C{IDWrapper{6}}}
fmt.Println(GetIDs(cs))

输出相同。在 Go Playground 上试试吧。请注意,在这种情况下,唯一的方法是 IDWrapper.GetID(),不需要定义其他方法。