Golang中这种实现方式的背后原因是什么?求大神解答

Golang中这种实现方式的背后原因是什么?求大神解答 在构建一个非常简单的缓存系统时,我遇到了一个场景:我有一个切片,在遍历它的过程中,它会被消耗。新传入的数据会追加到切片末尾,而发送操作则通过发送 slice[0](假设 len() > 0 检查通过)并使用 myslice = myslice[1:] 重建切片来处理。我在这里重新实现了一个类似的逻辑:

func main() {

	var finval []int = []int{1, 2, 3, 4, 5, 6, 7}
	var lenfin = len(finval)
	//for i := 0; i < len(finval); i++{
	for i := 0; i < lenfin; i++ {
		fmt.Println(i, ":", finval[0])
		finval = finval[1:]
	}
}

我注意到,在摆弄这个简化版本时,被注释掉的那一行在迭代过程中会重新检查 finval 的长度,导致输出为:

0 : 1
1 : 2
2 : 3
3 : 4

而不是预期的、正确的结果:

0 : 1
1 : 2
2 : 3
3 : 4
4 : 5
5 : 6
6 : 7

在我看来,这似乎很明显:每次循环时,它都会重新检查长度。我不确定原因,但我原本期望长度值在开始时被检查一次,之后不再改变。我这样期望是错的吗?这种实现方式是否有技术上的原因?我原以为需要一些涉及指针的“黑魔法”才能让它每次循环都重新检查。


更多关于Golang中这种实现方式的背后原因是什么?求大神解答的实战教程也可以访问 https://www.itying.com/category-94-b0.html

7 回复

它在每次迭代后重新评估结束条件,并且 finval 的长度在循环内部发生变化。

func main() {
    fmt.Println("hello world")
}

更多关于Golang中这种实现方式的背后原因是什么?求大神解答的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


Go 语言中没有 while 循环。

func main() {
    for i := 0; i < 10; i++ {
        fmt.Println(i)
    }
}

有一个类似的讨论在这里,也许能帮到你

实际上,Go语言中的for循环是这样的:

func main() {

	var finval []int = []int{1, 2, 3, 4, 5, 6, 7}
	var lenfin = len(finval)
        var i int = 0
        while i < lenfin {
                fmt.Println(i, ":", finval[0])
                finval = finval[1:]
                i++
        }
}

如果你在循环中检查长度,会是这样的:

func main() {

	var finval []int = []int{1, 2, 3, 4, 5, 6, 7}
	var lenfin = len(finval)
        var i int = 0
        while i < lenfin {
                fmt.Println(i, ":", finval[0])
                finval = finval[1:]
                lenfin = len(finval)  // 这里再次检查
                i++
        }
}

考虑以下代码:

func main() {
	shiftingTarget := 100
	for i := 0; i < shiftingTarget; i++ {
		shiftingTarget--
	}
	fmt.Println("All done and shifting target is", shiftingTarget)
}

