首页 > 其他分享 >深入解析 Go 中 Slice

深入解析 Go 中 Slice

时间:2024-08-31 19:50:37浏览次数:11  
标签:Slice int fmt len 切片 Println slice Go 解析

在这里插入图片描述

0 前言

slice 是一种灵活且强大的数据结构,它在功能上类似于其他编程语言中的数组,但提供了更多的灵活性。与数组不同,slice 允许动态调整长度,使其在大多数场景中更加适用。本文将深入解析 slice 的基本概念及底层实现原理,并通过分析一些面试中常见的易错题,加深对 slice 的理解。使用的go版本1.22

1 基本概念

1.1 定义和初始化

slice 是基于数组构建的一个轻量级数据结构,拥有长度和容量两个属性,可以通过多种方式进行初始化。

// 直接初始化
s := []int{1, 2, 3, 4, 5}

// 使用make创建,指定长度和容量
s := make([]int, 3, 5)  // [0 0 0]

// 使用数组切片 包含arr[1]到arr[3],但不包括arr[4]
arr := [5]int{1, 2, 3, 4, 5}
s := arr[1:4]  // [2 3 4] 

1.2 长度和容量

  • 长度 (len): 当前 slice 中的元素个数。
  • 容量 (cap): 从 slice 的起始位置到底层数组末尾的元素个数。
	s := []int{1, 2, 3, 4, 5}
	fmt.Println(len(s)) // 输出: 5
	fmt.Println(cap(s)) // 输出: 5

	s2 := s[1:3]
	fmt.Println(len(s2)) // 输出: 2
	fmt.Println(cap(s2)) // 输出: 4 (底层数组从s[1]开始的元素数)

1.3 切片基本操作

1.3.1 访问和修改元素

与数组类似,slice 可以通过索引访问和修改元素

	s := []int{1, 2, 3}
	fmt.Println(s[1]) // 输出: 2

	s[2] = 4
	fmt.Println(s) // 输出: [1 2 4]
1.3.2 追加

使用内置函数 append 可以动态向 slice 追加元素。如果 slice 的容量不足以容纳新元素,append 会创建一个新的底层数组,并将原有数据复制到新数组中。

	s := []int{1, 2, 3}
	s = append(s, 4, 5)
	fmt.Println(s) // 输出: [1 2 3 4 5]

1.4 总结

相信大家对切片这一数据结构已经不再陌生。前面介绍的只是 slice 的一些基本概念和操作。对于更高级的功能,或者使用中可能遇到的问题,比如扩容策略、函数内部对切片的修改是否会影响外部等,本文将在后面进行详细探讨。

2 底层实现原理

2.1 slice 数据结构

type slice struct {  
    // 指向了内存空间地址的起点. 由于 slice 数据存放在连续的内存空间中,后续可以根据索引 index,在起点的基础上快速进行地址偏移
    array unsafe.Pointer
    // 切片长度
    len   int
    // 切片容量 >=len
    cap   int
}

2.2 创建切片

2.2.1 源码

先走一遍源码runtime/slice.go再图解各种初始化的区别。

func makeslice(et *_type, len, cap int) unsafe.Pointer {
  // 根据cap和每个元素类型大小,计算总容量
	mem, overflow := math.MulUintptr(et.Size_, uintptr(cap))
  // 容量溢出或超限或长度小于0或长度大于容量,直接panic
	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)
}
2.2.2 make 指定len和cap 创建

在这里插入图片描述

通过make函数创建了一个len=3cap=5的切片。内存申请了5个int大小的连续内存空间。但是后面两个暂时是访问不到的,slice[3]会直接panic的。index out of range [3] with length 3

2.2.3 直接初始化

在这里插入图片描述

直接初始化创建的是一个 len = 5cap = 5的切片,这时候数组里面每个元素的值都初始化完成了。

2.2.4 使用数组切片

在这里插入图片描述

2.3 切片追加

在 Go 中,append 是一个用于向切片(slice)中追加元素的内置函数。

	s := []int{1, 2, 3}
	s = append(s, 4, 5) //[1,2,3,4,5]

动态扩容

append 会根据需要自动扩展 slice 的容量。如果当前的 slice 容量足够大,append 会将新元素直接添加到现有的 slice 中;如果容量不足,append 会分配一个更大的底层数组,并将现有的元素复制到新的数组中,再添加新元素。扩容策略后面展开讲

	s := make([]int, 3, 5) 
	s = append(s, 4, 5)
	fmt.Println(s)      // [0 0 0 4 5]
	fmt.Println(cap(s)) // 5

	s = append(s, 6)
	fmt.Println(s)      // [0 0 0 4 5 6]
	fmt.Println(cap(s)) // 10 

