Can two new objects point to the same memory address in GoLang?

Summary

Go developers might be surprised to find that two distinct, newly instantiated empty structs can point to the same memory address, resulting in a true equality comparison. This behavior is observed when variables are forced to "escape to the heap," often triggered by functions like fmt.Printf due to their interface parameters and underlying reflection mechanisms. Upon escaping, Go's runtime.mallocgc function is called for memory allocation; however, for zero-sized objects, mallocgc specifically returns the address of zerobase, a global variable, rather than allocating new memory. Consequently, if multiple empty structs escape to the heap, they will all be assigned the identical zerobase address, explaining their unexpected equality.

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

GO GOLANG VARIABLE ESCAPE ZEROBASE

  RELATED

  COMMENTS

0

No comment for this article.