前言
数组(Array)是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。因其长度的不可变动,数组在Go中很少直接使用。作为替代是Slice(切片),它是可以增长和收缩的动态序列,Slice功能也更灵活,在Go中有着广泛使用。
关于Slice的使用有很多文章已经介绍了,本系列文章无意再重复介绍使用过程,主要专注于了解Slice的结构及底层的处理逻辑,从源码的角度加深对使用的了解,解决一些常见的使用错误点。
本系列文章涉及到的builtin/builtin.go的func或Go的关键字都是在编译期间进行处理,这些调用过程在cmd/compile/internal/gc包下,其处理细节大多复杂。为简化细节,文章会使用部分reflect包中的代码来说明处理逻辑。reflect需要在runtime时重新构建出相关的Type、Value,其与直接调用的处理过程可能并不完全一致,但处理逻辑是一致的,因为两者操作的结果是一致的。
reflect内的func仅用来说明处理逻辑,实际使用builtin内的type/func或者Go关键字,都需要在编译时进行处理。
概念
先从一个问题开始热身:
以下哪个变量的类型是Array?哪个是Slice?
var a [10]int
b := make([]int,10)
c := a[5:9]
定义
A slice is a data structure describing a contiguous section of an array stored separately from the slice variable itself. A slice is not an array. A slice describes a piece of an array.1
以上定义来自Go官方博客关于slices相关机制的说明,大致意思如下:
Slice是一种数据结构,描述与Slice变量本身分开存储的Array的连续部分。 Slice不是Array。Slice描述了Array的一部分。
类型与声明
Array的类型:
[len]Type
Slice的类型:
[]Type
声明变量时如下:
var a [10]int// Array
b := make([]int,10)// 直接声明Slice
结构
Slice由3部分组成:指针、长度和容量,指针指向的底层数组,长度是当前容纳的数据长度,容量是能容量数据的最大长度。
Slice底层实际是一个struct,结构如下:
// runtime/slice.go
type slice struct {
array unsafe.Pointer// 指向数组的指针
len int
cap int
}
创建slice的方式有以下几种方式:
- 直接通过make创建,可以指定len、cap
slice := make([]int,5,10)
- 对数组/slice进行切片生成
var data [10]int
slice := data[2:8]
slice = slice[1:3] - 对slice进行append生成
slice := make([]int,5,10)
slice = append(slice,6)
本文主要介绍第一种方式,slice make的过程。
make过程
slice描述了Array的一部分,从slice的结构可以看到大致的关系了,现在从底层的创建过程来确认两者的关系。
makeslice
slice的array确实是数组吗?先看下slice与array的创建过程:
//以下代码来源于:runtime/malloc.go
func makeslice(et *_type, len, cap int) unsafe.Pointer {
mem, overflow := math.MulUintptr(et.size, uintptr(cap))
if overflow || mem > maxAlloc || len < 0 || len > cap {
// NOTE: Produce a 'len out of range' error instead of a
// 'cap out of range' error when someone does make([]T, bignumber).
// 'cap out of range' is true too, but since the cap is only being
// supplied implicitly, saying len is clearer.
// See golang.org/issue/4085.
mem, overflow := math.MulUintptr(et.size, uintptr(len))
if overflow || mem > maxAlloc || len < 0 {
panicmakeslicelen()
}
panicmakeslicecap()
}
return mallocgc(mem, et, true)
}
makeslice分配的内存大小为类型et的size * cap
,创建时会判断是否超过允许的分配的最大内存。
newarray
// newarray allocates an array of n elements of type typ.
func newarray(typ *_type, n int) unsafe.Pointer {
if n == 1 {
return mallocgc(typ.size, typ, true)
}
mem, overflow := math.MulUintptr(typ.size, uintptr(n))
if overflow || mem > maxAlloc || n < 0 {
panic(plainError("runtime: allocation size out of range"))
}
return mallocgc(mem, typ, true)
}
newarray分配的内存大小为size * len
,与makeslice比,主要少了cap相关的检查。两者结合看,slice
在make时会先创建cap
大小的array
,这是Slice与Array的最直接的联系。
slice与array的关系
从makeslice的源码结合newarray来看,makeslice除校验cap外,实际就是在创建一个大小为cap的数组。
如果觉得两者的关系还不够直观,我们从reflect/value.go中的MakeSlice的源码看下:
// SliceHeader is the runtime representation of a slice.
// It cannot be used safely or portably and its representation may
// change in a later release.
// Moreover, the Data field is not sufficient to guarantee the data
// it references will not be garbage collected, so programs must keep
// a separate, correctly typed pointer to the underlying data.
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
// sliceHeader is a safe version of SliceHeader used within this package.
type sliceHeader struct {
Data unsafe.Pointer
Len int
Cap int
}
// MakeSlice creates a new zero-initialized slice value
// for the specified slice type, length, and capacity.
func MakeSlice(typ Type, len, cap int) Value {
if typ.Kind() != Slice {
panic("reflect.MakeSlice of non-slice type")
}
if len < 0 {
panic("reflect.MakeSlice: negative len")
}
if cap < 0 {
panic("reflect.MakeSlice: negative cap")
}
if len > cap {
panic("reflect.MakeSlice: len > cap")
}
s := sliceHeader{unsafe_NewArray(typ.Elem().(*rtype), cap), len, cap}
return Value{typ.(*rtype), unsafe.Pointer(&s), flagIndir | flag(Slice)}
}
sliceHeader是slice在运行时的表示,sliceHeader在构造时,先通过unsafe_NewArray创建Data。而unsafe_NewArray就是调用的newarray,因此MakeSlice就是创建一个持有cap大小的数组的sliceHeader。
//go:linkname reflect_unsafe_NewArray reflect.unsafe_NewArray
func reflect_unsafe_NewArray(typ *_type, n int) unsafe.Pointer {
return newarray(typ, n)
}
总结
本文主要从源码的角度去探究Slice与Array的关系。slice底层是一个strcut类型的数据结构,slice的在make时会先创建一个cap大小的数组,然后持有数组的指针,从而去描述一个数组。这是基础的数据结构,后续的使用均是建立在此基础上的。
思考
我们知道对数组、Slice进行切片操作可以返回新的slice类型的数据,string也可以进行切片操作,但返回的数据类型并不是slice,而是string,这是为什么呢?后续我们一起接着探索切片的过程吧!
公众号
鄙人刚刚开通了公众号,专注于分享Go开发相关内容,望大家感兴趣的支持一下,在此特别感谢。