结构体的值传递

栏目: 编程语言 · 发布时间: 5年前

内容简介:前天在修改服务器端 Go 代码的时候因为一个很基本的问题纠结了好一会儿,今天还和同事认真地探讨了一番,结果是因为太长时间都在玩儿 JavaScript,连基本的值传递与引用传递都搞混了…业务逻辑中有一个结构体某个特定的

前天在修改服务器端 Go 代码的时候因为一个很基本的问题纠结了好一会儿,今天还和同事认真地探讨了一番,结果是因为太长时间都在玩儿 JavaScript,连基本的值传递与引用传递都搞混了…

业务逻辑中有一个结构体 Item ,它有一个 Options 的字段,其具体结构依赖于 ItemType 字段,所以 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.OptionsLabel 属性并没有发生任何变化,那行对 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) 起始位置!


以上所述就是小编给大家介绍的《结构体的值传递》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

PHP从入门到精通

PHP从入门到精通

邹天思、孙鹏 / 清华大学出版社 / 2008-10-1 / 68.00元

DVD语音视频教学光盘,22小时教学视频录像,全程语音讲解,本书实例源程序、相关素材,本书特色:基础知识—核心技术—高级应用—项目实战,268个应用实例,41个典型应用,1个项目案例,内容极为详尽,实例典型丰富。一起来看看 《PHP从入门到精通》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具