go 字节数组比较_【Go】深入剖析slice和array

2b50f11bd1b0bf20187cca13913c20e2.png

文章来源:https://blog.thinkeridea.com/201901/go/shen_ru_pou_xi_slice_he_array.html

arrayslice 看似相似,却有着极大的不同,但他们之间还有着千次万缕的联系 slice 是引用类型、是 array 的引用,相当于动态数组, 这些都是 slice 的特性,但是 slice 底层如何表现,内存中是如何分配的,特别是在程序中大量使用 slice 的情况下,怎样可以高效使用 slice? 今天借助 Gounsafe 包来探索 arrayslice 的各种奥妙。

数组

slice 是在 array 的基础上实现的,需要先详细了解一下数组。

维基上如此介绍数组:

在计算机科学中,数组数据结构(英语:array data structure),简称数组(英语:Array),是由相同类型的元素(element)的集合所组成的数据结构,分配一块连续的内存来存储,利用元素的索引(index)可以计算出该元素对应的存储地址。 数组设计之初是在形式上依赖内存分配而成的,所以必须在使用前预先请求空间。这使得数组有以下特性:
1. 请求空间以后大小固定,不能再改变(数据溢出问题);
2. 在内存中有空间连续性的表现,中间不会存在其他程序需要调用的数据,为此数组的专用内存空间;
3. 在旧式编程语言中(如有中阶语言之称的C),程序不会对数组的操作做下界判断,也就有潜在的越界操作的风险(比如会把数据写在运行中程序需要调用的核心部分的内存上)。

根据维基的介绍,了解到数组是存储在一段连续的内存中,每个元素的类型相同,即是每个元素的宽度相同,可以根据元素的宽度计算元素存储的位置。 通过这段介绍总结一下数组有一下特性:

  • 分配在连续的内存地址上
  • 元素类型一致,元素存储宽度一致
  • 空间大小固定,不能修改
  • 可以通过索引计算出元素对应存储的位置(只需要知道数组内存的起始位置和数据元素宽度即可)
  • 会出现数据溢出的问题(下标越界)

Go 中的数组如何实现的呢,恰恰就是这么实现的,实际上几乎所有计算机语言,数组的实现都是相似的,也拥有上面总结的特性。

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

在程序中数组的初始化有两种方法 arr := [10]int{}var arr [10]int,但是不能使用 make 来创建,数组这节结束时再探讨一下这个问题。 使用 unsafe来看一下在内存中都是如何存储的吧:

package main

import (
    "fmt"
    "unsafe"
)

func main() {
    
    var arr = [3]int{
    1, 2, 3}

    fmt.Println(unsafe.Sizeof(arr))
    size := unsafe.Sizeof(arr[0])

    // 获取数组指定索引元素的值
    fmt.Println(*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&arr[0])) + 1*size)))

    // 设置数组指定索引元素的值
    *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&arr[0])) + 1*size)) = 10

    fmt.Println(arr[1])
}

这段代码的输出如下 (Go Playground):

12
2
10

首先说 12fmt.Println(unsafe.Sizeof(arr)) 输出的,unsafe.Sizeof 用来计算当前变量的值在内存中的大小,12 这个代表一个 int 有4个字节,3 * 4 就是 12

这是在32位平台上运行得出的结果, 如果在64位平台上运行数组的大小是 24。从这里可以看出 [3]int 在内存中由3个连续的 int 类型组成,且有 12 个字节那么长,这就说明了数组在内存中没有存储多余的数据,只存储元素本身。

size := unsafe.Sizeof(arr[0]) 用来计算单个元素的宽度,int在32位平台上就是4个字节,uintptr(unsafe.Pointer(&arr[0])) 用来计算数组起始位置的指针,1*size 用来获取索引为1的元素相对数组起始位置的偏移,unsafe.Pointer(uintptr(unsafe.Pointer(&arr[0])) + 1*size)) 获取索引为1的元素指针,*(*int) 用来转换指针位置的数据类型, 因为 int 是4个字节,所以只会读取4个字节的数据,由元素类型限制数据宽度,来确定元素的结束位置,因此得到的结果是 2

上一个步骤获取元素的值,其中先获取了元素的指针,赋值的时候只需要对这个指针位置设置值就可以了, *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&arr[0])) + 1*size)) = 10 就是用来给指定下标元素赋值。

6244529c4fcef1e4776ff5eefa17f3d6.png
package main

import (
    "fmt"
    "unsafe"
)

func main() {
    
    n:= 10
    var arr = [n]int{}
    fmt.Println(arr)
}

如上代码,动态的给数组设定长度,会导致编译错误 non-constant array bound n, 由此推导数组的所有操作都是编译时完成的,会转成对应的指令,通过这个特性知道数组的长度是数组类型不可或缺的一部分,并且必须在编写程序时确定。

可以通过 GOOS=linux GOARCH=amd64 go tool compile -S array.go 来获取对应的汇编代码,在 array.go 中做一些数组相关的操作,查看转换对应的指令。

之前的疑问,为什么数组不能用 make 创建? 上面分析了解到数组操作是在编译时转换成对应指令的,而 make 是在运行时处理(特殊状态下会做编译器优化,make可以被优化,下面 slice 分析时来讲)。

slice

因为数组是固定长度且是值传递,很不灵活,所以在 Go 程序中很少看到数组的影子。然而 slice 无处不在,slice 以数组为基础,提供强大的功能和遍历性。

slice 的类型规范是[]T,slice T元素的类型。与数组类型不同,slice 类型没有指定的长度。

slice 申明的几种方法:

s := []int{1, 2, 3} 简短的赋值语句 var s []int var 申明 make([]int, 3, 8)make([]int, 3) make 内置方法创建 s := ss[:5] 从切片或者数组创建
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值