Go Pointers

Go Pointers

The pointers in the Go language are like in the C language. A pointer holds a memory address for a value. * operator is used to denote a variable as a pointer of the type, also used in pointer dereferencing, and & operator is used to getting the underlying value.

A memory address is stored as an unsigned native word. The size of a native word is 4 bytes in 32-bit architectures and 8 bits in 64-bit architectures and indicated by hexadecimal values like 0x1234ABCD.

A basic example is:

package main
import "fmt"

func main() {
	str := "Hello Cosmos"

	// Pointer to string
	var pStr *string

	// pStr points to addr of message
	pStr = &str
	fmt.Println("Message = ", *pStr)
	fmt.Println("Message Address = ", pStr)

	// Change message using pointer de-referencing
	*pStr = "Hello Universe"
	fmt.Println("Message = ", *pStr)
	fmt.Println("Message Address = ", pStr)
}

We can pass by reference in Go:

package main

import "fmt"

// pass by reference
func zero(x int) {
	x = 0
}

func main() {
	x := 5
	zero(x)
	fmt.Println(x)
}

As well as we can pass by value with pointers:

package main

import "fmt"

// pass by value
func zero(xPtr *int) {
	fmt.Println(xPtr)  // memory address of x
	fmt.Println(*xPtr) // value of x
	*xPtr = 0
}

func main() {
	x := 5
	zero(&x)        // pass the memory address of x
	fmt.Println(&x) // memory address of x stays the same
	fmt.Println(x)  // x is 0
}

Output:

$ go run pointers.go
0xc0000aa058
5           
0xc0000aa058
0 

Pointers in slicing

It would be good if I were writing slices in another post but whenever you see a slice you see pointers in Go. So I think it’s a good place to mention it.

A string is represented by a 2-word structure in the memory. This structure contains a pointer to the string data and its length. The string data is an array of bytes, as in C.

A slice is a reference to a section of an array. So it does not create a copy of the sliced part, it only holds a pointer. A slice is represented by a 3-word structure. This structure contains a pointer to the first element of the sliced part, the slice length, and the capacity. As you know, length is the upper bound for index operations like x[i], and capacity is the upper bound for slice operations like x[i:j].

Also I saw another thing in a Medium post here:

package main

import "fmt"

type Dog struct {
    name string
}

func buggyLoop() {
    dogs := []Dog{Dog{"Ghost"}, Dog{"Bruno"}, Dog{"Lucky"}}
    dogPtrs := []*Dog{}
    for _, dog := range dogs {
        fmt.Printf("Dog with name <%s> and pointer: <%p>\n", dog, &dog)
        dogPtrs = append(dogPtrs, &dog)
    }

    for _, dogPtr := range dogPtrs {
        fmt.Printf("dogPtr with name <%s> and pointer: <%p>\n", dogPtr.name, dogPtr)
    }
}

func main() {
    buggyLoop()
}

Output:

Dog with name <{Ghost}> and pointer: <0xc00004c250>
Dog with name <{Bruno}> and pointer: <0xc00004c250> 
Dog with name <{Lucky}> and pointer: <0xc00004c250> 
dogPtr with name <Lucky> and pointer: <0xc00004c250>
dogPtr with name <Lucky> and pointer: <0xc00004c250>
dogPtr with name <Lucky> and pointer: <0xc00004c250>

This code seems to work when appending to the variable itself, but it seems failed to append to a pointer to the pointer list. It’s because of the append method’s structure. When we look at the append‘s source code we can see that the method actually takes slices.

func append(slice []Type, elems ...Type) []Type

And we learned that slice holds only a pointer to values. So here:

    for _, dog := range dogs {

        dogPtrs = append(dogPtrs, &dog)
    }

The for loop copies the value of the next value of dogs and assigns it to the loop variable dog. So dog variable only holds a copy of the value, at the same address every iteration. And append method appends the address value of the dog to dogPtrs array of pointers. At the end of the iteration, the final value of the dog will be Luck, and dogPtrs will be holding the address value of the dog 3 times. (Btw, after the loop method, the garbage collector won’t delete the dog from the memory because dogPtrs still holds its memory address.)

new & make

There are two different ways to create a data structure in Go, they’re new and make. The new returns a pointer to the created structure, while make returns the structure itself, not a pointer to it.

new(T) returns a pointer to a new T
make(T) returns a new T

You cannot create a pointer to T with make. This code will not compile:

package main

func main() {
	var1 := new(int)
	*var1 = 10

	var2 := make(*int)
	*var2 = 20
}

The compiler will give an error for line var2 := make(*int) :

.\main.go:7:12: invalid argument: cannot make *int; type must be slice, map, or channel

Resources:
Go Pointers
Go Data Structures
Pointers in Go
Go Web Programming Bootcamp