前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Go切片数组深度解析

Go切片数组深度解析

作者头像
Tim在路上
发布2021-02-04 10:05:58
5420
发布2021-02-04 10:05:58
举报

Go 中的分片数组,实际上有点类似于Java中的ArrayList,是一个可以扩展的数组,但是Go中的切片由比较灵活,它和数组很像,也是基于数组,所以在了解Go切片前我们先了解下数组。

  • Go 数组原理
  • Go 切片的特性
  • Go 切片的扩容
Go 数组原理

数组简单描述就由相同类型元素组成的数据结构, 在创建初期就确定了长度,是不可变的。

type Array struct {
    Elem  *Type // element type
    Bound int64 // number of elements; <0 if unknown yet
}
func NewArray(elem *Type, bound int64) *Type {
    if bound < 0 {
        Fatalf("NewArray: invalid bound %v", bound)
    }
    t := New(TARRAY)
    t.Extra = &Array{Elem: elem, Bound: bound}
    t.SetNotInHeap(elem.NotInHeap())
    return t
}

但是Go的数组类型又和C与Java的数组类型不一样,NewArray用于创建一个数组,从源码中可以看出最后返回的是 &Array{}的指针,并不是第一个元素的指针,在Go中数组属于值类型,在进行传递时,采取的是值传递,通过拷贝整个数组。Go语言的数组是一种有序的struct。

  • 数组的初始化

Go 语言的数组有两种不同的创建方式,一种是显示的初始化,一种是隐式的初始化。

arr1 := [3]int{1, 2, 3}
arr2 := [...]int{1, 2, 3}

注意一定是使用[...]T进行创建,使用三个点的隐式创建,编译器会对数组的大小进行推导,只是Go提供的一种语法糖。

其次,Go中数组的类型,是由数值类型和长度两个一起确定的。[2]int 和 [3]int 不是同一个类型,不能进行传参和比较,把数组理解为类型和长度两个属性的结构体,其实就一目了然了。

  • 数组的存储

Go中的数组属于值类型,通常应该存储于栈中,局部变量依然会根据逃逸分析确定存储栈还是堆中。

编译器对数组函数中做两种不同的优化:

  1. 当元素数量小于或者等于 4 个时,会直接将数组中的元素放置在栈上;
  2. 当元素数量大于 4 个时,会将数组中的元素放置到静态区并在运行时取出;
var arr [5]int
statictmp_0[0] = 1
statictmp_0[1] = 2
statictmp_0[2] = 3
statictmp_0[3] = 4
statictmp_0[4] = 5
arr = statictmp_0

在静态区完成赋值后复制到栈中。

总结起来,在不考虑逃逸分析的情况下,如果数组中元素的个数小于或者等于 4 个,那么所有的变量会直接在栈上初始化,如果数组元素大于 4 个,变量就会在静态存储区初始化然后拷贝到栈上。

切片的特性

由于数组是值类型,那么赋值和函数传参操作都会复制整个数组数据。

func main() {
    arrayA := [2]int{100, 200}
    var arrayB [2]int

    arrayB = arrayA

    fmt.Printf("arrayA : %p , %v\n", &arrayA, arrayA)
    fmt.Printf("arrayB : %p , %v\n", &arrayB, arrayB)

    testArray(arrayA)
}

func testArray(x [2]int) {
    fmt.Printf("func Array : %p , %v\n", &x, x)
}

arrayA : 0xc0000160c0 , [100 200]
arrayB : 0xc0000160d0 , [100 200]
func Array : 0xc000016120 , [100 200]

不管是赋值或函数传参,地址都不一致,发生了拷贝。如果数组的数据较大,则会消耗掉大量内存。那么为了减少拷贝我们可以主动的传递指针呀。

func main() {
    arrayA := [2]int{100, 200}
    fmt.Printf("func Array : %p , %v\n", &arrayA, arrayA)
    testArray(&arrayA)
    arrayA = [2]int{0, 200}
}

func testArray(x *[2]int) {
    fmt.Printf("func Array : %p , %v\n", x, *x)
}

