如何在同一个函数中接受不同类型的切片?

How to accept different types of slices in the same function?

我有一个函数 removeFrom 可以从切片中删除一个项目。它接受一个 float64 切片和一个索引:

func removeFrom(slice []float64, index int) []float64 {
    if len(slice) > index {
        return append(slice[:index], slice[index+1:]...)
    }
}

它工作正常,但现在我还必须从整数切片中删除。那么我该如何更改它以接受这两种类型(以及 return 给定类型的一部分)?我尝试使用空接口,但显然我必须在函数内部做一些转换,但我没有找到如何做。

简答?你不能。

答案很长,你仍然不能直接这样做,但是:

func removeFrom(slice interface{}, index int) interface{} {
    switch slice := slice.(type) {
    case []float64:
        if len(slice) > index {
            return append(slice[:index], slice[index+1:]...)
        }
    case []int64:
        if len(slice) > index {
            return append(slice[:index], slice[index+1:]...)
        }
    case []int:
        if len(slice) > index {
            return append(slice[:index], slice[index+1:]...)
        }
    default:
        log.Panicf("unknown type: %T", slice)
    }
}

Go 不支持泛型,没有 "common ancestor" 适用于所有切片类型([]interface{} 不是 "compatible" with []int 例如,参见 Cannot convert []string to []interface {}了解更多详情)。

因此,如果您希望您的函数接受任何切片类型,则必须使用 interface{}(对于 "incoming" 参数和 return 类型)。但是现在您有一个(接口)包装器值,您不能对其应用切片,也不能将其传递给内置 append() 函数。

您可以对已知类型使用 type assertion and type switches,但您必须为每个类型重复代码,所以这并不是真正的领先。

实际上有一种方法可以创建适用于所有切片类型的 removeFrom() 函数,使用 反射.

reflect.Value 是描述任何 Go 值的类型。它具有针对不同类型的 Go 值的支持方法,包括切片。

我们感兴趣的是Value.Slice()方法:

func (v Value) Slice(i, j int) Value

我们可以用它来切一片。好的。这是我们的元素移除算法中的一个关键点。还需要 "join" 2 个切片,一个在可移动元素之前,一个在可移动元素之后。幸运的是 reflect 包也支持这个:reflect.AppendSlice():

func AppendSlice(s, t Value) Value

作为最后剩下的键,我们可以使用Value.Len()来获取任何切片的长度。

我们现在拥有了通用 removeFrom() 函数所需的一切,它非常简单:

func removeFrom(s interface{}, idx int) interface{} {
    if v := reflect.ValueOf(s); v.Len() > idx {
        return reflect.AppendSlice(v.Slice(0, idx), v.Slice(idx+1, v.Len())).Interface()
    }
    return s
}

真的,仅此而已。测试它:

for i := 0; i < 4; i++ {
    fmt.Println(removeFrom([]int{0, 1, 2}, i), "missing:", i)
}
for i := 0; i < 4; i++ {
    fmt.Println(removeFrom([]string{"zero", "one", "two"}, i), "missing:", i)
}

输出(在 Go Playground 上尝试):

[1 2] missing: 0
[0 2] missing: 1
[0 1] missing: 2
[0 1 2] missing: 3
[one two] missing: 0
[zero two] missing: 1
[zero one] missing: 2
[zero one two] missing: 3

备注:

此解决方案使用反射,因此它会比另一个不使用反射但具有具体的受支持类型的解决方案慢 "wired in"。快速基准测试表明,这种通用解决方案比使用有线输入类型的非反射慢 2.5 倍。应权衡性能或 convenience/general 解决方案是否更重要。或者你可以将它与具体类型结合起来:你可以添加一个类型开关来处理频繁的类型,并且只有在实际的具体类型没有被类型开关处理时才恢复到这个通用解决方案。