[go]Slice 切片原理

go 切片

在go 语言的世界中,切片是一个很常用的数据结构,同时也有很多的坑,在面试中十个有八个面试官会问到,本文主要整理下切片数据结构、创建方式、扩容方式、常见面试题等。

slice 又称动态数组,依托数组实现,可以方便地进行扩容、传递等,实际使用中比数组更加灵活。

切片与底层数组共享一个内存地址,当切片的值改变时,会影响底层数组的值也一起改变,当切片扩容时会重新申请内存地址,此时切片不会影响底层数组

slice 依托数组实现,底层数组对用户屏蔽,在底层数组容量不足时可以实现自动重新分配并生成新的slice。

数据结构

go 复制代码
type slice struct {
    array unsafe.Pointer    // 指向底层数组的地址
    len int                 // 长度  
    cap int                 // 容量
}

创建切片

  • 使用make

使用make 创建slice ,可以指定长度和容量,创建时底层会分配一个数组,数组的长度即容量 例如slice :=make([]int,5,10),底层指向的就是一个长度为10的数组,该slice 长度为5 ,可以使用slice[0]~slice[4]来操作元素,capacity为10,表示后续想slice添加新元素时不必重新分配内存,直接使用预留内存

  • 使用数组创建

使用数组创建slice ,slice与原数组共用一部分内存 slice:=array[5:7]切片从数组array[5]开始到array[7]结束,切片长度为2,数组后面的内容都作为切片的预留内存

slice 扩容

使用append 想slice 追加元素时,如果slice 空间不足,将会触发slice 扩容,扩容实际上就是重新分配一个更大的内存,将原slice 的数据拷贝进新的slice ,然后返回新的slice 扩容原则

  • 如果原Slice容量小于1024,则新Slice容量将扩大为原来的2倍
  • 如果原slice 大于等于1024 扩大为原来的1.25倍

append 的实现步骤

  • 假如Slice容量够用,则将新元素追加进去,Slice.len++,返回原Slice
  • 原Slice容量不够,则将Slice先扩容,扩容后得到新Slice
  • 将新元素追加进新Slice,Slice.len++,返回新的Slice。

slice copy

使用copy()内置函数拷贝两个切片时,会将源切片的数据逐个拷贝到目的切片指向的数组中,拷贝数量取两个切片长度的最小值。

例如长度为10的切片拷贝到长度为5的切片时,将会拷贝5个元素,也就是说copy 的过程中不会发生扩容 // 可以使用这个特性来比较两个数的大小

go 复制代码
min= copy(make([]struct{},a),make([]struct{},b))

特殊切片

根据数组或者切片生成新切片一般使用slice:=array[start:end],这种新生成的切片没有指定容量,实际上新切片容量为从start 开始到array 结束 比如如下两个切片,长度和容量都是一致的,使用共同的内存地址

go 复制代码
sliceA := make([]int,5,10)
sliceB := sliceA[0:5]

根据数组或者切片生成新切片还有另一种写法,可以指定新切片的容量slice := array[start:end:cap],cap不能超过原切片的实际值

go 复制代码
sliceA := make([]int,5,10)
sliceB := sliceA[0:5]
sliceC := sliceA[0:5:5]

总结

  • 创建切片时可根据实际需要预分配容量,尽量避免追加过程中扩容操作,有利于提升性能;
  • 切片拷贝时需要判断实际拷贝的元素个数
  • 谨慎使用多个切片操作同一个数组,以防读写冲突
  • 每个切片都指向一个底层数组
  • 每个切片都保存了当前切片的长度、底层数组可用容量
  • 使用len()计算切片长度时间复杂度为O(1),不需要遍历切片
  • 使用cap()计算切片容量时间复杂度为O(1),不需要遍历切片
  • 通过函数传递切片时,不会拷贝整个切片,因为切片本身只是个结构体而已
  • 使用append()向切片追加元素时有可能触发扩容,扩容后将会生成新的切片

面试题

go 复制代码
package main

import "fmt"

func main() {

	{
		test := []int{1, 2, 3, 4, 5}
		change2(test)
		fmt.Println(test)
	}
}

func change2(param []int) {
	param = append(param, 6)
}

输出结果为 1,2,3,4,5 解析:change2 对切片进行了扩容操作,param 是一个函数内的局部变量已经指向了新的内存地址,test指向的底层数组不变

go 复制代码
package main

import "fmt"

func main() {
	{
		test := []int{1, 2, 3, 4, 5}
		change(test)
		fmt.Println(test)

	}

}

func change(param []int) {
	param = append(param[:1], param[2:]...)
}

输出结果为 1,3,4,5,5 解析,底层数组没变,append 操作把 切片前4位的值覆盖掉了

相关推荐
岁忧9 分钟前
(LeetCode 面试经典 150 题) 138. 随机链表的复制 (哈希表)
java·c++·leetcode·链表·面试·go
鹦鹉0079 分钟前
IO流中的字节流
java·开发语言·后端
AntBlack2 小时前
闲谈 :AI 生成视频哪家强 ,掘友们有没有推荐的工具?
前端·后端·aigc
Livingbody3 小时前
使用gradio构建一个大模型多轮对话WEB应用
后端
泉城老铁5 小时前
Spring Boot 对接阿里云 OSS 的详细步骤和流程
java·后端·程序员
Aurora_NeAr5 小时前
大数据之路:阿里巴巴大数据实践——元数据与计算管理
大数据·后端
喜欢板砖的牛马5 小时前
容器(docker container):你需要知道的一切
后端·docker
lichenyang4535 小时前
从零开始学Express,理解服务器,路由于中间件
后端
EnigmaGcl5 小时前
领域驱动设计,到底在讲什么?
后端·架构
丘山子5 小时前
API Gateway 工作原理介绍
前端·后端·面试