func Array : 0xc0000160c0 , [100 200]
func Array : 0xc0000160c0 , [100 200]

地址是一样的,不过传指针会有一个弊端,从打印结果可以看到,指针地址都是同一个,万一原数组的指针指向更改了,那么函数里面的指针指向都会跟着更改。

func main() {
    arrayA := [2]int{100, 200}
    array := arrayA[:]
    fmt.Printf("func Array : %p , %v\n", &arrayA, arrayA)
    testArray(&array)
    arrayA = [2]int{0, 200}
}

func testArray(x *[]int) {
    fmt.Printf("func Array : %p , %v\n", x, *x)
}
func Array : 0xc0000160c0 , [100 200]
func Array : 0xc0000044c0 , [100 200]

同样的我们将数组转换为切片,通过传递切片,地址是不一样的,数组值相同。

切片是引用传递,所以它们不需要使用额外的内存并且比使用数组更有效率。

所以,切片属于引用类型。

  • 切片的初始化
  1. 通过下标的方式获得数组或者切片的一部分;
slices := arr[:]
slices2 := arr[1:3]

通过这种方式可以将数组转换为切片。

  1. 使用字面量初始化新的切片;
slice := []int{1, 2, 3}

中间不加三个点就是切片,使用这种方式创建切片,实际上是先创建数组,然后再通过第一种方式创建。

  1. 使用关键字 make 创建切片:
slice := make([]int, 10)

使用make创建切片,就不光编译期了,make创建切片会涉及到运行期。1. 切片的大小和容量是否足够小; 切片是否发生了逃逸,最终在堆上初始化。如果切片小的话会先在栈或静态区进行创建。

  • 切片的底层结构
struct  Slice
{               // must not move anything
    byte*   array;      // actual data
    uintgo  len;        // number of elements
    uintgo  cap;        // allocated number of elements
};
type slice struct {
 array unsafe.Pointer
 len   int
 cap   int
}

切片有一个数组的指针,len是指切片的长度, cap指的是切片的容量。

    arrayA := [5]int{100, 200,4,5,6}
    array := arrayA[1:3]
    array = append(array,1,3,5)
    fmt.Printf("%v, %d, %d, %v\n",array,len(array),cap(array),&array)
    [200 4 1 3 5], 5, 8, &[200 4 1 3 5]

cap是在初始化切片是生成的容量。

发现切片的结构体是数组的地址指针array unsafe.Pointer,而Go中数组的地址代表数组结构体的地址。

slice 中得到一块内存地址,&array[0]或者unsafe.Pointer(&array[0])。

也可以通过地址构造切片

var ptr unsafe.Pointer
var s1 = struct {
   addr uintptr
   len int
   cap int
}{ptr, length, length}
s := *(*[]byte)(unsafe.Pointer(&s1))
  • nil 切片 和 空切片

nil切片:指的unsafe.Pointer 为nil

空切片:

silce := make( []int , 0 )
slice := []int{ }

创建的指针不为空,len和cap为空

切片扩容

当一个切片的容量满了,就需要扩容了。怎么扩,策略是什么?

