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
你可以尝试使用Markdown语法。我不确定是否完全支持,但至少对于编写示例代码来说很有用。
更多关于Golang语言:我一直以为Go是按值传递的语言的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
谢谢,我现在明白了——结构体和切片本质上是指针,被调用的是指针的值。
顺便问一下,你看到补充问题了吗?我粘贴了代码和输出,但如你所见,它把列表弄乱了。我应该阅读哪些资料来学习如何正确地做到这一点呢?
感谢您的澄清,这真的很有帮助。非常感谢。
那么,如果被复制的切片包含结构体,使用 copy(dst, src) 就足够了,除非(???)这些结构体本身内部也包含切片。
假设我的“除非”是成立的,那么我是否还需要复制这些第二级的切片呢?
您能推荐一些我在网上看到的(实际上是针对哪种情况的)深度复制或克隆函数库吗?
Discourse(本论坛软件)支持 Markdown。要将文本格式化为代码,请在代码前后使用三个反引号,如下所示:
<插入代码>
(推荐)要进行语法高亮,请在第一个反引号后指定语言,如下所示:
func main() {}
效果如下:
func main() {}
为了澄清这一点:切片是指针,映射和接口也是。但结构体不是指针。结构体是按值传递的,会复制结构体中的所有字段,即使它非常大。(请注意,Go 语言中的结构体很少会非常大,因为它必须有很多字段或包含具有很多字段的结构体。)
在你的例子中,你有一个包含结构体的切片。切片会被复制,但它本质上是一个指针,它所包含的结构体最终会像通过引用一样被传递。
你可以参考这个实现,它并不难。
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的按值传递意味着函数参数总是获得参数的副本,但对于引用类型,这个副本指向相同的内存位置。要获得真正的独立副本,需要使用make和copy进行深度复制,或者使用第三方库如go-clone。


