[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 ,可以使用slice0~slice4来操作元素,capacity为10,表示后续想slice添加新元素时不必重新分配内存,直接使用预留内存

  • 使用数组创建

使用数组创建slice ,slice与原数组共用一部分内存 slice:=array[5:7]切片从数组array5开始到array7结束,切片长度为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位的值覆盖掉了

相关推荐
weixin_4462608534 分钟前
HACO:面向动态部署环境的对冲式智能计算可靠多智能体调度框架
后端·python·flask
ttwuai2 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
用户8356290780512 小时前
Python 实现 Excel 页面布局与打印设置自动化
后端·python
用户9931441579842 小时前
微服务框架中获取用户信息
后端
xuanWb2 小时前
手写一个 LLM API 网关:Anthropic 与 OpenAI 协议转换的完整实现
后端
苍何2 小时前
给 Codex 换皮肤这门生意,被我开源了
后端
用户8356290780512 小时前
Python 实现 Excel 命名范围(Named Range)的创建与管理
后端·python
程序员David3 小时前
我让 Claude 从架构文档一路干到代码,踩了三个坑才摸清边界
后端
Zane19943 小时前
并发 vs 并行:别再傻傻分不清了,一文讲透 Java 并发编程的第一课
java·后端