func growslice(et *_type, old slice, cap int) slice {
    if raceenabled {
        callerpc := getcallerpc(unsafe.Pointer(&et))
        racereadrangepc(old.array, uintptr(old.len*int(et.size)), callerpc, funcPC(growslice))
    }
    if msanenabled {
        msanread(old.array, uintptr(old.len*int(et.size)))
    }

    if et.size == 0 {
        // 如果新要扩容的容量比原来的容量还要小,这代表要缩容了,那么可以直接报panic了。
        if cap < old.cap {
            panic(errorString("growslice: cap out of range"))
        }

        // 如果当前切片的大小为0,还调用了扩容方法,那么就新生成一个新的容量的切片返回。
        return slice{unsafe.Pointer(&zerobase), old.len, cap}
    }

    // 这里就是扩容的策略
    newcap := old.cap
    doublecap := newcap + newcap
    if cap > doublecap {
        newcap = cap
    } else {
        if old.len < 1024 {
            newcap = doublecap
        } else {
            // Check 0 < newcap to detect overflow
            // and prevent an infinite loop.
            for 0 < newcap && newcap < cap {
                newcap += newcap / 4
            }
            // Set newcap to the requested cap when
            // the newcap calculation overflowed.
            if newcap <= 0 {
                newcap = cap
            }
        }
    }

    // 计算新的切片的容量,长度。
    var lenmem, newlenmem, capmem uintptr
    const ptrSize = unsafe.Sizeof((*byte)(nil))
    switch et.size {
    case 1:
        lenmem = uintptr(old.len)
        newlenmem = uintptr(cap)
        capmem = roundupsize(uintptr(newcap))
        newcap = int(capmem)
    case ptrSize:
        lenmem = uintptr(old.len) * ptrSize
        newlenmem = uintptr(cap) * ptrSize
        capmem = roundupsize(uintptr(newcap) * ptrSize)
        newcap = int(capmem / ptrSize)
    default:
        lenmem = uintptr(old.len) * et.size
        newlenmem = uintptr(cap) * et.size
        capmem = roundupsize(uintptr(newcap) * et.size)
        newcap = int(capmem / et.size)
    }

    // 判断非法的值,保证容量是在增加,并且容量不超过最大容量
    if cap < old.cap || uintptr(newcap) > maxSliceCap(et.size) {
        panic(errorString("growslice: cap out of range"))
    }

    var p unsafe.Pointer
    if et.kind&kindNoPointers != 0 {
        // 在老的切片后面继续扩充容量
        p = mallocgc(capmem, nil, false)
        // 将 lenmem 这个多个 bytes 从 old.array地址 拷贝到 p 的地址处
        memmove(p, old.array, lenmem)
        // 先将 P 地址加上新的容量得到新切片容量的地址,然后将新切片容量地址后面的 capmem-newlenmem 个 bytes 这块内存初始化。为之后继续 append() 操作腾出空间。
        memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
    } else {
        // 重新申请新的数组给新切片
        // 重新申请 capmen 这个大的内存地址,并且初始化为0值
        p = mallocgc(capmem, et, true)
        if !writeBarrier.enabled {
            // 如果还不能打开写锁,那么只能把 lenmem 大小的 bytes 字节从 old.array 拷贝到 p 的地址处
            memmove(p, old.array, lenmem)
        } else {
            // 循环拷贝老的切片的值
            for i := uintptr(0); i < lenmem; i += et.size {
                typedmemmove(et, add(p, i), add(old.array, i))
            }
        }
    }
    // 返回最终新切片,容量更新为最新扩容之后的容量
    return slice{p, old.len, newcap}
}
  1. 首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)
  2. 否则判断,如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap)
  3. 否则判断,如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的 1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap)
  4. 如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)

如果原来数组切片的容量已经达到了最大值,再想扩容, Go 默认会先开一片内存区域,把原来的值拷贝过来,然后再执行 append() 操作。这种情况对现数组的地址和原数组地址不相同。

  • 切片数组的拷贝
func main() {
    array := []int{10, 20, 30, 40}
    slice := make([]int, 6)
    n := copy(slice, array)
    fmt.Println(n,slice)
}
func main() {
    slice := []int{10, 20, 30, 40}
    for index, value := range slice {
        fmt.Printf("value = %d , value-addr = %x , slice-addr = %x\n", value, &value, &slice[index])
    }
}
value = 10 , value-addr = c4200aedf8 , slice-addr = c4200b0320
value = 20 , value-addr = c4200aedf8 , slice-addr = c4200b0328
value = 30 , value-addr = c4200aedf8 , slice-addr = c4200b0330
value = 40 , value-addr = c4200aedf8 , slice-addr = c4200b0338

从上面结果我们可以看到,如果用 range 的方式去遍历一个切片,拿到的 Value 其实是切片里面的值拷贝,即浅拷贝。所以每次打印 Value 的地址都不变。

由于 Value 是值拷贝的,并非引用传递,所以直接改 Value 是达不到更改原切片值的目的的,需要通过 &slice[index] 获取真实的地址。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Go 数组原理
  • 切片的特性
  • 切片扩容
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档