Golang中指针不断变化的问题探讨

Golang中指针不断变化的问题探讨 我是Go语言的新手,正在尝试通过制作一个贪吃蛇游戏克隆版来学习它。在调试过程中,我注意到了一些奇怪的现象。

蛇的结构体有一个 body []raylib.Vector2(raylib.Vector2 只是一个包含 x 和 y 值的结构体),一个 head *raylib.Vector2 和一个 tail *raylib.Vector2,它还有一个游戏上下文(game_ctx *Game)。Game 是协调一切的主结构体。我注意到的问题是,headtail 指针的地址值在不断变化,没有被重新赋值,但 game_ctx 的地址却保持稳定。

我原本以为地址变化可能只是Go语言的工作方式,但当我看到 game_ctx 的地址不变化时,我开始相信是我做错了什么!

我认为有问题的函数如下:

func (this *Snake) draw() {
	for _, body_part := range this.body {
		color := rl.Black
		log.Debug("body_part: %p, tail: %p, head: %p", &body_part, this.tail, this.head)
		if &body_part == this.tail {
			color.R += 0xFF
		}
		if &body_part == this.head {
			color.B += 0xFF
		}

		if color == rl.Black {
			color = this.color
		}

		this.game_ctx.grid.drawCell(int(body_part.X), int(body_part.Y), color)

	}
}

输出如下(部分额外输出来自此函数外部):

🟩 | DEBUG | 23-10-2024 22:27:48:715 | Body: 0xc000132c00
🟩 | DEBUG | 23-10-2024 22:27:48:732 | body_part: 0xc000015578, tail: 0xc0001c0400, head: 0xc0001c0410
🟩 | DEBUG | 23-10-2024 22:27:48:732 | body_part: 0xc000015588, tail: 0xc0001c0400, head: 0xc0001c0410
🟩 | DEBUG | 23-10-2024 22:27:48:732 | body_part: 0xc000015598, tail: 0xc0001c0400, head: 0xc0001c0410
🟩 | DEBUG | 23-10-2024 22:27:48:732 | Snake: &{game_ctx:0xc00009e0c0 body:[{X:0 Y:0} {X:0 Y:1} {X:1 Y:1}] head:0xc0001c0410 tail:0xc0001c0400 color:{R:0 G:228 B:48 A:255} direction:1}
🟩 | DEBUG | 23-10-2024 22:27:48:732 | Body: 0xc0001c0400
🟩 | DEBUG | 23-10-2024 22:27:48:748 | body_part: 0xc000180e18, tail: 0xc0000d0000, head: 0xc0000d0010
🟩 | DEBUG | 23-10-2024 22:27:48:748 | body_part: 0xc000015640, tail: 0xc0000d0000, head: 0xc0000d0010
🟩 | DEBUG | 23-10-2024 22:27:48:748 | body_part: 0xc000015650, tail: 0xc0000d0000, head: 0xc0000d0010
🟩 | DEBUG | 23-10-2024 22:27:48:748 | Snake: &{game_ctx:0xc00009e0c0 body:[{X:0 Y:0} {X:0 Y:1} {X:1 Y:1}] head:0xc0000d0010 tail:0xc0000d0000 color:{R:0 G:228 B:48 A:255} direction:1}
🟩 | DEBUG | 23-10-2024 22:27:48:749 | Body: 0xc0000d0000
🟩 | DEBUG | 23-10-2024 22:27:48:765 | body_part: 0xc0001129e0, tail: 0xc0000d0400, head: 0xc0000d0410
🟩 | DEBUG | 23-10-2024 22:27:48:765 | body_part: 0xc0000156f8, tail: 0xc0000d0400, head: 0xc0000d0410
🟩 | DEBUG | 23-10-2024 22:27:48:765 | body_part: 0xc000015708, tail: 0xc0000d0400, head: 0xc0000d0410
🟩 | DEBUG | 23-10-2024 22:27:48:765 | Snake: &{game_ctx:0xc00009e0c0 body:[{X:0 Y:0} {X:0 Y:1} {X:1 Y:1}] head:0xc0000d0410 tail:0xc0000d0400 color:{R:0 G:228 B:48 A:255} direction:1}
🟩 | DEBUG | 23-10-2024 22:27:48:765 | Body: 0xc0000d0400
🟩 | DEBUG | 23-10-2024 22:27:48:782 | body_part: 0xc0000157a8, tail: 0xc0000d0800, head: 0xc0000d0810
🟩 | DEBUG | 23-10-2024 22:27:48:782 | body_part: 0xc0000157b8, tail: 0xc0000d0800, head: 0xc0000d0810
🟩 | DEBUG | 23-10-2024 22:27:48:782 | body_part: 0xc0000157c8, tail: 0xc0000d0800, head: 0xc0000d0810
🟩 | DEBUG | 23-10-2024 22:27:48:782 | Snake: &{game_ctx:0xc00009e0c0 body:[{X:0 Y:0} {X:0 Y:1} {X:1 Y:1}] head:0xc0000d0810 tail:0xc0000d0800 color:{R:0 G:228 B:48 A:255} direction:1}
🟩 | DEBUG | 23-10-2024 22:27:48:782 | Body: 0xc0000d0800
🟩 | DEBUG | 23-10-2024 22:27:48:799 | body_part: 0xc000180e28, tail: 0xc0000d0c00, head: 0xc0000d0c10
🟩 | DEBUG | 23-10-2024 22:27:48:799 | body_part: 0xc000015868, tail: 0xc0000d0c00, head: 0xc0000d0c10
🟩 | DEBUG | 23-10-2024 22:27:48:799 | body_part: 0xc000015878, tail: 0xc0000d0c00, head: 0xc0000d0c10
🟩 | DEBUG | 23-10-2024 22:27:48:799 | Snake: &{game_ctx:0xc00009e0c0 body:[{X:0 Y:0} {X:0 Y:1} {X:1 Y:1}] head:0xc0000d0c10 tail:0xc0000d0c00 color:{R:0 G:228 B:48 A:255} direction:1}
🟩 | DEBUG | 23-10-2024 22:27:48:799 | Body: 0xc0000d0c00
🟩 | DEBUG | 23-10-2024 22:27:48:815 | body_part: 0xc000015918, tail: 0xc0000d1000, head: 0xc0000d1010
🟩 | DEBUG | 23-10-2024 22:27:48:815 | body_part: 0xc000015928, tail: 0xc0000d1000, head: 0xc0000d1010
🟩 | DEBUG | 23-10-2024 22:27:48:815 | body_part: 0xc000015938, tail: 0xc0000d1000, head: 0xc0000d1010
🟩 | DEBUG | 23-10-2024 22:27:48:815 | Snake: &{game_ctx:0xc00009e0c0 body:[{X:0 Y:0} {X:0 Y:1} {X:1 Y:1}] head:0xc0000d1010 tail:0xc0000d1000 color:{R:0 G:228 B:48 A:255} direction:1}
🟩 | DEBUG | 23-10-2024 22:27:48:815 | Body: 0xc0000d1000
🟩 | DEBUG | 23-10-2024 22:27:48:832 | body_part: 0xc0000159d8, tail: 0xc0000d1400, head: 0xc0000d1410
🟩 | DEBUG | 23-10-2024 22:27:48:832 | body_part: 0xc0000159e8, tail: 0xc0000d1400, head: 0xc0000d1410
🟩 | DEBUG | 23-10-2024 22:27:48:832 | body_part: 0xc0000159f8, tail: 0xc0000d1400, head: 0xc0000d1410
🟩 | DEBUG | 23-10-2024 22:27:48:832 | Snake: &{game_ctx:0xc00009e0c0 body:[{X:0 Y:0} {X:0 Y:1} {X:1 Y:1}] head:0xc0000d1410 tail:0xc0000d1400 color:{R:0 G:228 B:48 A:255} direction:1}