追加另一个切片

	s1 := []int{1, 2, 3}
	s2 := []int{4, 5, 6}
	s1 = append(s1, s2...) // 将s2展开并追加到s1 [1,2,3,4,5,6]

函数内append对外部slice的影响

func modifySlice(s []int) []int {
	s = append(s, 4) // 触发扩容了
	s[0] = 100
	fmt.Println(cap(s)) // 6
	return s
}

func main() {
	s := []int{1, 2, 3}
	fmt.Println(s) // [1 2 3]

	s2 := modifySlice(s)
	fmt.Println(s)  //  [1 2 3]
	fmt.Println(s2) //  [100 2 3 4]
}

2.4 切片扩容

func nextslicecap(newLen, oldCap int) int {
	newcap := oldCap
	doublecap := newcap + newcap
  // 新的容量大于原容量的2倍,直接返回新的容量
	if newLen > doublecap {
		return newLen
	}
	const threshold = 256
  // 原来容量小于256,则扩容后的容量为原容量的2倍
	if oldCap < threshold {
		return doublecap
	}
	for {
    // 原容量*1.25+192
    // 一直循环 直到满足
		newcap += (newcap + 3*threshold) >> 2
		if uint(newcap) >= uint(newLen) {
			break
		}
	}
  // 溢出直接取预期的新容量
	if newcap <= 0 {
		return newLen
	}
	return newcap
}
func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice {
	oldLen := newLen - num
	if raceenabled {
		callerpc := getcallerpc()
		racereadrangepc(oldPtr, uintptr(oldLen*int(et.Size_)), callerpc, abi.FuncPCABIInternal(growslice))
	}
	if msanenabled {
		msanread(oldPtr, uintptr(oldLen*int(et.Size_)))
	}
	if asanenabled {
		asanread(oldPtr, uintptr(oldLen*int(et.Size_)))
	}

	if newLen < 0 {
		panic(errorString("growslice: len out of range"))
	}

	if et.Size_ == 0 {
    // 当前切片大小为0,直接返回一个新的容量的切片
		return slice{unsafe.Pointer(&zerobase), newLen, newLen}
	}

  // 具体代码上面解释了
	newcap := nextslicecap(newLen, oldCap)

  //根据容量,计算所需的内存空间大小
	var overflow bool
  // lenmem 旧切片长度所占用的内存大小
  // newlenmem 新切片长度所占用的内存大小
  // capmem 新切片容量所需的内存大小
  // noscan 切片元素是否包含指针
	var lenmem, newlenmem, capmem uintptr
	noscan := et.PtrBytes == 0
	switch {
	case et.Size_ == 1:
    // 数组元素大小为1
		lenmem = uintptr(oldLen)
		newlenmem = uintptr(newLen)
    // 函数计算需要的容量,并调整到合适的大小,将大小对齐到内存分配的 span class
		capmem = roundupsize(uintptr(newcap), noscan)
		overflow = uintptr(newcap) > maxAlloc
		newcap = int(capmem)
	case et.Size_ == goarch.PtrSize:
    // 数组元素为指针类型,则根据指针占用空间结合元素个数计算空间大小
		lenmem = uintptr(oldLen) * goarch.PtrSize
		newlenmem = uintptr(newLen) * goarch.PtrSize
		capmem = roundupsize(uintptr(newcap)*goarch.PtrSize, noscan)
		overflow = uintptr(newcap) > maxAlloc/goarch.PtrSize
		newcap = int(capmem / goarch.PtrSize)
	case isPowerOfTwo(et.Size_):
    // 数组元素大小是2的指数,通过位运算进行空间大小的计算 
		var shift uintptr
		if goarch.PtrSize == 8 {
			shift = uintptr(sys.TrailingZeros64(uint64(et.Size_))) & 63
		} else {
			shift = uintptr(sys.TrailingZeros32(uint32(et.Size_))) & 31
		}
		lenmem = uintptr(oldLen) << shift
		newlenmem = uintptr(newLen) << shift
		capmem = roundupsize(uintptr(newcap)<<shift, noscan)
		overflow = uintptr(newcap) > (maxAlloc >> shift)
		newcap = int(capmem >> shift)
		capmem = uintptr(newcap) << shift
	default:
    // 其他情况,直接使用乘法计算
		lenmem = uintptr(oldLen) * et.Size_
		newlenmem = uintptr(newLen) * et.Size_
		capmem, overflow = math.MulUintptr(et.Size_, uintptr(newcap))
		capmem = roundupsize(capmem, noscan)
		newcap = int(capmem / et.Size_)
		capmem = uintptr(newcap) * et.Size_
	}

  // 内存超出了可分配的范围 直接panic
	if overflow || capmem > maxAlloc {
		panic(errorString("growslice: len out of range"))
	}

	var p unsafe.Pointer
	if et.PtrBytes == 0 {
    // 无指针类型 直接mallocgc 分配内存
		p = mallocgc(capmem, nil, false)
		memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
	} else {
		p = mallocgc(capmem, et, true)
		if lenmem > 0 && writeBarrier.enabled {
      // 触发写屏障
			bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(oldPtr), lenmem-et.Size_+et.PtrBytes, et)
		}
	}
  // 将原切片内容拷贝到新的
	memmove(p, oldPtr, lenmem)

	return slice{p, newLen, newcap}
}

