Pointers are still a mystery to me

Let me start saying that I have no informatics background whatsoever, but I do like coding, and I do like doing it in Go :slight_smile:
I am not a newbie, it has been already 4-5 years that I am coding (sporadically tho) in Go, and I believe I have a good knowledge of the Go ecosystem given the fact I managed to create several running-fine apps.

However, there is something that I really do not understand, pointers. Just to be clear, I do understand what pointers are, but I really not able to grasp where/when/why I should use them.

Why one would do this

	i, j := 42, 2701

	p := &i         // point to i
	fmt.Println(*p) // read i through the pointer
	*p = 21         // set i through the pointer
	fmt.Println(i)  // see the new value of i

	p = &j         // point to j
	*p = *p / 37   // divide j through the pointer
	fmt.Println(j) // see the new value of j

and not this?

	i, j := 42, 2701

	fmt.Println(i) // read i directly
	i = 21         // set new i value directly
	fmt.Println(i)  // see the new value of i


	j = j / 37   // divide j directly
	fmt.Println(j) // see the new value of j

Also, could someone explain me with a less trivial example where pointers have an advance over calling the variable directly?

1 Like

Your examples are without use-case context so they aren’t the right example of showing how pointer is used. Hence, the confusion.

For simple function context as shown above, example here:

func HelloWorld() {
	i, j := 42, 2701

	fmt.Println(i) // read i directly
	i = 21         // set new i value directly
	fmt.Println(i)  // see the new value of i


	j = j / 37   // divide j directly
	fmt.Println(j) // see the new value of j
}

Then the 2nd one makes a lot more sense as it makes no sense to use pointers at all.

In the context of the following processing function however:

func Calculate(i, j *int) {
    if i == nil || j == nil {
         return  // handle error and return. Can't operate
    }

	fmt.Println(*i) // read i through the pointer
	*i = 21         // set value directly to the memory i
	fmt.Println(*i)  // see the new value of i

	*j = *j / 37   // divide j through the pointer
	fmt.Println(*j) // set value directly to the memory j
}

func main() {
       i := 42;
       j := 2701;
       Calculate(&i, &j);

      fmt.Printf("After caculate i is: %v\n", i); // you won't get 42
      fmt.Printf("After caculate j is: %v\n", j); // you won't get 2701
}

Then it makes sense to use pointer. Note that you do not need to assign p pointer just to use them but you have to check the pointer’s viability (they can be supplied with nil symbolizing no data in memory is given).

The introduction and its mechanics was explained in this forum before here:

Generally speaking, you use pointer in the event where you want to:

  1. get/set a set of data stored in a specific memory rather than copy-pasting all over the places (e.g. function taking non-pointer type are usually copied as duplicate and not directly modifying the original one).
  2. Separate the processing function away from actual runtime data.
  3. Compositing a larger data structure.

A good case: if you’re working on a function for a complex data structure, as below, pointer is highly essential as duplicating can be quite daunting:

type Wheel struct {
      Clean bool
      Dimension int64
      Brand string
      ...
}

// create more component data structure, like door, windows, engine, etc. 

type Car struct {
      WheelA *Wheel
      WheelB *Wheel
      WheelC *Wheel
      WheelD *Wheel
      ...   
}

func WashTheCar(car *Car) {
        if car != nil {
                WashTheWheel(car.WheelA);
                WashTheWheel(car.WheelB);
                WashTheWheel(car.WheelC);
                WashTheWheel(car.WheelD);
                // wash something else ... like door, windows, wipers, bumper, car plate, etc.
        }
}

func WashTheWheel(wheel *Wheel) {
       if wheel != nil {
              wheel.Clean = true
       }
}

func main() {
        mercedes := &Car {
               // fill in car data
        };

        lambo := &Car {
               // fill in car data
        };

        WashTheCar(mercedes);
        WashTheCar(lambo);
}

You can still re-write the above without pointer implementations using return value but for a large data structure, you will find at one point you will be bottle-necking yourself copy-pasting the return-ed value here and there, making your main function unnecessary long and complicated. Also, FYI, at low-level, these copy-pasting can cost a lot of compute cycles just to achieve the intended effect: directly update the already memory allocated mercedes and lambo data for a given function WashTheCar.

3 Likes

