Go 数据结构浅析

74 阅读5分钟

Go数组和切片

数组

几乎所有计算机语言,数组的实现都是相似的:一段连续的内存,Go语言也一样,Go语言的数组底层实现就是一段连续的内存空间。每个元素有唯一一个索引(或者叫下标)来访问。如下图所示,下图是 [5]int{1:10, 2:20} 数组的内部实现逻辑图:

由于内存连续,CPU很容易计算索引(即数组的下标),可以快速迭代数组里的所有元素。

Go语言的数组不同于C语言或者其他语言的数组,C语言的数组变量是指向数组第一个元素的指针;而Go语言的数组是一个值,Go语言中的数组是值类型,一个数组变量就表示着整个数组,意味着Go语言的数组在传递的时候,传递的是原数组的拷贝。

c.runoob.com/compile/21

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

参考文档

draveness.me/golang/docs…