Golang中变量切片的处理机制解析

Golang中变量切片的处理机制解析 我最初在Reddit上发布了这个问题,但一直没有得到想要的答案,所以尝试扩大提问范围。希望链接到其他网站的问题不会造成困扰。

Reddit上的相关讨论

我写了一段简短的代码来演示我在切片方面遇到的问题,并为每个问题创建了单独的案例。我将分别讨论它们,因为它们在切片行为中产生了一些真正的不一致。

package main

import (
	"fmt"
)

func printfSlice(slc []int, itr int) {

	fmt.Printf("\ncount : %v", itr)

	fmt.Printf("\nslice01 val : %v"+
		"\nslice01 addr : %p "+
		"\nslice length : %v "+
		"\nslice01 cap : %v ",
		slc, &slc, len(slc), cap(slc))

	fmt.Printf("\n")
}

func main() {
	fmt.Println("start")

	slice01 := []int{2, 3, 5, 7, 11, 23}
	count01 := 0

	//count 1
	count01++
	printfSlice(slice01, count01)

	//count 2
	count01++
	slice01 = slice01[0:0]
	printfSlice(slice01, count01)

	//count 3
	count01++
	slice01 = slice01[1:5]
	printfSlice(slice01, count01)

	//count 4
	count01++
	slice01 = slice01[:3]
	printfSlice(slice01, count01)

	//count 5
	count01++
	slice01 = slice01[:]
	printfSlice(slice01, count01)

	//count 6
	count01++
	slice01 = slice01[0:cap(slice01)]
	printfSlice(slice01, count01)

	//count 7
	count01++
	slice01 = slice01[1:4]
	printfSlice(slice01, count01)

输出结果:

count : 1
slice01 val : [2 3 5 7 11 23]
slice01 addr : 0xc000046420 
slice length : 6 
slice01 cap : 6 

count : 2
slice01 val : []
slice01 addr : 0xc000046460 
slice length : 0 
slice01 cap : 6 

count : 3
slice01 val : [3 5 7 11]
slice01 addr : 0xc0000464a0 
slice length : 4 
slice01 cap : 5 

count : 4
slice01 val : [3 5 7]
slice01 addr : 0xc0000464e0 
slice length : 3 
slice01 cap : 5 

count : 5
slice01 val : [3 5 7]
slice01 addr : 0xc000046520 
slice length : 3 
slice01 cap : 5 

count : 6
slice01 val : [3 5 7 11 23]
slice01 addr : 0xc000046560 
slice length : 5 
slice01 cap : 5 

count : 7
slice01 val : [5 7 11]
slice01 addr : 0xc0000465a0 
slice length : 3 
slice01 cap : 4 
delve closed with code 0

a) 指针和地址:

我理解切片是指针,但所有的值最终都有不同的地址。如果它们都指向同一个数组,难道不应该都有相同的地址吗?

b) 所有内容实际指向的底层对象是什么?:

在输出的第二部分,我通过选择空内容将切片长度设为0。然后在第三部分,我选择了slice01[1:5],得到了*[3 5 7 11]*,这完全没有意义。我在前一部分已经将slice01定义为空,为什么它不会返回空内容或错误?它现在还将容量减少到了5。为什么?它应该只减少长度而不是容量?为什么在切片中进行特定选择会减少容量,而选择空内容却没有?

然后在第四部分我选择了slice01 = slice01[:3](请记住我每次都在重用同一个变量)。但我没有得到原始切片中的*[2 3 5],而是从第三部分定义的切片中得到了[3 5 7]*。这更加令人困惑。在第三部分中,它从第一部分进行了选择,尽管我在第二部分重新定义了变量!现在在第四部分,当我进行另一个切片选择时,它从第三部分而不是第一部分进行选择?这到底是什么?

第五、六和七部分继续演示了同样的问题。

我在这里遗漏了什么?为什么切片会这样行为?是否存在某种我遗漏的模式或规则?


