首页 > 后端开发 > Golang > 进行切片和子链条:了解共享内存并避免```append''

进行切片和子链条:了解共享内存并避免```append''

Barbara Streisand
发布: 2025-01-29 00:21:10
原创
846 人浏览过

Go Slices and Subslices: Understanding Shared Memory and Avoiding `append()` Pitfalls

深入理解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>
登录后复制
登录后复制
登录后复制

当您创建一个切片时,它包含三个组件:

  1. 指向底层数组的指针
  2. len 切片的长度(它包含的元素数量)
  3. 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>
登录后复制
登录后复制
登录后复制
  • 原始切片有 5 个元素,长度和容量均为 5。
  • 当您使用 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 时,它使用原始切片的剩余容量。
  • originalsubslice都反映了这些更改,因为它们共享相同的底层数组。

惊讶吗?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中文网其他相关文章!

来源:php.cn
本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
作者最新文章
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板