Go数组和切片
数组
几乎所有计算机语言,数组的实现都是相似的:一段连续的内存,Go语言也一样,Go语言的数组底层实现就是一段连续的内存空间。每个元素有唯一一个索引(或者叫下标)来访问。如下图所示,下图是 [5]int{1:10, 2:20} 数组的内部实现逻辑图:
由于内存连续,CPU很容易计算索引(即数组的下标),可以快速迭代数组里的所有元素。
Go语言的数组不同于C语言或者其他语言的数组,C语言的数组变量是指向数组第一个元素的指针;而Go语言的数组是一个值,Go语言中的数组是值类型,一个数组变量就表示着整个数组,意味着Go语言的数组在传递的时候,传递的是原数组的拷贝。
Go语言的数组为值传递
代码表现
C++
以C++为例,数组赋值,是数组第一个元素的地址,所以对赋值后的数组做改动,会影响到原数组。
#include <iostream>
using namespace std;
void printArr(int* arr, int length){
for (int i =0; i < length; i++) {
std::cout << arr[i] << ",";
}
std::cout << std::endl;
}
int main()
{
int arr1[3] {1,2,3};
int* arr2;
int length = sizeof (arr1) / sizeof (arr1[0]);
arr2 = arr1;
arr2[0] = 999;
printArr(arr1, length);
printArr(arr2, length);
return 0;
}
执行结果
将arr1赋值给arr2,修改arr2的值,arr1的值也跟随着发生了改变。
GO
Go数组赋值,是数组的整体拷贝,对赋值后的数组改动,不会影响原数组。
package main
import "fmt"
func main() {
a := [3]int{1, 2, 3}
b := a
b[0] = 999
printArray(a)
printArray(b)
}
func printArray(arr [3]int) {
for _, a := range arr {
fmt.Print(a)
}
fmt.Println()
}
结果
将a赋值给b,修改b的值,a的值不受影响,与预期相符
slice
切片是一个很小的对象,是对数组进行了抽象,并提供相关的操作方法。切片有三个属性字段:长度、容量和指向数组的指针。
上图中,ptr指的是指向array的pointer,len是指切片的长度, cap指的是切片的容量。
slice 和 array 使用上的区别
slice的赋值,也是值传递,但是是ptr、len、cap的值传递。
//TODO 插入示意图。
所以修改被赋值后的新slice时,原slice的值也会发生变化。
package main
import "fmt"
func main() {
a := [3]int{1, 2, 3}
b := a
b[0] = 999
fmt.Println("-----------------Array------------------")
printArray(a)
printArray(b)
c := []int{1, 2, 3}
d := c
d[0] = 999
fmt.Println("-----------------Slice------------------")
printSlice(c)
printSlice(d)
}
func printArray(arr [3]int) {
for _, a := range arr {
fmt.Print(a)
}
fmt.Println()
}
func printSlice(arr []int) {
for _, a := range arr {
fmt.Print(a)
}
fmt.Println()
}
输出
比较空切片和nil切片
对于 nil 的切片即 var s []byte 对应的逻辑图是
在此有一个说明:nil切片和空切片是不太一样的,空切片即s := make([]byte, 0)或者s := []byte{}出来的切片
空切片的逻辑图为:
空切片指针不为nil,而nil切片指针为nil。但是,不管是空切片还是nil切片,对其调用内置函数append()、len和cap的效果都是一样的,感受不到任何区别。
尝试打印出两者的值、类型等信息
package main
import "fmt"
func main() {
var c []int
d := []int{}
fmt.Println("--------------------print value-------------------")
fmt.Println("c value: ", c)
fmt.Println("d value: ", d)
fmt.Println("--------------------print type-------------------")
fmt.Println("c type: ", fmt.Sprintf("%T", c))
fmt.Println("d type: ", fmt.Sprintf("%T", d))
fmt.Println("--------------------print is null-------------------")
fmt.Println("c == null: ", c == nil)
fmt.Println("d == null: ", d == nil)
}
输出
发现一个奇怪的现象,两者的value输出是一样的,
但是与nil比较之后,一个为true,另一个为false。我们来查看一下两种初始化在汇编层面的差异。
源代码
汇编代码
汇编解读
汇编方法执行时,栈空间存储如下,左侧为调用方法前,右侧为调用方法后。
汇编脚本在第18行,给SP赋值,SP为入参地址。
调用 CALL 指令之后, 会将当前函数的return address 压栈。所以之前写入的入参,地址变为8(SP)
IsNil函数详细分析如下,
0x0000 00000 (slice_test_2.go:5) MOVQ "".a+8(SP), AX //通过"".a+8(SP)获取到入参,并赋值给AX寄存器
0x0005 00005 (slice_test_2.go:5) TESTQ AX, AX //AX与AX做按位与操作,如果值为0,则ZF寄存器置为1,反之置为0
0x0008 00008 (slice_test_2.go:5) SETEQ "".~r1+32(SP) //将ZF寄存器的值 存入 32(SP) 中
分析汇编差异点
两个汇编文件的差异,在于17、18行,
var a []int
0x001d 00029 (slice_test_1.go:10) MOVQ $0, (SP)
给(SP)设0(空)值
执行TESTQ时,0&0 -> 0,返回true
a := []int{}
0x001d 00029 (slice_test_2.go:11) LEAQ ""..autotmp_3+32(SP), AX
0x0022 00034 (slice_test_2.go:11) MOVQ AX, (SP)
LEAQ ""..autotmp_3+32(SP), AX 获取地址,赋值给AX寄存器
MOVQ AX, (SP) 用AX寄存中存储的地址值,给(SP)赋值
执行TESTQ时,地址&地址 -> 非0,返回false
interface{}
Go的interface源码在Golang源码的runtime目录中。
Go在不同版本之间的interface结构可能会有所不同,但是,整体的结构是不会改变的,此文章用的Go版本是1.11。
Go的interface是由两种类型来实现的:iface和eface。
type iface struct {
tab *itab
data unsafe.Pointer
}
type eface struct {
_type *_type
data unsafe.Pointer
}
iface表示的是包含方法的interface,例如:
type Person interface {
Print()
}
eface代表的是不包含方法的interface
type Person interface {}
eface
一共有两个属性构成,一个是类型信息_type,一个是数据信息。 其中,_type可以认为是Go语言中所有类型的公共描述,Go语言中几乎所有的数据结构都可以抽象成_type,是所有类型的表现,可以说是万能类型, data是指向具体数据的指针。
import (
"fmt"
"strconv"
)
type Binary uint64
func main() {
b := Binary(200)
any := (interface{})(b)
fmt.Println(any)
}
代码表现
package main
import "fmt"
func main() {
var c []int
fmt.Println("--------------------print is null-------------------")
fmt.Println("c = null: ", isNil_1(c))
fmt.Println("c = null: ", isNil_2(c))
return
}
func isNil_1(c []int) bool {
return c == nil
}
func isNil_2(c interface{}) bool {
return c == nil
}
isNil_1 返回 true, isNil_2 返回false。这是因为在调用isNil_2的时候,[]int类型会自动转化为interface{}类型,在判空的时候,之比较了参数的前8个字节,因为eface._type非空,所以返回false
让我们一起看一下汇编层面的差异
源码
汇编
汇编解读
只需要看两者汇编的传参,即可看出原因
slice
0x001d 00029 (test_interface_1.go:10) MOVQ $0, (SP) // 0(SP) - 7(SP) 赋值为0
0x0025 00037 (test_interface_1.go:10) XORPS X0, X0 // 清理X0寄存器,置为0
0x0028 00040 (test_interface_1.go:10) MOVUPS X0, 8(SP) // 8(SP) - 23(SP) 赋值为0
进行非空判断时,比较的是 注释中的 0(SP) - 7(SP),所以返回true
interface
0x001d 00029 (test_interface_2.go:10) MOVQ $0, ""..autotmp_1+24(SP)
0x0026 00038 (test_interface_2.go:10) XORPS X0, X0
0x0029 00041 (test_interface_2.go:10) MOVUPS X0, ""..autotmp_1+32(SP)
// 以上为初始化 slice
0x002e 00046 (test_interface_2.go:10) LEAQ type.[]int(SB), AX // 将 []int 类型赋值给 AX 寄存器
0x0035 00053 (test_interface_2.go:10) MOVQ AX, (SP) // AX 寄存器 赋值给 0(SP) - 7(SP)
0x0039 00057 (test_interface_2.go:10) LEAQ ""..autotmp_1+24(SP), AX // 对slice取址,赋值给AX寄存器
0x003e 00062 (test_interface_2.go:10) MOVQ AX, 8(SP) //将AX中存储的地址赋值给 8(SP) - 15(SP)
进行非空判断时,比较的是 0(SP) - 7(SP),即eface的_type信息,所以返回false