更多关于Golang中变量切片的处理机制解析的实战教程也可以访问 https://www.itying.com/category-94-b0.html

5 回复

好的,明白了。这样解释很合理,感谢您的详细回复!

更多关于Golang中变量切片的处理机制解析的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


感谢您的回复。这确实澄清了关于容量的困惑。不过关于地址我还有一些问题。

a)
在您调整后的函数脚本中,您添加了:

"\nunderlying array addr: %p\n",
slc, &slc, len(slc), cap(slc), slc

并且您使用slc作为底层数组。但这难道不和下面这行一样吗?

"\nslice01 addr : %p "+

然而您在结果中得到了两个不同的地址。这是怎么做到的?

slice01值:[2 3 5 7 11 23]
slice01地址:0xc000046420
切片长度:6
slice01容量:6
底层数组地址:0xc000072030
slc第一个元素位于0xc000072030,第二个元素位于0xc000072038

如果您在函数中对"底层数组地址"和"slice01地址"都使用"%p slc",怎么会得到两个不同的地址?

b)
您得到的底层数组内存地址与第一个元素的地址相同。为什么?元素的地址不应该与数组本身的地址不同吗?

a) 是的,我在两个地方都使用 %p 进行打印,但第一个是 &slc,第二个是 slc。它们并不相同,因为一个是获取切片的地址,即 printf("addr of the slice is %p, &slc)",而另一个给出的是切片所指向数组的地址,即 printf("addr of the array is %p, slc)",区别在于 & 符号。正如您在 Reddit 的回答和文档中可能看到的,切片是一个指向数组的指针、一个长度和一个容量,第二个打印(打印上述指针)将给出底层数组的地址,而第一个打印将给出指向切片的指针,即其地址。

b) 数组基本上是从某个地址开始的连续元素集合。在这个例子中,我们有一个整数数组,因此如果数组从 0xc000072030 开始,这意味着它的第一个元素在 0xc000072030,第二个元素在 0xc000072038(请注意,在您最新的示例中,它们正好相差 8 个字节,即 64 位,这是在 64 位机器上整数的大小,而在 Playground 中它们相差 4 个字节,因为 Playground 运行在 32 位架构上,因此整数的大小为 32 位),依此类推。Go 没有像 C 那样的指针算术,其中第一个元素和数组本身的关系更加明显,但在底层,它们基本上是相同的。

我在这里稍微简化了,但关键是数组的第一个元素实际上是数组的起点,这是有意义的,并且根据 Go 的语义,一旦您切片超过其初始成员,就无法返回,那么它的新第一个元素及其“新底层数组”(考虑切片的新起点)就会转移到您切片的位置。另一方面,切片只是指向数组的指针,而数组仍然存在于内存中,您可以重新扩展切片,但只能扩展到它指向的数组的极限。

也许这个稍微修改过的版本能帮你理清思路:https://play.golang.org/p/7pnNMqUVlq0

我声明了一个[6]int数组并通过切片操作获得了你最初使用的切片,然后打印了你之前打印的所有内容,还包括底层数组的地址和前两个元素(当存在时)的地址。如你所见,切片发生了变化,但它们都是同一个底层数组的不同视图。当你使用s[:n]或s[0:n]对数组进行切片时,你保留的是从起始点开始的视图,总是可以重新扩展回去。但当你使用s[1:n]进行切片时,你的视图从底层数组的位置1开始,这样实际上使新切片的容量变为5(在此例中)。如你所见,新切片的底层数组地址已经移动到原始切片位置1的元素处。正如https://blog.golang.org/go-slices-usage-and-internals中所述:

Earlier we sliced s to a length shorter than its capacity. We can grow s to its capacity by slicing it again
A slice cannot be grown beyond its capacity. Attempting to do so will cause a runtime panic, just as when indexing outside the bounds of a slice or array. Similarly, slices cannot be re-sliced below zero to access earlier elements in the array.

因此,通过重新切片已经从位置1开始的切片,你无法回到原始数组的位置0,但你可以在其容量范围内的任何位置进行扩展。

当你从非0位置开始重新切片并进一步减少容量时,这种情况会再次发生。当然,如果你保留了原始切片,在另一个变量中重新切片,然后继续对后者进行重新切片,前者将保持不变,仍然指向原始数组,长度和容量都为6,正如你在最后打印中看到的那样(不必在意变量名,只是保留了你的函数)。

希望这能澄清问题。

在Go语言中,切片的行为确实有其特定的机制,我来详细解析一下你的代码示例。

切片底层结构

首先需要理解切片的底层结构。切片不是简单的指针,而是一个包含三个字段的结构体:

  • 指向底层数组的指针
  • 长度(当前元素数量)
  • 容量(从起始位置到底层数组末尾的元素数量)
type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

代码执行过程分析

count 1 - 初始状态:

slice01 := []int{2, 3, 5, 7, 11, 23}
// 底层数组: [2, 3, 5, 7, 11, 23]
// len=6, cap=6, 指向数组起始位置

count 2 - 切片重置:

slice01 = slice01[0:0]
// 底层数组不变,但len=0, cap=6
// 仍然指向同一个数组,只是长度为0

count 3 - 从位置1开始切片:

slice01 = slice01[1:5]
// 这里的关键:slice01当前len=0,但底层数组仍然存在
// 从索引1开始切片,得到 [3, 5, 7, 11]
// len=4, cap=5 (因为从索引1开始,到底层数组末尾有5个位置)

count 4 - 限制长度:

slice01 = slice01[:3]
// 从当前切片取前3个元素: [3, 5, 7]
// len=3, cap保持不变=5

解答你的具体问题

a) 地址变化的原因

