结构体的值传递

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

内容简介:前天在修改服务器端 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) 起始位置!


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

查看所有标签

猜你喜欢:

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

An Introduction to Genetic Algorithms

An Introduction to Genetic Algorithms

Melanie Mitchell / MIT Press / 1998-2-6 / USD 45.00

Genetic algorithms have been used in science and engineering as adaptive algorithms for solving practical problems and as computational models of natural evolutionary systems. This brief, accessible i......一起来看看 《An Introduction to Genetic Algorithms》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

在线进制转换器
在线进制转换器

各进制数互转换器

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

HSV CMYK互换工具