Do you have any idea what the output will be for below GoLang snippet?
package main
import (
"fmt"
)
type obj struct{}
func main() {
a := &obj{}
fmt.Printf("%p\n", a)
c := &obj{}
fmt.Printf("%p\n", c)
fmt.Println(a == c)
}
Many people would think that a and c are two different object instances which have different memory addresses. Hence a == c will be false. But if you try to run the above program, you would see below output
0x5781c8
0x5781c8
true
To get to know the reason why the comparison returns true, some understanding of how Go allocates memory and how variable memory escape happens are needed. Before that, let's do some experiment first, what if we comment either fmt.Printf() statement.
package main
import (
"fmt"
)
type obj struct{}
func main() {
a := &obj{}
// fmt.Printf("%p\n", a)
c := &obj{}
fmt.Printf("%p\n", c)
fmt.Println(a == c)
}
The output is
0x5781c8
false
The comparison returns false now. This means that the fmt.Printf() is making some difference here. Let's run below command
go run -gcflags '-m -l' main.go
# command-line-arguments
.\main.go:13:13: c escapes to heap
.\main.go:12:7: &obj literal escapes to heap
.\main.go:15:16: a == c escapes to heap
.\main.go:10:7: main &obj literal does not escape
.\main.go:13:12: main ... argument does not escape
.\main.go:15:13: main ... argument does not escape
0x5781c8
false
From the output, variable c has escaped from stack to heap, but a is not and it remains at stack. A simple conclusion here is that the fmt.Printf() statement causes the variable to escape from stack to heap.
Why would fmt.Printf() cause variable to escape? Indeed the second parameter of fmt.Printf() is type of interface, the internal implementation of fmt.Printf() uses reflect which causes the variable to escape from stack to heap(though not all reflect would have this effect).
Another question is that why the two objects would have the same address after the escape to heap? This is because the memory allocation on heap calls the newobject function in runtime package. newobject function would invoke mallocgc function to allocate memory. This function is a bit special and it has some logic to check whether an object is indeed occupying memory or not, if no memory is needed, the address of zerobase(a global variable) will be assigned to it. In the above example snippet, both a and c are empty struct which doesn't need memory. Hence both a and c are having the same memory address which is the address of zerobase.
import "unsafe"
// Allocate an object of size bytes.
// Small objects are allocated from the per-P cache's free lists.
// Large objects (> 32 kB) are allocated straight from the heap.
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
if gcphase == _GCmarktermination {
throw("mallocgc called with gcphase == _GCmarktermination")
}
// ...
if size == 0 {
return unsafe.Pointer(&zerobase)
}
// ...
}
Hope this helps a bit understand how memory works in GoLang.
Reference: https://mp.weixin.qq.com/s/GvPQVvGj0o0w5zlUD-GMpg