另一个问题是,我试图查看切片中的某个元素是否与 tailhead 具有相同的地址,但范围变量给出的地址似乎完全不同。

完整代码的仓库在这里:GitHub - NikosGour/snake_clone_go: A snake clone in Golang using Raylib

非常感谢任何能帮助我理解为什么地址在没有重新赋值的情况下不断变化的帮助!


更多关于Golang中指针不断变化的问题探讨的实战教程也可以访问 https://www.itying.com/category-94-b0.html

4 回复

看起来这是范围迭代器的问题,在 go1.23 版本之前,迭代器共享一个公共地址,因此指针变量会发生变化。

func (this *Snake) draw() {
    // 将 this.body 改为 []*raylib.Vector2 会更好
	for _, body_part := range this.body {
        part := body_part
		color := rl.Black
		log.Debug("body_part: %p, tail: %p, head: %p", &body_part, this.tail, this.head)
		if part == this.tail {
			color.R += 0xFF
		}
		if part == this.head {
			color.B += 0xFF
		}

		if color == rl.Black {
			color = this.color
		}

		this.game_ctx.grid.drawCell(int(body_part.X), int(body_part.Y), color)

	}
}

更多关于Golang中指针不断变化的问题探讨的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


是的,我考虑过将 body 设为 []*rl.Vector2 的解决方案,但我不想这样做,因为这会让我的思路变得更复杂。我找到了一个不同的解决方案:

func (this *Snake) draw() {
	for i := range this.body {
		body_part := &this.body[i]
		color := rl.Black
		// log.Debug("body_part: %p, tail: %p, head: %p", body_part, this.tail, this.head)
		if body_part == this.tail {
			color.R += 0xFF
		}
		if body_part == this.head {
			color.B += 0xFF
		}

		if color == rl.Black {
			color = this.color
		}

		this.game_ctx.grid.drawCell(int(body_part.X), int(body_part.Y), color)

	}
}

这不是一个好的解决方案。如果涉及切片扩容,同样会出现指针地址不一致的问题。

	ls := []int{1, 2, 3}
	fmt.Println(&ls[0]) //0xc00001e330
	ls = append(ls, []int{4, 5, 6, 7, 8, 9, 10, 11, 12}...)
	fmt.Println(&ls[0]) //0xc00002a840

