Golang语言:我一直以为Go是按值传递的语言

Golang语言:我一直以为Go是按值传递的语言 以下是我的代码以及其后的输出——我将我的问题写在了输出中。但总结一下:一个函数如何能够修改它的一个参数,即一个(包含切片的)结构体?如下所示。

package main

import (
	"fmt"
)

type bType struct {
	s []int // 这里本应显示为 s []int - 抱歉,请参见底部的补充问题
	w []int
}

func main() {

	bOrig := bType{s: []int{11, 12, 13, 14, 15, 16}, w: []int{21, 22, 23, 24, 25, 26}}
	bSecond := bOrig

	bThird := bType{}
	bThird = bOrig
	bFourth := bType{}
	bFourth = clonebType(bOrig)
	fmt.Printf("After Creation\n    bOrig: %v\n", bOrig)
	fmt.Printf("  bSecond: %v\n", bSecond)
	fmt.Printf("   bThird: %v\n", bThird)
	fmt.Printf("   bFourth: %v\n", bFourth)

	bSecond.s[2] += 70
	bThird.s[4] += 80

	fmt.Printf("\nAfter bSecond.s[2] += 70 and bThird.s[4] += 80\n    bOrig: %v\n", bOrig)
	fmt.Printf("  bSecond: %v\n", bSecond)
	fmt.Printf("   bThird: %v\n", bThird)
	fmt.Printf("   bFourth: %v\n", bFourth)
	fmt.Printf("\nSIDE QUESTION:\n   OK so I get it, in a structure with slices when copied with a \n      simple = or := the slices still point to the same memory locations. \n      Is that going to be true for primitive types or sub structures of non primitive types?")

	fmt.Printf("\n\nPRIMARY QUESTION:\n   Below I pass a structure (bOrig) containing slices to a \n      method that changes part of the slice it then returns nothing.\n      Upon return the structure that was passed reflects the changes made to it in the method.\n      My question is: given this behavior how can Go be called a language that uses\n      CALL BY VALUE???\n\n" +
		"Am I missing something?")

	bOrig.methodmessUpbType()

	fmt.Printf("\nAfter bOrig.methodmessUpbType()\n    bOrig: %v\n", bOrig)
	fmt.Printf("  bSecond: %v\n", bSecond)
	fmt.Printf("   bThird: %v\n", bThird)
	fmt.Printf("   bFourth: %v\n", bFourth)
}
func clonebType(bIn bType) bType {
	var bOut bType
	bOut.s = make([]int, len(bIn.s))
	copy(bOut.s, bIn.s)
	bOut.w = make([]int, len(bIn.w))
	copy(bOut.w, bIn.w)
	return bOut
}
// 未能弄清楚如何安装和使用 go-clone,所以自己写了一个方法:clonebType
// 请务必安装 go get GitHub - huandu/go-clone: Clone any Go data structure deeply and thoroughly.
// "github.com/huandu/go-clone/generic"
func (bIn bType) methodmessUpbType() {
	bIn.s[0] += 1000
	return
}

以及输出:

GOROOT=C:\Users\dan\Dropbox\00 DropBox DAP\Go Language\go1.23.1 #gosetup
GOPATH=C:\Users\dan\go #gosetup
"C:\Users\dan\Dropbox\00 DropBox DAP\Go Language\go1.23.1\bin\go.exe" build -o C:\Users\dan\AppData\Local\JetBrains\GoLand2024.2\tmp\GoLand___go_build_testProject.exe . #gosetup
C:\Users\dan\AppData\Local\JetBrains\GoLand2024.2\tmp\GoLand___go_build_testProject.exe #gosetup
After Creation
    bOrig: {[11 12 13 14 15 16] [21 22 23 24 25 26]}
  bSecond: {[11 12 13 14 15 16] [21 22 23 24 25 26]}
   bThird: {[11 12 13 14 15 16] [21 22 23 24 25 26]}
   bFourth: {[11 12 13 14 15 16] [21 22 23 24 25 26]}

