深入理解Go语言切片:共享内存与append()
陷阱
大家好!欢迎回到我的博客。?如果您在这里,您可能刚接触Golang,或者您是经验丰富的开发者,想深入了解切片的内部工作原理。那么,让我们开始吧!
Go语言因其简洁性和高效性而备受赞誉——正如人们常说的那样,“Go语言就是能完成工作”。对于我们这些来自C、C 或Java等语言的开发者来说,Go语言简洁明了的语法和易用性令人耳目一新。然而,即使在Go语言中,某些特性也可能让开发者感到困惑,尤其是在处理切片和子切片时。让我们揭开这些细微之处,更好地理解如何避免append()
和切片共享内存的常见陷阱。
Go语言中的切片是什么?
通常,当您需要一种数据结构来存储一系列值时,切片是Go语言中的首选。它们的灵活性来自于这样一个事实:它们的长度不是其类型的一部分。此特性克服了数组的限制,使我们能够创建一个可以处理任何大小切片的单个函数,并使切片能够根据需要增长或扩展。
虽然切片与数组有一些相似之处,例如都是可索引的并且具有长度,但它们在数据管理方式上有所不同。切片充当对底层数组的引用,该数组实际上存储切片的数据。从本质上讲,切片提供对该数组的某些或所有元素的视图。因此,当您创建一个切片时,Go会自动处理创建保存切片元素/数据的底层数组。
切片的共享内存
数组是连续的内存块,但使切片有趣的是它们如何引用此内存。让我们分解切片的结构:
<code class="language-go">type slice struct { array unsafe.Pointer // 指向底层数组的指针 len int // 切片中的元素数量 cap int // 底层数组的容量 }</code>
当您创建一个切片时,它包含三个组件:
len
切片的长度(它包含的元素数量)cap
容量(在需要增长之前它可以包含的元素数量)这就是事情变得有趣的地方。如果您有多个派生自同一数组的切片,则通过一个切片进行的更改将体现在其他切片中,因为它们共享相同的底层数组。
让我们看下面的例子:
<code class="language-go">package main import "fmt" func main() { // 创建一个具有初始值的切片 original := []int{1, 2, 3, 4, 5} // 创建一个子切片——两个切片共享相同的底层数组! subslice := original[1:3] fmt.Println("未修改的子切片:", subslice) // 输出 => 未修改的子切片: [2 3] // 修改子切片 subslice[0] = 42 fmt.Println("原始切片:", original) // 输出 => 原始切片: [1 42 3 4 5] fmt.Println("修改后的子切片:", subslice) // 输出 => 修改后的子切片: [42 3] }</code>
理解切片容量
在我们进一步深入之前,让我们尝试理解切片容量cap()
。当您从现有的Go语言切片中获取子切片时,新子切片的容量由原始切片从子切片开始位置的剩余容量决定。让我们稍微分解一下:
当您从数组创建切片时,切片的长度是它最初包含的元素数量,而它的容量是它在需要增长之前可以包含的元素总数。
当您从现有切片中获取子切片时:
让我们看一个详细的例子:
<code class="language-go">type slice struct { array unsafe.Pointer // 指向底层数组的指针 len int // 切片中的元素数量 cap int // 底层数组的容量 }</code>
subslice := original[1:4]
时,它指向从索引 1 到 3 的元素 (2, 3, 4)。subslice
的长度是 4 - 1 = 3。subslice
的容量是 5 - 1 = 4,因为它从索引 1 开始,并包含到原始切片末尾的元素。append()
陷阱!
这就是开发者经常被困住的地方。Go语言中的append()
函数在处理子切片时可能会导致意外行为。
子切片的容量包括不属于其长度但位于原始切片容量范围内的元素。这意味着如果子切片增长,它可以访问或修改这些元素。
让我们考虑这个例子:
<code class="language-go">package main import "fmt" func main() { // 创建一个具有初始值的切片 original := []int{1, 2, 3, 4, 5} // 创建一个子切片——两个切片共享相同的底层数组! subslice := original[1:3] fmt.Println("未修改的子切片:", subslice) // 输出 => 未修改的子切片: [2 3] // 修改子切片 subslice[0] = 42 fmt.Println("原始切片:", original) // 输出 => 原始切片: [1 42 3 4 5] fmt.Println("修改后的子切片:", subslice) // 输出 => 修改后的子切片: [42 3] }</code>
subslice
最初指向 2, 3,容量为 4(它可以增长到原始切片的末尾)。subslice
追加 60, 70 时,它使用原始切片的剩余容量。original
和subslice
都反映了这些更改,因为它们共享相同的底层数组。惊讶吗?append()
操作修改了原始切片,因为底层数组中有足够的容量。但是,如果我们超过容量或追加的元素超过容量允许的范围,Go将为子切片分配一个新的数组,从而打破与原始切片的共享:
<code class="language-go">func main() { // 原始切片 original := []int{1, 2, 3, 4, 5} // 创建一个子切片 subslice := original[1:4] // 指向元素 2, 3, 4 fmt.Println("子切片:", subslice) // 输出 => 子切片: [2 3 4] fmt.Println("子切片的长度:", len(subslice)) // 输出 => 子切片的长度: 3 fmt.Println("子切片的容量:", cap(subslice)) // 输出 => 子切片的容量: 4 }</code>
在这种情况下,append()
创建了一个新的底层数组,因为原始容量已超过。
避免陷阱的最佳实践
<code class="language-go">func main() { original := []int{1, 2, 3, 4, 5} subslice := original[1:3] // 指向元素 2, 3 fmt.Println("追加前原始切片:", original) // 输出 => [1 2 3 4 5] fmt.Println("追加前子切片:", subslice) // 输出 => [2 3] fmt.Println("子切片的容量:", cap(subslice)) // 输出 => 4 // 在容量范围内追加到子切片 subslice = append(subslice, 60, 70) // 追加到子切片后打印 fmt.Println("追加后原始切片:", original) // 输出 => [1 2 3 60 70] fmt.Println("追加后子切片:", subslice) // 输出 => [2 3 60 70] }</code>
主要优点是:
i. make([]int, len(subslice))
创建一个具有其自身独立底层数组的新切片。这至关重要——它不仅仅是一个新的切片头,而是在内存中一个全新的数组。
ii. copy()
然后只传输值,而不是内存引用。这就像复印一份文件而不是共享原始文件。
<code class="language-go">func main() { original := []int{1, 2, 3, 4, 5} subslice := original[1:3] // 指向元素 2, 3 // 追加超出子切片容量的元素 subslice = append(subslice, 60, 70, 80) fmt.Println("大容量追加后原始切片:", original) // 输出 => [1 2 3 4 5] fmt.Println("大容量追加后子切片:", subslice) // 输出 => [2 3 60 70 80] }</code>
<code class="language-go">// 假设我们从这里开始 original := []int{1, 2, 3, 4, 5} subslice := original[1:3] // subslice 指向 original 的底层数组 // 这是我们的解决方案: newSlice := make([]int, len(subslice)) // 步骤 1:创建新的底层数组 copy(newSlice, subslice) // 步骤 2:复制值</code>
主要优点是:
i. 数据保护:原始数据保持不变,防止意外副作用
ii. 可预测的行为:函数对输入没有隐藏的影响
iii. 并发安全:在处理过程中可以在其他 goroutine 中安全地使用原始数据
记住:
append()
是否创建新的底层数组取决于容量<code class="language-go">type slice struct { array unsafe.Pointer // 指向底层数组的指针 len int // 切片中的元素数量 cap int // 底层数组的容量 }</code>
祝您编码愉快。记住,能力越大,责任越大,尤其是在共享内存方面!?
恭喜您阅读完本文。
您觉得这篇资源有帮助吗?您有问题吗?或者您发现了错误或错别字?请在评论中留下您的反馈。
不要忘记与可能从中受益的其他人分享此资源。关注我以获取更多信息。
以上是进行切片和子链条:了解共享内存并避免```append''的详细内容。更多信息请关注PHP中文网其他相关文章!