如果 Vector2 是一个可比较的类型,那么也可以这样做:

type Vector2 struct {
	X float32
	Y float32
}

func (this *Snake) draw() {
    // this.tail 和 this.head 是 raylib.Vector2 类型,不是 *raylib.Vector2
    // this.body 是 []raylib.Vector2 类型
	for _, body_part := range this.body {
		color := rl.Black
		if body_part == this.tail {
			color.R += 0xFF
		}
		if body_part == this.head {
			color.B += 0xFF
		}

		if color == rl.Black {
			color = this.color
		}

		this.game_ctx.grid.drawCell(int(body_part.X), int(body_part.Y), color)
	}
}

如果你不想处理指针,这样做会更稳定一些。

你的问题源于对Go语言中切片和指针行为的误解。在draw()函数中,你使用了for range循环,这会创建切片元素的副本,导致你获取的是临时变量的地址,而不是切片中原始元素的地址。

以下是具体分析和解决方案:

问题分析

  1. for range循环创建副本

    for _, body_part := range this.body {
        // body_part 是 raylib.Vector2 的副本,不是原始元素
        // &body_part 获取的是副本的地址,每次循环都不同
    }
    
  2. 切片底层数组可能重新分配: 当切片容量不足时,Go会分配新的底层数组,导致所有元素的地址都发生变化。

解决方案

方案1:使用索引访问切片元素

func (this *Snake) draw() {
    for i := range this.body {
        color := rl.Black
        // 直接获取切片中元素的地址
        bodyPartPtr := &this.body[i]
        
        log.Debug("body_part: %p, tail: %p, head: %p", 
            bodyPartPtr, this.tail, this.head)
        
        if bodyPartPtr == this.tail {
            color.R += 0xFF
        }
        if bodyPartPtr == this.head {
            color.B += 0xFF
        }

        if color == rl.Black {
            color = this.color
        }

        this.game_ctx.grid.drawCell(int(this.body[i].X), int(this.body[i].Y), color)
    }
}

方案2:存储切片索引而不是指针

修改Snake结构体,存储head和tail的索引:

type Snake struct {
    game_ctx  *Game
    body      []raylib.Vector2
    headIndex int
    tailIndex int
    color     raylib.Color
    direction int
}

// 初始化时设置索引
func (this *Snake) init() {
    this.headIndex = len(this.body) - 1
    this.tailIndex = 0
}

func (this *Snake) draw() {
    for i := range this.body {
        color := rl.Black
        
        if i == this.tailIndex {
            color.R += 0xFF
        }
        if i == this.headIndex {
            color.B += 0xFF
        }

        if color == rl.Black {
            color = this.color
        }

        this.game_ctx.grid.drawCell(int(this.body[i].X), int(this.body[i].Y), color)
    }
}

方案3:使用指针切片

type Snake struct {
    game_ctx  *Game
    body      []*raylib.Vector2  // 存储指针
    head      *raylib.Vector2
    tail      *raylib.Vector2
    color     raylib.Color
    direction int
}

func (this *Snake) draw() {
    for _, bodyPartPtr := range this.body {
        color := rl.Black
        
        log.Debug("body_part: %p, tail: %p, head: %p", 
            bodyPartPtr, this.tail, this.head)
        
        if bodyPartPtr == this.tail {
            color.R += 0xFF
        }
        if bodyPartPtr == this.head {
            color.B += 0xFF
        }

        if color == rl.Black {
            color = this.color
        }

        this.game_ctx.grid.drawCell(int(bodyPartPtr.X), int(bodyPartPtr.Y), color)
    }
}

为什么game_ctx地址稳定

game_ctx是指向Game结构体的指针,这个指针值在Snake的生命周期内不会改变。而body切片中的元素地址会变化,是因为:

  1. 切片扩容时底层数组重新分配
  2. 切片重新分配(如使用append
  3. for range循环创建副本

示例:演示地址变化

package main

import "fmt"

type Vector2 struct {
    X, Y float32
}

func main() {
    // 创建切片
    body := []Vector2{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 1, Y: 1}}
    
    // 获取原始元素的地址
    fmt.Printf("原始元素地址: %p, %p, %p\n", 
        &body[0], &body[1], &body[2])
    
    // for range循环 - 获取的是副本地址
    for i, v := range body {
        fmt.Printf("循环副本地址[%d]: %p\n", i, &v)
    }
    
    // 使用索引 - 获取的是原始地址
    for i := range body {
        fmt.Printf("原始元素地址[%d]: %p\n", i, &body[i])
    }
    
    // 演示切片扩容
    body = append(body, Vector2{X: 2, Y: 2})
    fmt.Printf("扩容后地址: %p, %p, %p, %p\n", 
        &body[0], &body[1], &body[2], &body[3])
}

输出会显示for range循环中的地址与原始切片元素地址不同,且切片扩容后地址发生变化。

建议采用方案2(使用索引)作为最佳实践,因为它避免了指针比较的复杂性,且更符合Go语言的惯用法。

回到顶部