Home  >  Article  >  Backend Development  >  Look! A stupid mistake you can't make when using Go struct!

Look! A stupid mistake you can't make when using Go struct!

藏色散人
藏色散人forward
2021-06-19 15:53:081761browse

The following tutorial column from golang will introduce to you a low-level mistake that cannot be made when using Go struct. I hope it will be helpful to friends in need!

A stupid mistake you can’t make when using Go struct!

This article GitHub github.com/eddycjy/blog has been included

Hello everyone, I am Jianyu.

Some time ago I shared "Shandou Go Interviewer: Can Go structures be compared? Why?" 》 article, studied the comparison basis of basic Go struct. No, recently a reader encountered a new problem about struct that he couldn't solve.

Let’s take a look together. It is recommended that you think about the answer after seeing the code example, and then read on.

It is important to think independently.

Doubtful example

The example given is as follows:

type People struct {}

func main() {
 a := &People{}
 b := &People{}
 fmt.Println(a == b)
}

What do you think the output result is?

The output result is: false.

With a slight modification, Example 2 is as follows:

type People struct {}

func main() {
 a := &People{}
 b := &People{}
 fmt.Printf("%p\n", a)
 fmt.Printf("%p\n", b)
 fmt.Println(a == b)
}

The output result is: true.

His question is "Why does the first one return false and the second one return true? What's the reason?

Further simplify this example and get Minimal example:

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)
}
Output result:

// a, b; a == b
0xc00005cf57 0xc00005cf57 false

// c, d
&{} &{}
// c, d, c == d
0x118c370 0x118c370 true
The result of the first paragraph of code is false, the result of the second paragraph is true, and you can see that the memory address points to exactly the same, that is The cause of the change in variable memory pointer after output is ruled out.

Looking further, it seems to be caused by the

fmt.Print method, but an output method in the standard library will cause this Strange problem?

Problem Analysis

If you have been "trapped" by this before, or have read the source code, you may be able to quickly realize that the output is

The result of escape analysis.

We perform escape analysis on the example:

// 源代码结构
$ 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
Through the analysis, we can know that variables a and b are allocated on the stack, and variable c, d is allocated in the heap.

The key reason is that the

fmt.Println method is called, which involves a large number of calls to reflection-related methods, which will cause escape behavior and also It is allocated on the heap.

Why are two empty structs equal after escaping?

Focus on the first detail, which is "Why are two empty structs equal after escaping?" ".

This is mainly related to an optimization detail of Go runtime, as follows:

// runtime/malloc.go
var zerobase uintptr
Variable

zerobase is the base address of all 0-byte allocations. Going further , which is empty (0 bytes). After escape analysis, everything allocated to the heap will point to the zerobase address.

So the empty struct essentially points to # after escape. ##zerobase

, the two are equal when compared, and true is returned. Why they are not equal without escape

Focus on the second detail, which is "Why before escape, the two Are empty structs not equal? ”.

Go specFrom the Go spec, this is a deliberate design by the Go team. We don’t want everyone to rely on this as a basis for judgment. .As follows:

This is an intentional language choice to give implementations flexibility in how they handle pointers to zero-sized objects. If every pointer to a zero-sized object were required to be different, then each allocation of a zero-sized object would have to allocate at least one byte. If every pointer to a zero-sized object were required to be the same, it would be different to handle taking the address of a zero-sized field within a larger struct.
also said a very classic saying:

Pointers to distinct zero-size variables may or may not be equal.
In addition, empty struct is rarely used in actual use scenarios. The common ones are:

Set context, which is used as a key when passing.
  • Set empty struct is temporarily used in business scenarios.
  • But in business scenarios, most of them will continue to change with the development of the business. Suppose there is a Go code from ancient times that relies on empty struct Wouldn't it be an accident to judge directly?

Cannot be relied upon directly

Therefore, the Go team's operation is exactly the same as the randomness of Go map. It is worthwhile to avoid everyone's direct reliance on this kind of logic. Thinking.

In the scenario where there is no escape, the comparison action of two empty structs, you think it is really comparing. In fact, it has been directly optimized during the code optimization stage and turned to false.

Therefore, although it looks like == is being compared in the code, in fact when the result is a == b, it is directly converted to false, and there is no need to compare.

Don’t you think it’s wonderful?

No escape to make it equal

Now that we know that it is optimized during the code optimization phase, then on the other hand, if we know the principle, we can also use gcflags during go compilation and runtime. Instructions to prevent him from optimizing.

在运行前面的例子时,执行 -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 催更。

The above is the detailed content of Look! A stupid mistake you can't make when using Go struct!. For more information, please follow other related articles on the PHP Chinese website!

Statement:
This article is reproduced at:segmentfault.com. If there is any infringement, please contact admin@php.cn delete