After bSecond.s[2] += 70 and bThird.s[4] += 80
    bOrig: {[11 12 83 14 95 16] [21 22 23 24 25 26]}
  bSecond: {[11 12 83 14 95 16] [21 22 23 24 25 26]}
   bThird: {[11 12 83 14 95 16] [21 22 23 24 25 26]}
   bFourth: {[11 12 13 14 15 16] [21 22 23 24 25 26]}

SIDE QUESTION:
   OK so I get it, in a structure with slices when copied with a
      simple = or := the slices still point to the same memory locations.
      Is that going to be true for primitive types or sub structures of non primitive types?

PRIMARY QUESTION:
   Below I pass a structure (bOrig) containing slices to a
      method that changes part of the slice it then returns nothing.
      Upon return the structure that was passed reflects the changes made to it in the method.
      My question is: given this behavior how can Go be called a language that uses
      CALL BY VALUE???

Am I missing something?

After bOrig.methodmessUpbType()
    bOrig: {[1011 12 83 14 95 16] [21 22 23 24 25 26]}
  bSecond: {[1011 12 83 14 95 16] [21 22 23 24 25 26]}
   bThird: {[1011 12 83 14 95 16] [21 22 23 24 25 26]}
   bFourth: {[11 12 13 14 15 16] [21 22 23 24 25 26]}

Process finished with the exit code 0

我的结论是,结构体和切片实际上是指针吗?或者这个解释过于简单了?但仍然是按值调用?

补充问题——我粘贴了代码和输出,但如你所见,它把列表弄乱了。我应该阅读哪里来学习如何正确地做到这一点???


更多关于Golang语言:我一直以为Go是按值传递的语言的实战教程也可以访问 https://www.itying.com/category-94-b0.html

11 回复

你可以尝试使用Markdown语法。我不确定是否完全支持,但至少对于编写示例代码来说很有用。

更多关于Golang语言:我一直以为Go是按值传递的语言的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


谢谢,我现在明白了——结构体和切片本质上是指针,被调用的是指针的值。

顺便问一下,你看到补充问题了吗?我粘贴了代码和输出,但如你所见,它把列表弄乱了。我应该阅读哪些资料来学习如何正确地做到这一点呢?

感谢您的澄清,这真的很有帮助。非常感谢。

那么,如果被复制的切片包含结构体,使用 copy(dst, src) 就足够了,除非(???)这些结构体本身内部也包含切片。

假设我的“除非”是成立的,那么我是否还需要复制这些第二级的切片呢?

您能推荐一些我在网上看到的(实际上是针对哪种情况的)深度复制或克隆函数库吗?

Discourse(本论坛软件)支持 Markdown。要将文本格式化为代码,请在代码前后使用三个反引号,如下所示:

<插入代码>

(推荐)要进行语法高亮,请在第一个反引号后指定语言,如下所示:

func main() {}

效果如下:

func main() {}

为了澄清这一点:切片是指针,映射和接口也是。但结构体不是指针。结构体是按值传递的,会复制结构体中的所有字段,即使它非常大。(请注意,Go 语言中的结构体很少会非常大,因为它必须有很多字段或包含具有很多字段的结构体。)

在你的例子中,你有一个包含结构体的切片。切片会被复制,但它本质上是一个指针,它所包含的结构体最终会像通过引用一样被传递。

“这是一份关于如何使用 Discourse 论坛/社区软件的‘快速’操作指南。它包含了面向用户/内容编辑者的大多数常用功能,但可能未能完全覆盖所有功能集。如果有人发现需要补充或更正的内容,欢迎评论。如果你想了解更多关于 Discourse 编辑器的语法,可以查看 commonmark(一种 Markdown 风格)文档以获取更多信息。” 我在…偶然发现了这篇关于 Discourse 帖子格式化的精彩用户参考指南。

你可以参考这个实现,它并不难。

package dcopy

import (
	"errors"
	"reflect"
	"time"
)

