이 기사 GitHub github.com/eddycjy/blog가 포함되었습니다
안녕하세요 여러분, 저는 Jianyu입니다.
얼마 전 "Shandou Go 인터뷰어: Go 구조를 비교할 수 있나요? 왜?"에 대해 공유한 적이 있습니다. 》 기사에서는 기본 Go 구조체의 비교 기초를 연구했습니다. 아니요, 최근 한 독자가 구조체에 대해 해결할 수 없는 새로운 문제에 직면했습니다.
아래를 읽기 전에 코드 예시를 보고 답을 생각해 보는 것이 좋습니다.
독립적인 사고가 중요합니다.
혼란의 예
주어진 예는 다음과 같습니다.
type People struct {} func main() { a := &People{} b := &People{} fmt.Println(a == b) }
출력 결과는 무엇이라고 생각하시나요?
출력 결과는 false입니다.
조금 더 수정하면 예제 2는 다음과 같습니다.
type People struct {} func main() { a := &People{} b := &People{} fmt.Printf("%p\n", a) fmt.Printf("%p\n", b) fmt.Println(a == b) }
출력 결과는 true입니다.
그의 질문은 "왜 첫 번째는 false를 반환하고 두 번째는 true를 반환합니까? 이유는 무엇입니까?
Fried Fish는 이 예를 더욱 간소화하고 최소 예를 얻습니다.func main() { a := new(struct{}) b := new(struct{}) println(a, b, a == b) c := new(struct{}) d := new(struct{}) fmt.Println(c, d) println(c, d, c == d) }
// a, b; a == b 0xc00005cf57 0xc00005cf57 false // c, d &{} &{} // c, d, c == d 0x118c370 0x118c370 true
fmt.Print
방식인 것 같은데 표준 라이브러리의 출력 방식이 이렇게 이상한 문제를 일으킬까요? fmt.Print
方法导致的,但一个标准库里的输出方法,会导致这种奇怪的问题?
问题剖析
如果之前有被这个 “坑” 过,或有看过源码的同学。可能能够快速的意识到,导致这个输出是逃逸分析所致的结果。
我们对例子进行逃逸分析:
// 源代码结构 $ cat -n main.go 5 func main() { 6 a := new(struct{}) 7 b := new(struct{}) 8 println(a, b, a == b) 9 10 c := new(struct{}) 11 d := new(struct{}) 12 fmt.Println(c, d) 13 println(c, d, c == d) 14 } // 进行逃逸分析 $ go run -gcflags="-m -l" main.go # command-line-arguments ./main.go:6:10: a does not escape ./main.go:7:10: b does not escape ./main.go:10:10: c escapes to heap ./main.go:11:10: d escapes to heap ./main.go:12:13: ... argument does not escape
通过分析可得知变量 a, b 均是分配在栈中,而变量 c, d 分配在堆中。
其关键原因是因为调用了 fmt.Println
方法,该方法内部是涉及到大量的反射相关方法的调用,会造成逃逸行为,也就是分配到堆上。
为什么逃逸后相等
关注第一个细节,就是 “为什么逃逸后,两个空 struct 会是相等的?”。
这里主要与 Go runtime 的一个优化细节有关,如下:
// runtime/malloc.go var zerobase uintptr
变量 zerobase
是所有 0 字节分配的基础地址。更进一步来讲,就是空(0字节)的在进行了逃逸分析后,往堆分配的都会指向 zerobase
这一个地址。
所以空 struct 在逃逸后本质上指向了 zerobase
문제 분석
이전에 이것에 "갇힌" 적이 있거나 본 적이 있습니까? 소스 코드를 읽은 학생들은 이 출력이
탈출 분석예제에 대한 탈출 분석을 수행합니다. $ go run -gcflags="-N -l" main.go
0xc000092f06 0xc000092f06 true
&{} &{}
0x118c370 0x118c370 true
fmt.Println
메서드가 호출되기 때문입니다. 이스케이프 동작, 즉 힙에 대한 할당을 유발하는 다수의 리플렉션 관련 메서드 호출 이스케이프 후 왜 동일한가요? 이것은 주로 다음과 같이 Go 런타임의 최적화 세부 사항과 관련됩니다.첫 번째 세부 사항인 "왜 두 개의 빈 구조체가 다음과 같은가요?"에 집중하세요. 탈출?" ”.
rrreee
변수zerobase
는 모든 0바이트 할당의 기본 주소입니다(0). bytes) 이스케이프 분석을 수행한 후 힙에 대한 모든 할당은 zerobase
주소를 가리킵니다. 이스케이프하지 않으면 왜 동일하지 않습니까? 두 번째 세부 사항인 "이스케이프하기 전에 두 개의 빈 구조체가 동일하지 않은 이유는 무엇입니까?"에 집중하세요. ”.그래서 빈 구조체는 본질적으로 이스케이프 후zerobase
를 가리킵니다.
Go 사양에서 이는 Go 팀의 의도적인 설계이며 모든 사람이 이를 판단의 근거로 삼는 것을 원하지 않습니다.
이것은 다음과 같습니다. 크기가 0인 객체에 대한 포인터를 처리하는 방법에 있어 구현 유연성을 제공하기 위한 의도적인 언어 선택입니다. . 크기가 0인 객체에 대한 모든 포인터가 동일해야 한다면 더 큰 구조체 내에서 크기가 0인 필드의 주소를 처리하는 것이 다를 것입니다.또한 매우 고전적이고 훌륭한 제품이 말했습니다.
별도의 0 크기 변수에 대한 포인터는 같을 수도 있고 같지 않을 수도 있습니다.또한 빈 구조체의 실제 사용 시나리오는 비교적 드뭅니다.
컨텍스트를 설정하고 전달할 때 사용됩니다. 🎜🎜 빈 구조체를 설정하는 것은 비즈니스 시나리오에서 일시적으로 사용됩니다. 🎜🎜🎜그러나 대부분의 비즈니스 시나리오는 비즈니스 개발에 따라 계속해서 변경됩니다. 🎜🎜직접적으로 신뢰할 수는 없습니다🎜🎜그래서 Go 팀의 조작은 사람들의 무작위성을 피하는 것과 똑같습니다. 이런 종류의 논리에 직접 의존 🎜🎜두 개의 빈 구조체의 비교 작업인 시나리오에서는 실제로 코드 최적화 단계에서 직접 최적화되어 변환되었습니다. 따라서 코드에서는 ==가 비교하는 것처럼 보이지만 실제로는 a == b일 때 바로 false로 변환되므로 비교할 필요가 없습니다. 🎜🎜멋지지 않나요? 🎜🎜동등하게 만드는 탈출구는 없습니다🎜🎜이제 코드 최적화 단계에서 최적화되었음을 알았으므로, 반면에 원리를 알면 go 컴파일 중에 gcflags 명령어를 사용할 수도 있습니다. 최적화를 방지하기 위해 런타임을 사용합니다. 🎜在运行前面的例子时,执行 -gcflags="-N -l"
指令:
$ go run -gcflags="-N -l" main.go 0xc000092f06 0xc000092f06 true &{} &{} 0x118c370 0x118c370 true
你看,两个比较的结果都是 true 了。
总结
在今天这篇文章中,我们针对 Go 语言中的空结构体(struct)的比较场景进行了进一步的补全。经过这两篇文章的洗礼,你会更好的理解 Go 结构体为什么叫既可比较又不可比较了。
而空结构比较的奇妙,主要原因如下:
- 若逃逸到堆上,空结构体则默认分配的是
runtime.zerobase
变量,是专门用于分配到堆上的 0 字节基础地址。因此两个空结构体,都是runtime.zerobase
,一比较当然就是 true 了。 - 若没有发生逃逸,也就分配到栈上。在 Go 编译器的代码优化阶段,会对其进行优化,直接返回 false。并不是传统意义上的,真的去比较了。
不会有人拿来出面试题,不会吧,为什么 Go 结构体说可比较又不可比较?
若有任何疑问欢迎评论区反馈和交流,最好的关系是互相成就,各位的点赞就是煎鱼创作的最大动力,感谢支持。
文章持续更新,可以微信搜【脑子进煎鱼了】阅读,回复【000】有我准备的一线大厂面试算法题解和资料;本文 GitHub github.com/eddycjy/blog 已收录,欢迎 Star 催更。