Golang中内存、作用域与函数返回的解析

Golang中内存、作用域与函数返回的解析 大家好,

这是我的第一个帖子,请多多包涵。我是一名Go语言新手,大约20年前曾是一名C++程序员,之后从事了其他职业。我学习Go语言大约只有一个半月,而且是兼职学习,所以并没有看过所有的教程。对于这个问题(这仍然只是一个新手问题,抱歉!),谷歌搜索到的页面解释得有些浅显,所以我觉得需要请教一下真正的人。

那么,我对下面的代码有两个问题。这段代码是我实际代码的(极大)简化版本,但它具有我不理解/不想要的不良行为。

  1. 第一个问题(次要的):为什么我不能直接将指针赋值给函数返回对象的地址?例如,我下面代码的第44-47行本可以是: dss.dOnep := &NewDude(“Albert”) dss.dTwop := &NewDude(“Bella”) 但这似乎行不通(而我给出的代码确实有效)。

  2. 更重要的是,为什么在最终的代码中,反转操作没有“保持”?也就是说,我以为在第67行调用Invert时,我是在直接操作第65行myDudes的内存,但我们可以看到,虽然在Invert()函数调用内部发生了正确的事情(参见我的playground输出),但在返回后顺序又切换回来了。

以下是代码:

package main

import (
    "fmt"
)

type dudeStruct struct {
    dude string
}

// Dude 是 dudeStruct 的接口。
// 可以创建一个 Dude 并获取其 HelloString()。
type Dude interface {
    HelloString() string
}

// NewDude 创建一个指向新 Dude 的指针,并指定名称。
func NewDude(name string) Dude {
    var ds dudeStruct
    ds.dude = name
    return ds
}

// String 为 dudeStruct 对象提供打印字符串。
func (ms dudeStruct) String() string {
    return "Hello from " + ms.dude + ".  "
}

// 现在是一个包含两个 dude 的集合
type dudesStruct struct {
    dOnep *Dude
    dTwop *Dude
}

// Dudes 是作为有序集合的两个 dude 的接口。
type Dudes interface {
    String() string
    Invert()
}

// MakeDudes 如其名所示。返回指向创建的 Dudes 对象的指针。
func MakeDudes(dOne, dTwo string) Dudes {
    var dss dudesStruct
    albert := NewDude("Albert")
    bella := NewDude("Bella")
    dss.dOnep = &albert
    dss.dTwop = &bella
    return &dss
}
// String 为 dudesStruct 对象提供打印字符串。
func (dss dudesStruct) String() string {
    return ((*dss.dOnep).HelloString() + (*dss.dTwop).HelloString())
}
// Invert 应该交换两个 dude 的顺序。它在内部确实这样做了,但事后却没有保持。
func (dss dudesStruct) Invert() {
    fmt.Println("In Invert() – before inversion: `" + dss.String() + "'")
    swapSpacep := dss.dOnep
    dss.dOnep = dss.dTwop
    dss.dTwop = swapSpacep
    fmt.Println("In Invert() -- after inversion: `" + dss.String() + "'")
}

func main() {
    var myDudes Dudes
    myDudes = MakeDudes("Alice", "Bob")
    fmt.Println(myDudes.String())
    myDudes.Invert()
    fmt.Println(myDudes.String())
}

以下是 Go Playground 的输出:


Hello from Albert. Hello from Bella.

In Invert() – before inversion: `Hello from Albert. Hello from Bella. ’

In Invert() – after inversion: `Hello from Bella. Hello from Albert. ’

Hello from Albert. Hello from Bella.


显然,正如正确的那样,我希望 Bella 排在第一位!!

另外,如果对我所做的任何愚蠢之处有任何(温和的)建议,我将不胜感激。


更多关于Golang中内存、作用域与函数返回的解析的实战教程也可以访问 https://www.itying.com/category-94-b0.html

5 回复

谢谢!我想我现在明白了。

试图理解 Go 在“底层”如何处理内存有点困难,因为我看到的大多数入门教程都只是像这样做这就会发生。你的评论对我帮助很大。

更多关于Golang中内存、作用域与函数返回的解析的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


首先,欢迎来到论坛!

其次,发帖时请在代码前后加上三个“反引号”,这样更容易阅读。例如:

代码放在这里

现在,来回答你的问题!

