6

I was just playing around with Go interfaces and structs that suddenly found something weird to me. This is the case:

https://play.golang.org/p/FgvRFV9Lij9

package main

import (
    "fmt"
)

func main() {
    scopedInt := 100
    fmt.Printf("%p\n", &scopedInt)
    
    globalInt = 100
    fmt.Printf("%p\n", &globalInt)
}

var globalInt int

output:

0xc0000ba010
0x57b2a8

The value of the addresses doesn't matter. The point is that Why is the number of digits of first address more than second?

I think I have missed a point about the concept of global variables in Go.

Jonathan Hall
  • 75,165
  • 16
  • 143
  • 189
shayan rok rok
  • 494
  • 3
  • 12

2 Answers2

10

The "length" of the addresses differs by many digits because those variables are allocated on different areas of the memory, which have different offsets (starting locations).

scopedInt will likely be allocated on the heap because it "escapes" from main() (its address is passed to fmt.Printf()), while globalInt is a package level variable and as such will be allocated in one of the fixed sized segments, the data segment.

The "length" of addresses doesn't matter as long as they point to a valid memory area. Go has automatic memory management, so unless you touch package unsafe, you don't have to worry about addresses and pointers being valid or not.

To read more about memory management, see Doug Richardson: Go Memory Management. Quoting from it:

What Goes Where?

The Go Programming Language Specification does not define where items will be allocated. For example, a variable defined as var x int could be allocated on the stack or the heap and still follow the language spec. Likewise, the integer pointed to by p in p := new(int) could be allocated on the stack or the heap.

However, certain requirements will exclude some choices of memory in certain conditions. For instance:

  • The size of the data segment cannot change at run time, and therefore cannot be used for data structures that change size.
  • The lifetime of items in the stack are ordered by their position on the stack. If the top of the stack is address X then everything above X will be deallocated while everything below X will remain allocated. Memory allocated by a function can escape that function if referenced by an item outside the scope of the function and therefore cannot be allocated on the stack (because it’s still being referenced), and neither can it be allocated in the data segment (because the data segment cannot grow at runtime), thus it must be allocated on the heap – although inlining can remove some of these heap allocations.
icza
  • 389,944
  • 63
  • 907
  • 827
2

Global uninitialized symbols like globalInt are stored in the BSS segment, close to the data segment, in the lower address space of the program.

You can check the addresses of program symbols with the nm utility:

$ go build main.go
$ go tool nm main | grep globalInt

Outputs:

118e210 B main.globalInt

The first hex in the output is the address of the symbol, which is what will be printed when you run the program. E.g. on my machine:

$ ./main
0xc000124008
0x118e210      <--- same as nm output

The letter B after the hex stands for bss segment symbol.

If you explicitly initialize the variable in the source code, like var globalInt int = 600 the output of nm will show:

114b268 D main.globalInt

where now D stands for data segment symbol.

Anyway all these are in the lower address space of the program. The scopedInt doesn't pass escape analysis and is allocated on the heap, where its address will be higher than the global var.


Consider that all this is implementation-dependent. The specs don't mandate where to allocate objects.

If you compile and run the same program with TinyGo, the output will not look alike:

$ tinygo build -o tinymain main.go
$ ./tinymain 
0x1158bf040
0x1061f3ca8
blackgreen
  • 34,072
  • 23
  • 111
  • 129