深入了解Go Slice(一)—— make的详细处理过程

前言

数组(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的方式有以下几种方式:

  1. 直接通过make创建,可以指定len、cap

    slice := make([]int,5,10)

  2. 对数组/slice进行切片生成

    var data [10]int
    slice := data[2:8]
    slice = slice[1:3]

  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开发相关内容,望大家感兴趣的支持一下,在此特别感谢。

参考

[1]Arrays, slices (and strings): The mechanics of ‘append’

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章