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
}
Man
和 Woman
都是我们自己的定义的,可以相互转换的类型,但是 []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% 左右。