2
search [160d2bfdc4128a brain into the fried fish ] follow this fried fish with liver-fried liver. This article GitHub github.com/eddycjy/blog has been included, and there are my series of articles, materials and open source Go books.

Hello everyone, I am fried fish.

Some time ago I shared the "Hand Ripped Go Interviewer: Are Go Structures Comparable? Why? "The article researched the basis of comparison of the basic Go struct. No, a reader recently encountered a new problem about struct and was puzzled.

Let's take a look. It is recommended that you think about the answer after seeing the code example, and then look down.

Independent thinking is important.

Examples of doubts

The example one given is as follows:

type People struct {}

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

What do you think is the output?

The output result is: false.

With a little modification, the second example 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 " return false in the first one and true in the second one?

Fried fish further streamlined this example to get the smallest 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, and 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 variable memory point change after the output is excluded.

Looking further, it seems to be fmt.Print method, but an output method in the standard library can cause this strange problem?

Problem analysis

If you have been "pitted" before, or have students who have read the source code. You may be able to quickly realize that this output is the result 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 analysis, we can know that variables a and b are allocated on the stack, while variables c and d are allocated on the heap.

The key reason is that the fmt.Println method is called, which involves a large number of reflection-related method calls, which will cause escape behavior, which is allocated to the heap.

Why are equal after escaping

Pay attention to the first detail, which is "Why are two empty structs equal after escaping?".

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

// runtime/malloc.go
var zerobase uintptr

The variable zerobase is the base address allocated for all 0 bytes. Furthermore, after the escape analysis is performed on the empty (0 bytes), all allocated to the heap will point to the address zerobase

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

Why not escape is not equal

Pay attention to the second detail, that is, "Why are two empty structs not equal before escaping?".

Go spec

From the Go spec, this is a deliberate design by the Go team, and I don't want everyone 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.

I also said a very classic, detailed product:

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

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

  • Set the context and use it as the key when passing it.
  • Set up an empty struct for temporary use in business scenarios.

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

Not directly dependent

Therefore, this operation of the Go team is exactly the same as the randomness of the Go map. It is worth thinking about avoiding everyone's direct dependence on this kind of logic.

And in the scene where there is no escape, you think that the comparison actions of two empty structs are really comparing. In fact, it has been directly optimized in the code optimization stage and turned to false.

Therefore, although it seems that == is doing comparison in the code, in fact, when the result is a == b, it is directly converted to false, and the comparison is unnecessary.

Are you wonderful?

No escape makes him equal

Now that we know that it is optimized during the code optimization stage, relatively, if we know the principle, we can also use the gcflags instruction in the go compilation and runtime to make it not optimized.

When running the previous example, execute the -gcflags="-N -l" command:

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

You see, the results of the two comparisons are both true.

to sum up

In today's article, we have further completed the comparison scenario of the empty structure (struct) in the Go language. After the baptism of these two articles, you will better understand why the Go structure is called both comparable and incomparable.

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

  • If it escapes to the heap, the empty structure is allocated by default to the runtime.zerobase variable, which is a 0-byte base address specifically used to allocate to the heap. Therefore, the two empty structures are both runtime.zerobase , which is of course true when compared.
  • If no escape occurs, it is allocated on the stack. In the code optimization phase of the Go compiler, it will be optimized and false will be returned directly. It's not in the traditional sense, it's really compared.

No one will come up with interview questions. No, why is the Go structure said to be comparable but not comparable?

If you have any questions please comment and feedback exchange area, best relationship is mutual achievement , everybody thumbs is fried fish maximum power of creation, thanks for the support.

The article is continuously updated, you can read it on [My brain is fried fish], and reply [160d2bfdc41ce5 000 ] I have prepared the first-line interview algorithm questions and materials; this article GitHub github.com/dc41 has been recorded , Welcome Star to urge you to update.

reference

  • Ou Shen's WeChat Exchange
  • Cao Da’s "pit" of an empty struct

煎鱼
8.4k 声望12.8k 粉丝