Go 切片 (slice) 类型转换

目录

刚刚接触 Go 的同学常见问题:需要将一个 []string 类型的变量转换成 []interface{} 类型,这个操作看似简单,但事实上由于 Go 没有在语法层面的支持,我们需要额外的代码去实现。下面我会介绍这种切片类型转换的最佳实践。

Go 的类型转换入门

首先我们先介绍一下 Go 的类型转换语法:

var i32 int32                // 定义一个 32 位整型变量
var i64 int64                // 定义一个 64 位整形变量

i64 = int64(i32)             // 将 32 位整型变量转换为 64 位整形变量

所以 Go 的类型转换语法为 T()

与大多数语言不同的是,Go 在进行类型转换的时候,编译器会对类型的底层结构进行比对,如果结构无法对应,则编译错误。这从一定程度上减少了运行时错误。例如:

type Man struct {
	name string
	age  int
}

type Woman struct {
	name string
	age  int
}

type Dog struct {
	nickname string
	age      int
}

type Cat struct {
	name string
	age  int8
}

func main() {
	var man Man
	var woman Woman
	var dog Dog
	var cat Cat

	man = Man(woman) // 转换成功
	man = Man(dog)   // 编译错误,因为字段名称不一致
	man = Man(cat)   // 编译错误,因为字段类型不一致
}

需要注意一个特殊的类型 interface{}(类型别名 any)。Go 的任意类型都可以转换成 interface{} 类型(这有点像 Java 中的 Object 类型):

var iface interface{}  // 定义一个 interface{} 类型变量
var i int              // 定义一个 int 类型变量

iface = i              // 将 int 类型变量赋值给 interface{} 类型变量
iface = interface{}(i) // 与上一行代码等效

当要把 interface{} 类型还原成具体类型的时候,就需要用到 Go 的 多返回值 语法:

var iface interface{} // 定义一个 interface{} 类型变量
var i int             // 定义一个 int 类型变量

iface = i
ii, ok := iface.(int64) // 因为iface的实际类型为 int,所以返回的 ok 值为 "false",ii 为 int64 的“零”值 0
iii := iface.(int64)    // 因为iface的实际类型为 int,不是 int64,所以 panic 异常

第 5 行和第 6 行的区别在于接收的返回值个数不一样。

严格来说 iface.(T) 并不是类型转换,而是 Go 在语言层面针对 interface{} 做的另一种设计 “断言”。 interface{} 类型不能使用 T() 语法转换成其它类型。想要了解更多底层的实现可以参考这篇文档

切片类型转换

回到本文主题:切片(slice)类型怎么转换?比如 []int 要怎么转换成 []interface{},我们尝试进行转换:

var ifaces []interface{} // 定义一个 interface{} 切片类型变量
var ints []int           // 定义一个 int 切片类型变量

ifaces = ints            // 编译错误,cannot use ints (variable of type []int) as []interface{} value in assignment
ii, ok := ifaces.([]int) // 编译错误,invalid operation: ifaces (variable of type []interface{}) is not an interface
iii := ifaces.([]int)    // 编译错误,invalid operation: ifaces (variable of type []interface{}) is not an interface

为什么把 []MyType 转换成 []interface{} 编译会报错? Go 官方文档 在技术原理上给出了解释:

这有两个主要原因。

首先是类型为 []interface{} 的变量不是接口! 它是一个元素类型恰好是 interface{} 的切片。 但即便如此,人们可能会说意思很清楚。

嗯,是吗? []interface{} 类型的变量具有特定的内存布局,在编译时已知。

每个 interface{} 占用两个词(一个词表示所包含内容的类型,另一个词表示包含的数据或指向它的指针)。 因此,长度为 N 且类型为 []interface{} 的切片由 N*2 个字长的数据块支持。

这与支持具有 []MyType 类型和相同长度的切片的数据块不同。 它的数据块将是 N*sizeof(MyType) 个字长。

结果是您不能快速将 []MyType 类型的东西分配给 []interface{} 类型的东西; 他们背后的数据看起来不同。

官方文档的解释,简单来说就是:无法把 []MyType 类型转换成 []interface{} 的原因是 interface{} 数据结构是固定的,而我们自定义的类型的数据结构是不固定的,所以无法直接进行转换。

但是这却无法解释我们为什么不能把 []MyType1 类型转换成 []MyType2 类型,看下面的示例:

type Man struct {
	name string
	age  int
}

type Woman struct {
	name string
	age  int
}

func main() {
	var men []Man
	var women []Woman

	men = []Man(women) // 编译错误,cannot convert women (variable of type []Woman) to []Man
}

ManWoman 都是我们自己的定义的,可以相互转换的类型,但是 []Man[]Woman 之间却不能转换,这显然已经不是官方文档描述的和 interface{} 的特殊数据结构问题了。

那么到底有没有办法把两种类型的切换互相转换呢?很遗憾,在 Go 的语言层面也没有提供支持

理论上,这些切片类型的转换,可以在编译器层面通过语法糖实现。但是基于一个设计原则:syntax should not hide complex/costly operations(语法不应该掩盖复杂或者昂贵的运算),Go 并没有设计语法上的支持。

而相应的 Go 官方文档也只有一个建议就是循环转换每一个元素:

var dataSlice []int = foo()
var interfaceSlice []interface{} = make([]interface{}, len(dataSlice))
for i, d := range dataSlice {
	interfaceSlice[i] = d
}

而我们提出的 []Man[]Woman 也是和官方建议一样的操作,循环转换。

事情到这里已经基本结束,现在没有,以后也几乎不会有这种语法糖的出现。

转 []interface{} 的通用函数?

如果有这样的需求,经常要把 []FirstType[]SecondType[]ThirdType … 等各种类型转换成 []interface{},想通过一个通用函数,减少重复代码,可行么?可以,并且有两种实现:

反射实现

注意,如果您需要在数据量大的情况下使用此方法就要小心了,这个使用了 reflect (反射)库的函数性能比直接循环的要低 20% 左右!

func InterfaceSlice(slice interface{}) []interface{} {
    s := reflect.ValueOf(slice)
    if s.Kind() != reflect.Slice {
        panic("InterfaceSlice() given a non-slice type")
    }

    // Keep the distinction between nil and empty slice input
    if s.IsNil() {
        return nil
    }

    ret := make([]interface{}, s.Len())

    for i:=0; i<s.Len(); i++ {
        ret[i] = s.Index(i).Interface()
    }

    return ret
}

泛型实现

如果您使用的是 go1.18 及以上的版本,那么很幸运,使用泛型实现这个函数,性能损失很小(经测试大概只变慢 1% 左右):

func InterfaceSlice[T interface{}](slice []T) []interface{} {
	res := make([]interface{}, len(slice))
	for i, v := range slice {
		res[i] = v
	}
	return res
}

最佳实践

  • 如果需要转换两个不同类型的切片,使用循环。
  • 如果是需要任意类型切片转 []interface{} 的可以使用反射或者泛型实现的通用方法。但是切记,使用反射性能会降低 20% 左右。

参考