Golang中接口与结构体的意外行为/数据问题

Golang中接口与结构体的意外行为/数据问题 我一直难以理解,为什么当我从 Integer 结构体中移除 additional 字段时,下面的代码块会打印出数字值 2 而不是 0。

Play

package main

import (
	"fmt"
)

type Number interface {
	Value() int
}

type Integer struct {
	value int
	additional int
}
func (integer Integer) Value() int{
	return integer.value
}

func main() {
	number, err := getNumber()
	if err != nil {
		panic(err)
	}
	fmt.Printf("number val: %d\n", number.Value())
}
func getNumber() (Number, error) {
	var intNumber Integer
	return intNumber, setInteger(&intNumber)
}
func setInteger(num *Integer) error{
	num.value = 2
	return nil
}

更多关于Golang中接口与结构体的意外行为/数据问题的实战教程也可以访问 https://www.itying.com/category-94-b0.html

11 回复

我已编辑问题以澄清我的疑问,抱歉之前的解释不够清楚。

更多关于Golang中接口与结构体的意外行为/数据问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


我不是想打印出2,我想问的是,当我从Integer结构体中移除additional字段时,为什么它会打印出2。

是的,我明白了。我也不知道为什么,我只是想为这个问题提供更多数据,希望能帮助找到答案。: )

啊,抱歉,没看到更新。

不知为何,行为也取决于返回类型。如果我将第一个返回值从 Number 改为 Integer,那么无论字段如何,它总是返回相同的结果。

当我移除 additional 字段,不改变求值顺序但结果却发生了变化,你如何解释这一点?我原以为可能是编译优化做了什么,我禁用了优化和内联,但结果仍然相同。

感谢您的回答,但我再次强调,我不是在问如何打印 02,我想知道为什么这一个字段会改变结果?背后发生了什么,这是编译优化、一个错误,还是我哪里做错了(显然我没有)?

嗯,你通过 & 引用了 Number,这意味着在返回值之前,需要先通过函数进行计算,这样你就得到了之前设置为 2 的 Number.Value() 的值。

说实话,这个结构真的让我很困惑 :),这只是一个理论问题,还是来自生产代码的示例?

如果是后者,我可能会考虑改变代码的结构设计。

我不太理解这种行为,但只是稍微修改了代码, https://play.golang.org/p/IVit28szGXh 将对 setInteger 的调用移出返回语句,现在无论是否添加额外代码,它都能按预期工作。也许这个改动有助于理解此处的行为。

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

我在运行 go build 之前成功设置了 GOSSAFUNC=getNumber 环境变量,这让我在优化器遍历生成的 SSA 时获得了一系列差异。我看到 setNumber 中的赋值在“后期优化”(“阶段”,是吗?)中开始被优化掉,然后在“死自动消除”“阶段”中进一步优化,最后在“通用死代码”阶段中赋值完全消失。我想我会尝试从这些优化器中获取更多输出。

func main() {
    fmt.Println("hello world")
}
func getNumber() (Number, error) {
	var intNumber Integer
	return intNumber, setInteger(&intNumber)
}

return 语句的操作数——intNumbersetInteger(&intNumber)——的求值顺序是未指定的。它是未定义的,可能会变化。

Go 编程语言规范

求值顺序

为了保证顺序,请这样写:

func getNumber() (Number, error) {
    var intNumber Integer
    err := setInteger(&intNumber)
    return intNumber, err
}

package main

import (
	"fmt"
)

type Number interface {
	Value() int
}

type Integer struct {
	value      int
	additional int
}

func (integer Integer) Value() int {
	return integer.value
}

func main() {
	number, err := getNumber()
	if err != nil {
		panic(err)
	}
	fmt.Printf("number val: %d\n", number.Value())
}
func getNumber() (Number, error) {
	var intNumber Integer
	err := setInteger(&intNumber)
	return intNumber, err
}

func setInteger(num *Integer) error {
	num.value = 2
	return nil
}

https://play.golang.org/p/9K8TRe3uq6Z

number val: 2

这是因为Go语言中接口值的底层表示导致的。当接口类型变量存储具体类型的值时,它包含两部分信息:动态类型和动态值。

在你的代码中,关键问题出现在 getNumber() 函数中:

func getNumber() (Number, error) {
    var intNumber Integer
    return intNumber, setInteger(&intNumber)
}

这里发生了以下情况:

  1. intNumber 被声明为 Integer 类型的零值
  2. setInteger(&intNumber) 修改了 intNumber.value 为 2
  3. 但是 intNumber 作为值被返回给接口 Number

当结构体包含 additional 字段时,结构体的大小足以让编译器做出不同的内存布局决策。移除 additional 字段后,结构体大小改变,这影响了接口值的表示方式。

实际上,问题在于返回值顺序和接口值的捕获时机。更准确的解释是:在函数返回时,返回值被评估的顺序是从左到右。所以 intNumber 的值在调用 setInteger 之前就已经被确定了。

要修复这个问题,可以调整代码确保在接口捕获值之前完成修改:

func getNumber() (Number, error) {
    var intNumber Integer
    err := setInteger(&intNumber)
    return intNumber, err
}

// 或者使用指针接收器
type Integer struct {
    value int
}

func (integer *Integer) Value() int {
    if integer == nil {
        return 0
    }
    return integer.value
}

func getNumber() (Number, error) {
    var intNumber Integer
    setInteger(&intNumber)
    return &intNumber, nil
}

这样就能确保接口值包含的是修改后的数据。

回到顶部