内容简介:前天在修改服务器端 Go 代码的时候因为一个很基本的问题纠结了好一会儿,今天还和同事认真地探讨了一番,结果是因为太长时间都在玩儿 JavaScript,连基本的值传递与引用传递都搞混了…业务逻辑中有一个结构体某个特定的
前天在修改服务器端 Go 代码的时候因为一个很基本的问题纠结了好一会儿,今天还和同事认真地探讨了一番,结果是因为太长时间都在玩儿 JavaScript,连基本的值传递与引用传递都搞混了…
业务逻辑中有一个结构体 Item
,它有一个 Options
的字段,其具体结构依赖于 Item
的 Type
字段,所以 Item
的定义如下:
type Item struct { Id string Type string Options interface{} }
某个特定的 Options
结构可能是:
type QuestionOptions struct { Id string Label string }
为了可以对 item.Options
进行具体的操作,需要先强制类型转化
options, _ := item.Options.(QuestionOptions) options.Label = "Hello"
简单到不能再简单的操作,一眼看过去妥妥的,但是结果完全不是想要的,实际情况是 item.Options
的 Label
属性并没有发生任何变化,那行对 Label
的赋值语句完全没有起作用。
第一时间想到的是难道强制类型转化的同时还会进行数据拷贝?立马再补上一句赋值:
options, _ := item.Options.(QuestionOptions) options.Label = "Hello" item.Options = options
好了,搞了定… 但是完全不能接受呀,思来想去,强制类型转化的同时还进行数据拷贝完全没道理呀,不仅浪费内存还违背常理,难道这是 Go 的“feature”?
今天一大早和同事认真讨论了一番,从变量可能的实际存储结构到内存布局,最后还拿到Go编译的汇编代码仔细瞅了瞅才恍然大悟,根本就是把值传递的事儿给抛之脑后了…
在 c/c++ 及 Go 这些语言中结构体之间的赋值都是值传递,而且普通情况下结构体都是在栈上创建的,也就是说每一次结构体变量之间的赋值都会在栈上进行数据拷贝:
opt1 := QuestionOptions{Id: "1", Label: "1"} opt2 := opt1 opt2.Id = "2"
结果是 opt1.Id != opt2.Id
,更进一步 &opt2 != &opt1
而 JavaScript 中并没有结构体,相近的只有对象字面量(Object literals) {}
,由于对象始终都是创建在堆上,变量存储的只是堆地址,所以在变量赋值时拷贝的仅仅是地址
const opt1 = { id: '1', label: '1' }; const opt2 = opt1; opt2.id = '2';
所以 opt1.id === opt2.id
并且 opt1 === opt2
,但是如果 JavaScript 支持取地址操作的话 &opt1 !== &opt2
到此都还很清晰,这些基本知识也还没有还给老师,不过加上一个强制类型转化就昏头了…
首先强制类型转换 item.Options.(QuestionOptions)
返回了一个结构体变量,但并没有进行数据拷贝,其数据还是指向同一个内存地址。然后 options, _ :=
进行了一次结构体变量之间的赋值,这才是真正触发拷贝的地方!
后记
后来又仔细看了下 Go 汇编,想找到具体的赋值语句,测试代码很简单:
var p interface{} = Option{Label: "1"} opt := p.(Option) opt.Label = "2"
对应的 Go 汇编代码是
0x0000 00000 (convert_type.go:11) TEXT "".main(SB), $104-0 0x0000 00000 (convert_type.go:11) MOVQ (TLS), CX 0x0009 00009 (convert_type.go:11) CMPQ SP, 16(CX) 0x000d 00013 (convert_type.go:11) JLS 265 0x0013 00019 (convert_type.go:11) SUBQ $104, SP 0x0017 00023 (convert_type.go:11) MOVQ BP, 96(SP) 0x001c 00028 (convert_type.go:11) LEAQ 96(SP), BP 0x0021 00033 (convert_type.go:12) MOVQ $0, ""..autotmp_2+88(SP) 0x002a 00042 (convert_type.go:12) LEAQ go.string."1"(SB), AX 0x0031 00049 (convert_type.go:12) MOVQ AX, ""..autotmp_2+80(SP) 0x0036 00054 (convert_type.go:12) MOVQ $1, ""..autotmp_2+88(SP) 0x003f 00063 (convert_type.go:12) LEAQ type."".Option(SB), AX 0x0046 00070 (convert_type.go:12) MOVQ AX, (SP) 0x004a 00074 (convert_type.go:12) LEAQ ""..autotmp_2+80(SP), CX 0x004f 00079 (convert_type.go:12) MOVQ CX, 8(SP) 0x0054 00084 (convert_type.go:12) CALL runtime.convT2E(SB) 0x0059 00089 (convert_type.go:12) MOVQ 24(SP), AX 0x005e 00094 (convert_type.go:12) MOVQ 16(SP), CX 0x0063 00099 (convert_type.go:13) LEAQ type."".Option(SB), DX 0x006a 00106 (convert_type.go:13) CMPQ CX, DX 0x006d 00109 (convert_type.go:13) JNE 237 0x006f 00111 (convert_type.go:13) MOVQ 8(AX), AX 0x0073 00115 (convert_type.go:13) MOVQ AX, "".opt+56(SP) 0x0078 00120 (convert_type.go:14) LEAQ go.string."2"(SB), AX 0x007f 00127 (convert_type.go:14) MOVQ AX, "".opt+48(SP) 0x0084 00132 (convert_type.go:14) MOVQ $1, "".opt+56(SP)
其中第二行(实际文件中的第13行)强制类型转换对应的汇编代码是
0x0063 00099 (convert_type.go:13) LEAQ type."".Option(SB), DX 0x006a 00106 (convert_type.go:13) CMPQ CX, DX 0x006d 00109 (convert_type.go:13) JNE 237 0x006f 00111 (convert_type.go:13) MOVQ 8(AX), AX 0x0073 00115 (convert_type.go:13) MOVQ AX, "".opt+56(SP)
这里的 AX
存放的是第一行初始化的结构体的地址,根据上下文可以看出 8(AX)
并不指指向 Label
这个字段( AX
才是),所以这一行实际并没有拷贝 Label
字段的值… 本来都满心感觉搞定了的问题又找不到北了…
不过故事总是有结果的,实际情况是在这段代码中,期待的结构体拷贝代码是没有必要的,因为在完成结构体强制类型转换后立马对其进行了 Label
的重新赋值,所以编译器优化了这个过程,在类型转换那行只创建了一个新的结构体,省略了重复而且不必要的结构体内容拷贝。可以通过 关掉编译优化的选项
,从原始的汇编代码中找到对应的拷贝操作,或者去掉那句 Label
的赋值语句,然后就能看到:
0x0063 00099 (convert_type.go:13) LEAQ type."".Option(SB), DX 0x006a 00106 (convert_type.go:13) CMPQ CX, DX 0x006d 00109 (convert_type.go:13) JNE 224 0x006f 00111 (convert_type.go:13) MOVQ (AX), CX 0x0072 00114 (convert_type.go:13) MOVQ 8(AX), AX 0x0076 00118 (convert_type.go:13) MOVQ CX, "".opt+48(SP) 0x007b 00123 (convert_type.go:13) MOVQ AX, "".opt+56(SP)
Label
字段的值通过 CX
复制到新结构体 "".opt+48(SP)
起始位置!
以上所述就是小编给大家介绍的《结构体的值传递》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- P/Invoke以指针形式传递结构体内的数组
- C++ 值传递、指针传递、引用传递详解
- 简明笔记:指针传递和值传递
- golang中的函数参数值传递和引用传递
- 现代编程语言的值传递与引用传递
- 这一次,彻底解决Java的值传递和引用传递
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
图片转BASE64编码
在线图片转Base64编码工具
UNIX 时间戳转换
UNIX 时间戳转换