I am a newbie as well, but to me the difference is about the same as between a parcel and an address to a parcel. Less weight with a piece of paper. :slight_smile:

I found one explanation here:

2 Likes

@hollowaykeanho Thanks for the detailed explanation, things start to be clearer :slight_smile:

Just to make sure I fully understand your first case

  1. get/set a set of data stored in a specific memory rather than copy-pasting all over the places (e.g. function taking non-pointer type are usually copied as duplicate and not directly modifying the original one).

Does this mean that if I do this:

func Calculate(i, j int) (int, int) {
	fmt.Println(i) // read i, duplicate?
	i = 21         // set new value i, into its duplicate?
	fmt.Println(i) // see the new value of i, from its duplicate?

	j = j / 37     // divide j
	fmt.Println(j) // see the new value of j

	return i, j //those are the duplicates right? they will get handled by the GC?
}

func main() {
	i := 42
	j := 2701
	i, j = Calculate(i, j)

	fmt.Printf("After caculate i is: %v\n", i) // you won't get 42
	fmt.Printf("After caculate j is: %v\n", j) // you won't get 2701
}

i and j are duplicated when used within Calculate? Meaning that
i = 21 and j = j / 37 are actually allocating new addresses into the memory, rather than modifying the already allocated ones directly?

If that is the case, what would happen if the Calculate function is as follow:

func Calculate(i, j int) (int, int) {
	m := i / 2        
	n := j / 37

	return m, n
}

m and n are for sure allocated. What about i and j, do they get duplicated here?

1 Like

Nope, that will be 2 copy-duplicates, one at the parameter input, one at return value in main. I’m referring to the parameter level. To be precise, here are the comparison (without the return modifications):

func Calculate(i, j int) { // as the function is called by main, i, j actual data are duplicated here as another set of memory data
	...
}

func main() {
	i := 42
	j := 2701
	Calculate(i, j)

	fmt.Printf("After calculate i is: %v\n", i) // you WILL get 42 unchanged
	fmt.Printf("After calculate j is: %v\n", j) // you WILL get 2701 unchanged
}

Compared to:

func Calculate(i, j *int) { // as the function is called by main, i, j pointer data are duplicated here as another set of memory data
	...
}

func main() {
	i := 42
	j := 2701
	Calculate(i, j)

	fmt.Printf("After calculate i is: %v\n", i) // you WONT get 42 but the value you last modified in Calculate()
	fmt.Printf("After calculate j is: %v\n", j) // you WONT get 2701 but the value you last modified in Calculate()
}

Notice that the one without using pointer, you will expect the i and j values in main remain unchanged but the one using pointer does even though main does not manually modify their values.

In case you’re wondering, for the function using the pointer input, the pointer data type is copied over (pointer itself is also a data) but not the actual data in memory. int data type is too primitive to visualise; imagine large data structure like the car above and you can see the profit.

Same case, explain in verbose mode with a manual copy over:

func Calculate(i, j int) (int, int) { // i, j are duplicated as a different set of data in memory
	m := i / 2        // i looks the same as 4 but it's a different 4 from main's i
	n := j / 37       // j looks the same as 37 but it's a different 37 from main's j

	return m, n      // return the newly processed values
}

func main() {
      var retI, retJ int
      i := 4
      j := 37

      retI, retJ = Calculate(i, j);  // newly processed values saved in retI and retJ

      fmt.Println(i);  // expect 4 instead of 2
      fmt.Println(j);  // expect 37 instead of 1
      fmt.Println(retI); // expect 2
      fmt.Println(retJ); // expect 1

      i = retI    // manually copy over from return value
      j = retJ   // manually copy over from return value

      fmt.Println(i);  // expect 2
      fmt.Println(j);  // expect 1
}

As for when GC kicks in, can’t tell unless you run your program against memory profile and tracks GC activities. However, m and n are definitely released since they’re highly likely to be in stack memory if we speak in C language (can’t guarantee since Go compiler operates differently without memory profile report). If for unknown reason Go compiler allocates them to heap memory (again very unlikely), then they are for sure GC-ed since they are no longer in used.

2 Likes

The best description I’ve seen on where/when/why to use pointers is in this video course by Bill Kennedy.

Not cheap, but very good.

1 Like