扩容流程如下:

在这里插入图片描述

2.5 切片拷贝

切片拷贝(Slice Copy)指的是将一个切片的内容复制到另一个切片中。Go 提供了一些简单而高效的方式来进行切片的拷贝,主要通过内置的 copy 函数来实现

src := []int{1, 2, 3, 4, 5}
dst := make([]int, 3)

n := copy(dst, src)
	
 // 两个切片是独立的,修改不会相互影响
src[0]=100 
dst[0]=10

fmt.Println("Copied elements:", n)     // Copied elements: 3
fmt.Println("Source slice:", src)      // Source slice: [100 2 3 4 5]
fmt.Println("Destination slice:", dst) // Destination slice: [10 2 3]

还有一种方式就是直接赋值的方式,这种方式实际底层还是指向的同一个地址,除非进行了扩容,否则修改原切片,复制的切片也是跟着修改

src := []int{1, 2, 3, 4, 5}
dst := src[1:]

src[1] = 100

fmt.Println("Source slice:", src)      // 输出:Source slice: [1 100 3 4 5]
fmt.Println("Destination slice:", dst) // 输出:Destination slice: [100 3 4 5]

3 常见面试题案例

3.1 切片扩容和底层数组共享

修改一个切片的元素可能会影响其他共享相同底层数组的切片。

arr := []int{1, 2, 3, 4, 5}
s1 := arr[1:4] // s1 = [2, 3, 4]
s1[0] = 10
fmt.Println("Array:", arr) //[1 10 3 4 5]

问题: s1arr 共享相同的底层数组,因此 s1 的修改会反映到 arr

3.2 切片扩容导致的不可预期行为

切片在扩容时,可能会重新分配底层数组,导致指向旧底层数组的切片失效。

s1 := []int{1, 2, 3}
s2 := s1
s1 = append(s1, 4, 5, 6)

fmt.Println("s1:", s1) // [1 2 3 4 5 6]
fmt.Println("s2:", s2) // [1 2 3]

问题:s1 扩容后,s2 仍指向旧的底层数组,因此 s2 不会看到扩容后的数据。

3.3 切片的容量和长度
s := []int{1, 2, 3}
s = append(s, 4)
fmt.Println("Length:", len(s)) // Length: 4
fmt.Println("Capacity:", cap(s)) // Capacity: 6 (根据扩容策略)
3.4 空切片的判定
var s1 []int
s2 := []int{}
fmt.Println(s1 == nil) //  true
fmt.Println(s2 == nil) //  false
3.5 切片切割

切片切割时,使用错误的索引或边界导致运行时错误。

s := []int{1, 2, 3, 4, 5}
fmt.Println(s[1:10]) // panic :slice bounds out of range [:10] with capacity 5

问题: 切片的切割操作超出了原切片的边界

3.6 切片传递到函数

修改切片的函数参数会影响原切片,扩容之后会指向一个新的底层数组。

func modifySlice(s []int) {
	s = append(s, 20)
	s[0] = 10
}

func main() {
	s := []int{1, 2, 3}
	modifySlice(s)
	fmt.Println("Slice after function:", s) // Slice after function: [1 2 3]
}

问题:

append 操作没有影响到原切片的容量,因为 s 在函数内重新指向了一个新的底层数组。

3.7 越界访问
s := make([]int, 10, 12)
v := s[10]

问题: 会发生 panic,切片长度为10,容量为 12,判断是否越界以长度为准, index = 10 已经越界.

3.8 扩容策略
s := make([]int, 512)
s = append(s, 1)
fmt.Printf("len of s: %d, cap of s: %d", len(s), cap(s))// len of s: 513, cap of s: 848

这道题目实际上考察的是切片的扩容策略以及内存对齐的知识。我们可以先回顾一下之前提到的扩容流程。

当切片 s 的原始容量为 512 时,由于已经超过了 256 的阈值,因此扩容后的新容量计算为 512 * 1.25 + 192 = 832

在申请内存时,依据切片元素的大小乘以容量来计算所需的内存空间,这里的结果是 8 字节 * 832 = 6656

随后,根据内存对齐的原则,内存会被补齐到 6784 这个档次。这样计算后,6784 / 8 = 848,因此最终的容量为 848。