func CopySDT[T any](src T, dest *T) error {
	sv := reflect.ValueOf(src)
	dv := reflect.ValueOf(dest)
	if dv.Kind() != reflect.Pointer || dv.IsNil() {
		return errors.New("nil dest")
	}
	copyRecursive(sv, dv.Elem())
	return nil
}

func CopyT[T any](src T) T {
	return Copy(src).(T)

这个实现对于我个人使用来说已经足够好了。

是的,我理解关于 main.go 中切片的那部分。我在附带问题中已经承认了这一点,当时我说:“好的,我明白了,在包含切片的结构体中,使用简单的 =:= 进行复制时,切片仍然指向相同的内存位置。”

但是,如果你回到主要问题,我不理解的是:如果我通过调用一个使用切片或包含切片的结构体的函数,它如何能够改变调用代码中的值。

总而言之,似乎如果我想要将切片或包含切片的结构体传递给一个函数,我必须先制作一个副本,然后传递那个副本!

我感谢你的回复,但我的主要问题仍然是:“鉴于这种行为,Go 怎么能被称为使用按值调用的语言???”

这没什么难以理解的,按值传递,传递的就是值。当传递指针时,传递的是指针的值,而不是指针所指向的值。 让我们看一个示例代码,我不想多说了:

func main() {
	l := []int{1, 2, 3, 4}
	t := 1
	fmt.Printf("1 l %p %p %p\n", l, &l, &t) //1 l 0xc0000282c0 0xc000012b40 0xc0000147a0
	handle(l, t)
	fmt.Printf("4 l %p %p %p\n", l, &l, &t) //4 l 0xc0000282c0 0xc000012b40 0xc0000147a0
}

func handle(l []int, t int) {
	fmt.Printf("2 l %p %p %p\n", l, &l, &t) //2 l 0xc0000282c0 0xc000012b70 0xc0000147a8
	l = append(l, []int{5, 6, 7, 8, 9}...)
	t = 2
	fmt.Printf("3 l %p %p %p\n", l, &l, &t) //3 l 0xc000024460 0xc000012b70 0xc0000147a8
}

嗯,我不太清楚你是怎么学习 Go 语言的,但这是一个基础问题。基本上所有的 Go 语言书籍都会提到切片的概念(更准确地说,这是基础知识)。

本质上,你直接使用 = 赋值,只是分配了底层的指针。两者使用的是同一个指针,所以当其中任何一个被修改时,结果都会相同。

当你进行复制时,实际上是创建了一个相同长度的切片,然后进行赋值,这时两者的指针是不同的。

让我们看一下示例代码来了解发生了什么:

type v struct {
	a []int
	b []int
}

func main() {
	v1 := v{
		a: []int{1, 2, 3, 4},
		b: []int{5, 6, 7, 8},
	}
	fmt.Printf("v1 %p %p \n", v1.a, v1.b) //v1 0xc0000282c0 0xc0000282e0
	v2 := v1
	fmt.Printf("v2 %p %p \n", v2.a, v2.b) //v2 0xc0000282c0 0xc0000282e0
	v3 := v{a: make([]int, len(v1.a)), b: make([]int, len(v1.b))}
	copy(v3.a, v1.a)
	copy(v3.b, v1.b)
	fmt.Printf("v3 %p %p \n", v3.a, v3.b) //v3 0xc000028300 0xc000028320
	v1.a = append(v1.a, []int{9, 10, 11, 12, 13, 14, 15, 16}...)
	fmt.Printf("v1.a %p &v2.a %p \n", v1.a, v2.a) //v1.a 0xc00002a840 &v2.a 0xc0000282c0
}

如果你不理解基本的数据结构,后续的开发会遇到各种问题。

Go确实是按值传递的语言,但需要理解切片和结构体的内存模型。切片本身是一个包含三个字段的结构:指向底层数组的指针、长度和容量。当复制切片或包含切片的结构体时,复制的是这个描述符,而不是底层数组。

以下是关键点的代码示例:

package main

import "fmt"

type MyStruct struct {
    slice []int
    value int
}

func modifySlice(s []int) {
    // 修改底层数组
    s[0] = 100
}

func modifyStructByValue(ms MyStruct) {
    // 修改切片元素 - 会影响原结构体
    ms.slice[1] = 200
    
    // 修改值字段 - 不会影响原结构体
    ms.value = 300
}

func modifyStructPointer(ms *MyStruct) {
    // 修改切片元素
    ms.slice[2] = 400
    
    // 修改值字段
    ms.value = 500
}

func main() {
    // 原始结构体
    original := MyStruct{
        slice: []int{1, 2, 3, 4, 5},
        value: 10,
    }
    
    fmt.Printf("原始: %v, value: %d\n", original.slice, original.value)
    
    // 1. 直接传递切片
    modifySlice(original.slice)
    fmt.Printf("修改切片后: %v, value: %d\n", original.slice, original.value)
    
    // 2. 按值传递结构体
    modifyStructByValue(original)
    fmt.Printf("按值传递结构体后: %v, value: %d\n", original.slice, original.value)
    
    // 3. 按指针传递结构体
    modifyStructPointer(&original)
    fmt.Printf("按指针传递结构体后: %v, value: %d\n", original.slice, original.value)
    
    // 4. 深度复制示例
    deepCopy := MyStruct{
        slice: make([]int, len(original.slice)),
        value: original.value,
    }
    copy(deepCopy.slice, original.slice)
    
    deepCopy.slice[0] = 999
    fmt.Printf("深度复制修改后 - 原结构体: %v, 复制结构体: %v\n", 
        original.slice, deepCopy.slice)
}

输出:

原始: [1 2 3 4 5], value: 10
修改切片后: [100 2 3 4 5], value: 10
按值传递结构体后: [100 200 3 4 5], value: 10
按指针传递结构体后: [100 200 400 4 5], value: 500
深度复制修改后 - 原结构体: [100 200 400 4 5], 复制结构体: [999 200 400 4 5]

对于你的补充问题,当结构体包含引用类型(切片、映射、通道、函数、指针)时,简单的赋值操作只会复制引用。对于基本类型(int、float、bool、string、数组)和包含基本类型的子结构体,会进行完整的值复制。

type SubStruct struct {
    num int
}

type Container struct {
    slice    []int      // 引用类型
    array    [3]int     // 值类型(数组)
    pointer  *int       // 引用类型
    value    int        // 值类型
    sub      SubStruct  // 值类型
}

func demonstrateCopy() {
    x := 42
    c1 := Container{
        slice:   []int{1, 2, 3},
        array:   [3]int{4, 5, 6},
        pointer: &x,
        value:   7,
        sub:     SubStruct{num: 8},
    }
    
    c2 := c1  // 浅复制
    
    // 修改c2的切片会影响c1
    c2.slice[0] = 100
    
    // 修改c2的数组不会影响c1
    c2.array[0] = 400
    
    // 通过指针修改会影响c1
    *c2.pointer = 4200
    
    // 修改值字段不会影响c1
    c2.value = 700
    c2.sub.num = 800
    
    fmt.Printf("c1: slice=%v, array=%v, *pointer=%d, value=%d, sub.num=%d\n",
        c1.slice, c1.array, *c1.pointer, c1.value, c1.sub.num)
    fmt.Printf("c2: slice=%v, array=%v, *pointer=%d, value=%d, sub.num=%d\n",
        c2.slice, c2.array, *c2.pointer, c2.value, c2.sub.num)
}

输出:

c1: slice=[100 2 3], array=[4 5 6], *pointer=4200, value=7, sub.num=8
c2: slice=[100 2 3], array=[400 5 6], *pointer=4200, value=700, sub.num=800

Go的按值传递意味着函数参数总是获得参数的副本,但对于引用类型,这个副本指向相同的内存位置。要获得真正的独立副本,需要使用makecopy进行深度复制,或者使用第三方库如go-clone

回到顶部