它的行为完全符合你的预期,对吧?如果你把它反转为 for i := 0; shiftingTarget > i; i++ {,你也会期望它能正常工作,对吧?那么编译器如何知道你认为哪些属性是常量呢?来自 Go 语言之旅(强调部分是我加的):

基本的 for 循环有三个由分号分隔的组成部分:

  • 初始化语句:在第一次迭代前执行
  • 条件表达式在每次迭代前求值
  • 后置语句:在每次迭代结束时执行

初始化/条件/后置语句并没有什么特殊之处。如果你愿意,可以写成下面这样:

func main() {
	i := 0
	for fmt.Println("init"); condition(); fmt.Println("post") {
		if i == 10 {
			break
		}
		i++
	}
}

func condition() bool {
	fmt.Println("condition")
	return true
}

话虽如此,我认为你的循环更准确和简单的表示方式可能是这样的:

func main() {
	finval := []int{1, 2, 3, 4, 5, 6, 7}
	// 处理切片中的所有元素
	for len(finval) > 0 {
		fmt.Println(finval[0])
		finval = finval[1:]
	}
}

我的一点看法。 看看这篇文章:

DEV Community

理解 Go 中的切片及其内部原理

探索 Go 中的切片,它们是什么以及如何有效地使用它们。

或者,你可以打印代码的状态:

package main

import "fmt"

func main() {
	fmt.Printf("i \t lenfin \t len(finval) \t finval\n")
	main1()
	fmt.Println("-------------------------------")
	fmt.Printf("i \t lenfin \t len(finval) \t finval\n")
	main2()
}

func main1() {
	var finval []int = []int{1, 2, 3, 4, 5, 6, 7}
	var lenfin = len(finval)
	for i := 0; i < lenfin; i++ {
		//		fmt.Println(i, ":", finval[0])
		finval = finval[1:]
		printStatus(i, lenfin, len(finval), finval)
	}
}

func main2() {
	var finval []int = []int{1, 2, 3, 4, 5, 6, 7}
	for i := 0; i < len(finval); i++ {
		//		fmt.Println(i, ":", finval[0])
		finval = finval[1:]
		printStatus(i, len(finval), len(finval), finval)
	}
}

func printStatus(pos int, lenfin, size int, currentSlice []int) {
	fmt.Printf("%d \t %d \t \t %d \t \t %+v\n", pos, lenfin, size, currentSlice)
}

在Go语言中,切片在循环条件中重新计算长度是预期的行为,这并非“黑魔法”,而是由语言规范和实现机制决定的。

你观察到的现象是因为:

  1. 循环条件在每次迭代时都会重新求值
    for i := 0; i < len(finval); i++ 中,len(finval) 会在每次循环开始前调用,此时 finval 可能已经被修改(例如 finval = finval[1:]),因此长度会变化。

  2. 切片是引用类型
    切片本身是一个包含指向底层数组指针的结构,当你执行 finval = finval[1:] 时,你创建了一个新的切片头(slice header),但底层数组可能仍然是同一个。循环条件中的 len(finval) 会读取这个新切片头的长度字段。

  3. 你“修复”后的版本
    for i := 0; i < lenfin; i++ 中,lenfin 是一个在循环开始前就确定的整数值,不会随切片变化而改变,因此循环会执行固定的次数。

示例代码说明

package main

import "fmt"

func main() {
    // 原始版本:每次迭代重新计算长度
    fmt.Println("原始版本(错误逻辑):")
    finval := []int{1, 2, 3, 4, 5, 6, 7}
    for i := 0; i < len(finval); i++ {
        fmt.Println(i, ":", finval[0])
        finval = finval[1:] // 修改了 finval,下次 len(finval) 会变小
    }

    // 修复版本:提前保存长度
    fmt.Println("\n修复版本(正确逻辑):")
    finval2 := []int{1, 2, 3, 4, 5, 6, 7}
    lenfin := len(finval2)
    for i := 0; i < lenfin; i++ {
        fmt.Println(i, ":", finval2[0])
        finval2 = finval2[1:]
    }

    // 另一种常见模式:直接遍历切片
    fmt.Println("\n直接遍历切片(推荐):")
    finval3 := []int{1, 2, 3, 4, 5, 6, 7}
    for _, v := range finval3 {
        fmt.Println(v)
    }
}

输出:

原始版本(错误逻辑):
0 : 1
1 : 2
2 : 3
3 : 4

修复版本(正确逻辑):
0 : 1
1 : 2
2 : 3
3 : 4
4 : 5
5 : 6
6 : 7

直接遍历切片(推荐):
1
2
3
4
5
6
7

技术原因
Go的 for 循环条件表达式在每次迭代前都会求值,这是语言规范定义的。这样做是为了允许动态条件(例如从通道读取、检查变化的状态等)。如果你希望长度固定,就必须在循环前保存它。

在你的缓存系统场景中,如果需要在遍历时修改切片,通常建议使用 for len(slice) > 0 或提前保存长度,而不是依赖会变化的循环条件。

回到顶部