如果您觉得有帮助,请关注我,另公众号【小张的编程世界】,如有任何错误或建议,欢迎指出。感谢您的阅读!

标签:Slice,int,fmt,len,切片,Println,slice,Go,解析
From: https://blog.csdn.net/luozong2689/article/details/141651890

相关文章

  • 2024-08-31:用go语言,给定一个数组apple,包含n个元素,每个元素表示一个包裹中的苹果数量;
    2024-08-31:用go语言,给定一个数组apple,包含n个元素,每个元素表示一个包裹中的苹果数量;另一个数组capacity包含m个元素,表示m个不同箱子的容量。有n个包裹,每个包裹内装有指定数量的苹果,以及m个箱子,每个箱子的容量不同。任务是将这n个包裹中的所有苹果重新分配到箱子中,最小化所需的......
  • 2024-08-31:用go语言,给定一个数组apple,包含n个元素,每个元素表示一个包裹中的苹果数量;
    2024-08-31:用go语言,给定一个数组apple,包含n个元素,每个元素表示一个包裹中的苹果数量;另一个数组capacity包含m个元素,表示m个不同箱子的容量。有n个包裹,每个包裹内装有指定数量的苹果,以及m个箱子,每个箱子的容量不同。任务是将这n个包裹中的所有苹果重新分配到箱子中,最小化所需的箱子......
  • zlib1.dll丢失全解析:压缩与解压修复的专家级策略
    zlib1.dll是一个与压缩和解压缩相关的动态链接库(DLL)文件,通常与zlib库的功能实现有关。这个DLL文件可能包含了处理数据压缩、资源管理和与其他系统组件交互等功能所需的函数和资源,对于确保使用zlib库的应用程序或服务的正常运行非常重要。当zlib1.dll文件丢失时,可能会导致以......
  • Spring高手之路22——AOP切面类的封装与解析
    1.AOP是如何收集切面类并封装的?在 Spring 中,AOP(Aspect-OrientedProgramming,面向切面编程)通过以下几个步骤收集切面类并进行封装:1.定义切面类:切面类通过 @Aspect 注解来标记,表示这是一个切面。在切面类中定义通知(advice),例如 @Before、@After、@Around 等,用于指定在目标方法......
  • Go实战全家桶之一:goconfig依赖注入扩展之自动注入配置项、工业级巨匠
    开源地址:goconfig:gitclonehttps://gitee.com/ichub/go.git基础结构packageichubconfigimport("gitee.com/ichub/goconfig/common/base/basedto""gitee.com/ichub/goconfig/common/base/baseutils/reflectutils""github.com/gogf/......
  • Goolge earth studio 进阶4——路径修改与平滑
    如果我们希望在大约中途时获得更多的城市鸟瞰视角。可以将相机拖动到这里并创建一个新的关键帧。camera_target_clip_7EarthStudio会自动平滑我们的路径,所以当我们通过这个关键帧时,不是一个生硬的角度,而是一个平滑的曲线。camera_target_clip_8路径上有贝塞尔控制......
  • AI写论文文献综述全指南:从理论到实践的全面解析
    在当今文献资料数量呈爆炸式的时代,如何快速的撰写一份高质量的论文文献综述成为了不得不面对的难题。随着人工智能技术的发展为文献综述的撰写提供了新的思路和方法,利用AI写论文文献综述可以大大的提高论文写作效率和质量。一、引言面对海量的文献资料,需要我们具备较高的学术......
  • Go plan9 汇编: 打通应用到底层的任督二脉
    原创文章,欢迎转载,转载请注明出处,谢谢。0.前言作为一个严肃的Gopher,了解汇编是必须的。本汇编系列文章会围绕基本的Go程序介绍汇编的基础知识。1.Go程序到汇编首先看一个简单到令人发指的示例:packagemainfuncmain(){ a:=1 print(a)}运行程序,输出:#gorun......
  • 深入解析多商户商城系统源码:如何开发直播商城小程序?
    本篇文章,小编将深入解析多商户商城系统源码的关键技术,并详细探讨如何基于这些源码开发一个功能完善的直播商城小程序。一、多商户商城系统源码的核心构架多商户商城系统源码的核心在于其能够支持多个商户独立运营,但同时又在一个统一的平台上实现数据的共享与整合。这个系统的核心构......
  • Android开发 - ClassLoader 加载外部类解析
    ClassLoader是什么ClassLoader主要作用是将字节码文件(.class文件)加载到Java虚拟机(JVM)中,以便应用程序可以使用这些类ClassLoader的好处模块化加载:应用程序可能由多个模块组成,而这些模块可能需要按需加载插件机制:很多应用支持插件化,插件在安装或更新后需要动态加载......