你打印的地址 &slc 是切片变量本身的地址,不是底层数组的地址。每次重新赋值 slice01 = ... 都会创建一个新的切片结构体,因此地址不同。

要查看底层数组地址,可以这样做:

func printfSlice(slc []int, itr int) {
    fmt.Printf("\ncount : %v", itr)
    
    // 获取底层数组的地址
    if len(slc) > 0 {
        fmt.Printf("\nunderlying array addr: %p", &slc[0])
    }
    
    fmt.Printf("\nslice val : %v"+
        "\nslice addr : %p "+
        "\nslice length : %v "+
        "\nslice cap : %v ",
        slc, &slc, len(slc), cap(slc))
}

b) 切片操作规则

切片操作遵循以下规则:

  • 切片操作 [low:high] 是在当前切片的可见范围内进行的
  • 容量计算:new_cap = old_cap - low
  • 切片操作不会创建新的底层数组,除非需要扩容

为什么count 3能工作:

// count 2之后:slice01长度为0,但底层数组仍然完整
// slice01[1:5] 从底层数组的索引1开始,取4个元素
// 这完全合法,因为底层数组仍然存在

容量减少的原因:

slice01 = slice01[1:5]
// 新容量 = 原容量(6) - 起始索引(1) = 5
// 这是Go语言的设计:容量表示从起始位置到底层数组末尾的元素数量

关键理解点

  1. 切片变量 vs 底层数组:切片变量是一个包含指针、长度、容量的结构体,底层数组是实际存储数据的地方

  2. 切片操作不复制数据:所有切片操作都共享同一个底层数组,直到需要扩容

  3. 重新赋值的本质slice01 = slice01[1:5] 创建了一个新的切片结构体,但指向同一个底层数组

  4. 可见性规则:切片操作只能访问当前切片长度范围内的元素,但可以扩展到容量范围内的元素

这个行为设计是为了高效的内存使用,避免不必要的数据复制。理解切片的三元组(指针、长度、容量)结构是掌握Go切片行为的关键。

回到顶部