A reader recently encountered a new problem with struct that he could not solve.

Examples of Doubt

The example 1 it gives is as follows.

1
2
3
4
5
6
7
type People struct {}

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

What do you think the output will be?

The output is: false.

With a little more modification, Example 2 is as follows.

1
2
3
4
5
6
7
8
9
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 is: true.

His question is “Why does the first return false and the second return true, and what causes it?

Further refinement of this example yields the following minimal example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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 results.

1
2
3
4
5
6
7
// a, b; a == b
0xc00005cf57 0xc00005cf57 false

// c, d
&{} &{}
// c, d, c == d
0x118c370 0x118c370 true

The result of the first code is false and the second one is true, and you can see that the memory address points to exactly the same, which means that the change in the memory pointing of the variable after the output is ruled out as the cause.

Looking further, it seems to be caused by the fmt.Print method, but an output method in a standard library that causes this strange problem?

Problem Analysis

Developers who have dealt with this problem before, or have seen the source code. You may be able to quickly realize that the output leading to this is the result of escape analysis.

Let’s perform an escape analysis on the example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// Source code structure
$ 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	}

// Perform escape analysis
$ 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

The analysis shows that variables a and b are allocated on the stack, while variables c and d are allocated on the heap.

The key reason for this is that the fmt.Println method is called, which involves a large number of reflection-related method calls inside the method, causing escape behavior, i.e., allocation to the heap.

Why comparing results after an escape is equal

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

Here it is mainly related to an optimization detail of Go runtime, as follows.

1
2
// runtime/malloc.go
var zerobase uintptr

The variable zerobase is the base address for all 0-byte allocations. Further, the empty (0-byte) ones will point to the zerobase address after the escape analysis.

So the empty struct essentially points to zerobase after the escape, and the comparison between the two is equal and returns true.

Why the comparison results are not equal in the case of non-escapement

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

From the Go spec, this is a deliberate design by the Go team, which does not want people to rely on this one 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.

And also said a very classic words

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

Also the scenarios in which empty structs are used in practice are relatively rare and common.

  • Set the context, which is used when passing as key.
  • Set empty struct for temporary use in business scenarios.

But most of the business scenarios will also change as the business evolves. Suppose there is a long-ago Go code that relies on the direct judgment of an empty struct, wouldn’t it be an accident?

Not directly dependent

So this action by the Go team is worth thinking about, as is the randomness of the Go map, to avoid everyone’s direct reliance on this kind of logic.

In the scenario where there is no escape, the comparison action of two empty structs that you think are really being compared. In fact it has been directly optimized away to false in the code optimization phase.

So, although it looks like == is doing the comparison in the code, the result is actually a == b when it turns out to be false, and there is no need to compare.

Don’t you think that’s great?

did not escape making him equal

现在我们知道他在代码优化阶段被优化了,我们也可以在go编译运行时借助于gcflags指令使他不被优化。

在运行前面的例子时,执行-gcflags="-N -l"指令。

1
2
3
4
$ go run -gcflags="-N -l" main.go 
0xc000092f06 0xc000092f06 true
&{} &{}
0x118c370 0x118c370 true

As you can see, the result of both comparisons is now true.

Summary

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

And the main reason for the wonders of empty structure comparison is as follows.

  • If escaping to the heap, the empty struct is allocated by default with the runtime.zerobase variable, a 0-byte base address specifically for allocation to the heap. So two empty structs, both runtime.zeroobase, will be true when compared.
  • If no escape occurs, they are also allocated to the stack. In the code optimization phase of the Go compiler, this is optimized to return false directly, not in the traditional sense of actually comparing.

Reference https://eddycjy.com/posts/go/go-empty-struct/