我一开始是写答案的,但它们逐渐变成了冗长的段落,所以我觉得最好还是直接在代码上做注释修改:https://play.golang.org/p/LglFe0kbnaI

我认为你问题的根源在于,你传递的是实现了 DudeDudes 接口的结构体(这对 dudeStruct 没问题,但对 dudesStruct 不行),但却使用了指向你接口的指针。指向接口的指针与指向接口内包装的值的指针是不同的。让我感到困惑的一点是看到 *Dude(*dss.dOnep).HelloString()。请别介意,因为这并非特指你的代码,但每当我看到指向接口的指针时,我都高度怀疑有问题。你可以传递指向 dudeStruct 的指针来实现 Dude 接口,但你不应该传递指向 Dude 接口的指针。这就是为什么你在调用 HelloString 之前必须进行显式的指针解引用。

我理解这可以看作是实现多态的一种机制,但我不明白在函数体内部使用指针,而在声明的返回类型中却不使用指针的技术语言上的理由。

听起来这里可能存在对 Go 语言中接口类型和具体类型的一些混淆。接口类型很明显,因为它们的定义中包含 interface 关键字。具体类型则是其他所有类型(例如 dudeStruct*dudeStructstring 等)。接口本质上是对具体类型值的包装。当你返回一个 dudesStruct 值,但函数的返回类型是 Dudes 接口时,dudesStruct 会被分配在堆上,然后被装箱成一个(类型,数据)指针对(这是 Go 接口当前的实现方式),接着这个指针对会作为 Dudes 接口被返回。

如果返回的是指向 dudesStruct 的指针而不是其值,那么 MakeDudes 返回值中的 type 指针就不同了:它不再指向 dudesStruct 的类型信息,而是指向 *dudesStruct 的类型信息。

指针是实现间接引用的最常见方式,以便你可以修改指针指向的目标。(*dudeStruct).InvertInPlace 函数需要能够修改 dss 内部的字段,所以它必须是指针。否则,它就是一个副本,因为按值传递结构体就意味着复制它。

根据我们的讨论,我猜这段代码实际上在分配给函数调用的栈内存上创建了整个 Y 的副本,然后反转操作发生在这个副本上,之后这个副本 Y 会被释放给垃圾回收器?(所以,X 不受影响,就像我最初的代码那样)?

正确。

在 Go 中,显式的函数参数总是按值传递。方法接收器仍然是函数参数;事实上,你可以像这样调用 dudesStruct.InvertInPlace

func main() {
    var ds dudesStruct
    (*dudesStruct).InvertInPlace(&ds)

    // 你也可以像这样调用 `Inverted`:
    dudes := (dudeStruct).Inverted(ds)
}

请注意,仅仅因为你可以这样调用方法,并不意味着你应该这样做。我只是想说明,Go 中没有像 C#、C++ 或 Java 等语言中那样的特殊“this”参数,它是一种特殊的“引用”类型。

感谢,@skillian

这非常有帮助,当然我现在可以让我的代码实现我想要的功能了,并且现在也有一些重构/重组的工作要做。

话虽如此,仍有几点我还不理解:也就是说,我可以“做到”,但我还没有“理解透”。

第一个问题:

  • 函数 MakeDudes 的返回类型是 Dudes
func MakeDudes(dOneName, dTwoName string) Dudes {
	return &dudesStruct{
		dOne: NewDude(dOneName),
		dTwo: NewDude(dTwoName),
	}
}

但我们实际上返回的是 dudesStruct 的地址。我理解这可以看作是实现多态的一种机制,但我不理解在函数体内部使用指针而在声明的返回类型中不使用指针的技术语言依据。在栈内存中为 MakeDudes 分配了空间,并在其中创建了局部的 dudeStruct。在返回语句中,我们要么是返回一个指向所创建副本地址的指针的副本(在我的机器上是64位整数,在playground上是32位整数),然后这个指针副本会被垃圾回收器保留直到不再使用;要么是在某个地方(我可能会说在堆上)构建了整个 dudesStruct 对象的副本,但我们声称返回的是一个东西而不是指向一个东西的指针,所以我不确定。也许因为我们返回的是一个接口,所以我们返回的不是一个“东西”?

第二个问题:

  • 在你的 InvertInPlace 中:
// InvertInPlace 反转其内部的两个人。
func (dss *dudesStruct) InvertInPlace() {
	dss.dOne, dss.dTwo = dss.dTwo, dss.dOne
}

据我所见,与我的代码的主要区别在于函数钩子 func (dss *dudesStruct) InvertInPlace() 使用了指向调用函数的底层对象的指针,而不是对象本身。这真的让我很困惑。如果我改用这段代码调用函数:

// InvertInPlace 反转其内部的两个人。
func (dss dudesStruct) InvertInPlace() {
	dss.dOne, dss.dTwo = dss.dTwo, dss.dOne
}

我本以为函数是作用于实际的调用对象 X 而不是一个副本。这让我非常困惑。根据我们的讨论,我猜测这段代码实际上在分配给函数调用的栈内存上创建了整个 Y 的副本,然后反转发生在这个副本中,之后这个副本 Y 就会被释放给垃圾回收器?(所以,X 不受影响,就像我原来的代码一样?)

另外,感谢你提醒我 Go 语言内部的交换机制,它简化了我传统的交换方式:

dss.dOne, dss.dTwo = dss.dTwo, dss.dOne

我在某处读到过这个,但在我学习用 Go 编码的过程中还没有把它整合进来。

非常感谢!

问题1:指针赋值问题

在Go中,接口值已经是引用类型(包含类型指针和数据指针)。当你尝试 &NewDude("Albert") 时,实际上是在获取接口值的地址,而不是底层结构体的地址。正确的做法是:

// 方法1:先创建接口值,再取地址
albert := NewDude("Albert")
dss.dOnep = &albert

// 方法2:直接创建指针(需要修改NewDude返回指针)
func NewDude(name string) *dudeStruct {
    return &dudeStruct{dude: name}
}
// 然后可以这样使用:
dss.dOnep = NewDude("Albert")

问题2:反转未保持的原因

Invert() 方法使用了值接收器 (dss dudesStruct),这意味着方法操作的是结构体的副本。修改不会影响原始对象。改为指针接收器即可:

// 修改1:改为指针接收器
func (dss *dudesStruct) Invert() {
    fmt.Println("In Invert() – before inversion: `" + dss.String() + "'")
    swapSpacep := dss.dOnep
    dss.dOnep = dss.dTwop
    dss.dTwop = swapSpacep
    fmt.Println("In Invert() -- after inversion: `" + dss.String() + "'")
}

// 修改2:String方法也需要改为指针接收器以保持一致性
func (dss *dudesStruct) String() string {
    return ((*dss.dOnep).HelloString() + (*dss.dTwop).HelloString())
}

完整修正示例:

package main

import (
    "fmt"
)

type dudeStruct struct {
    dude string
}

func (ds dudeStruct) HelloString() string {
    return "Hello from " + ds.dude + ".  "
}

func NewDude(name string) Dude {
    return dudeStruct{dude: name}
}

type dudesStruct struct {
    dOnep *Dude
    dTwop *Dude
}

func MakeDudes(dOne, dTwo string) Dudes {
    dss := &dudesStruct{}
    albert := NewDude("Albert")
    bella := NewDude("Bella")
    dss.dOnep = &albert
    dss.dTwop = &bella
    return dss
}

func (dss *dudesStruct) String() string {
    return ((*dss.dOnep).HelloString() + (*dss.dTwop).HelloString())
}

func (dss *dudesStruct) Invert() {
    fmt.Println("In Invert() – before inversion: `" + dss.String() + "'")
    swapSpacep := dss.dOnep
    dss.dOnep = dss.dTwop
    dss.dTwop = swapSpacep
    fmt.Println("In Invert() -- after inversion: `" + dss.String() + "'")
}

func main() {
    myDudes := MakeDudes("Alice", "Bob")
    fmt.Println(myDudes.String())
    myDudes.Invert()
    fmt.Println(myDudes.String())
}

输出:

Hello from Albert.  Hello from Bella.
In Invert() – before inversion: `Hello from Albert.  Hello from Bella.  '
In Invert() -- after inversion: `Hello from Bella.  Hello from Albert.  '
Hello from Bella.  Hello from Albert.

关键点:

  1. 值接收器操作副本,指针接收器操作原对象
  2. 接口包含的指针需要正确解引用
  3. Go中接口值本身是双字结构(类型指针+数据指针)
回到顶部