Wechat search [brain into fried fish] follow this fried fish with fried liver. In this paper, making github.com/eddycjy/blo… Already included, there are my series of articles, materials and open source Go books.

Hello, I’m fried fish.

Some time ago I shared a post titled “Hand Rip Go Interviewers: Are the Go structures comparable and why?” In this paper, the comparison basis of basic Go struct is studied. A reader recently encountered a new problem with structs that he couldn’t solve.

Let’s take a look. I encourage you to think about the answer after you see the code examples before you move on.

Independent thinking is important.

Examples of confusion

The first example it gives is as follows:

type People struct {}

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

What do you think the output is?

The output is: false.

With a little more modification, example 2 is as follows:

type People struct {}

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

The output is: true.

The question is “Why does the first one return false and the second one return true? What is the reason?

Fish fry further simplifies this example to get the smallest example:

func main(a) {
	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)
}
Copy the code

Output result:

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

// c, d
&{} &{}
// c, d, c == d
0x118c370 0x118c370 true
Copy the code

The result of the first paragraph is false, and the result of the second paragraph is true, and you can see that the memory address points exactly the same, that is, the reason why the variable memory point changes after the output is excluded.

Further, it seems to be the FMT.Print method, but an output method in a standard library can cause this strange problem?

The problem analysis

If you have been this “pit” before, or have seen the source code of students. It may be quickly realized that the output is the result of escape analysis.

Let’s do escape analysis for 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 escapeCopy the code

Through analysis, it can be known that variables A and B are allocated in the stack, while variables C and D are allocated in the heap.

The key reason for this is the call to the fmt.println method, which internally involves a large number of calls to reflection-related methods, causing escape behavior, i.e., allocation to the heap.

Why is it equal after escape

Focus on the first detail, which is “Why are two empty structs equal after escape?” .

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

// runtime/malloc.go
var zerobase uintptr
Copy the code

The variable Zerobase is the base address for all 0 byte allocations. Further, empty (0 bytes) addresses are allocated to the heap at zerobase after escape analysis.

So an empty struct essentially points to a Zerobase after escaping, and the comparison is equal, returning true.

Why isn’t escape unequal

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

According to the Go Spec, this is a deliberate design by the Go team. We don’t want people to rely on this one 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, fine product:

Pointers to distinct zero-size variables may or may not be equal.

In addition, there are few scenarios of empty struct in actual use. The common ones are:

  • Set the context to be passed as a key.
  • Set an empty struct for temporary use in business scenarios.

But in the case of business scenarios, most of them will constantly change with the development of business. Suppose there is an ancient Go code that relies on the direct judgment of empty struct, isn’t it an accident?

Not directly dependent

Therefore, the Go team’s operation is the same as the randomness of Go Map, so it is worth thinking about avoiding people’s direct dependence on this kind of logic.

In the scenario where there is no escape, the comparison between two empty structs, you think, is real. It’s actually been optimized out of the code optimization phase and changed to false.

So, even though in code it looks like == is doing the comparison, in fact the result is a == b and the comparison is changed to false.

What do you say?

No escape makes him equal

Now that we know that it was optimized during the code optimization phase, we can also use the GCFlags command at go compilation runtime to make it not optimized.

To run the previous example, execute the -gcflags=” -n -l” command:

$ go run -gcflags="-N -l" main.go 
0xc000092f06 0xc000092f06 true
&{} &{}
0x118c370 0x118c370 true
Copy the code

You see, the result of both comparisons is true.

conclusion

In today’s article, we further complete the comparison scenario of empty structures (structs) in Go. After these two articles, you will have a better understanding of why the Go structure is called both comparable and non-comparable.

The main reasons for the strangeness of empty structure comparison are as follows:

  • If it escapes onto the heap, the empty structure is allocated by defaultruntime.zerobaseVariable, which is a 0 byte base address specifically assigned to the heap. So two empty structures, both of themruntime.zerobaseTrue true true true true
  • If no escape occurs, it is also allocated to the stack. During the code optimization phase of the Go compiler, it is optimized and returns false directly. Not in the traditional sense, really.

No one’s going to come up with the questions, no, why does the Go structure say comparable and not comparable?

If you have any questions, welcome feedback and communication in the comments section. The best relationship is mutual achievement. Your praise is the biggest motivation for the creation of fried fish, thank you for your support.

This article is constantly updated. You can search “Brain into fried fish” on wechat to read it. Reply [000] There are the answers and materials for the interview algorithm of first-line big factories that I prepared. In this paper, making github.com/eddycjy/blo… Star has been included, welcome to urge more.

reference

  • Ou Shen’s wechat communication
  • “Pit” of an empty struct