[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位的值覆盖掉了

相关推荐
慕城南风1 小时前
Go语言中的defer,panic,recover 与错误处理
golang·go
刘大辉在路上2 小时前
突发!!!GitLab停止为中国大陆、港澳地区提供服务,60天内需迁移账号否则将被删除
git·后端·gitlab·版本管理·源代码管理
追逐时光者4 小时前
免费、简单、直观的数据库设计工具和 SQL 生成器
后端·mysql
初晴~4 小时前
【Redis分布式锁】高并发场景下秒杀业务的实现思路(集群模式)
java·数据库·redis·分布式·后端·spring·
盖世英雄酱581364 小时前
InnoDB 的页分裂和页合并
数据库·后端
小_太_阳5 小时前
Scala_【2】变量和数据类型
开发语言·后端·scala·intellij-idea
直裾5 小时前
scala借阅图书保存记录(三)
开发语言·后端·scala
星就前端叭6 小时前
【开源】一款基于Vue3 + WebRTC + Node + SRS + FFmpeg搭建的直播间项目
前端·后端·开源·webrtc
小林coding6 小时前
阿里云 Java 后端一面,什么难度?
java·后端·mysql·spring·阿里云