Explain To Me Like A Baby: Golang Pointers and receiver Method pt 2 - when to use

Explain To Me Like A Baby: Golang Pointers and receiver Method pt 2 - when to use

It took me so long to put my thoughts together about part 2 of this post where I described what pointers are, and how they can be used and accessed. I was recently discussing with a senior coworker about pointers and he mentioned something about garbage collections. I was glad I had read about it a few days ago and I could make contributions. I must share some of the things I have learned about pointers in GO. This article will explain when and why to use pointers. The next article will describe the relationship between pointers, values, and garbage collectors. All right, let's get right to it!

Methods in Golang

Just before explaining when to use pointers, I will explain methods in golang. Creating a Method in Golang simply attaches a function to the instance type. The type can be any of the inbuilt GO data types or a custom type you create yourself.

INSTANCE: the instance of a type is a variation of that type. For example, if you have a built-in type “string”, you can create multiple variations of the type “string”. You can have a string of 4 letter words and another of 100 letter words depending on what you are trying to store. You can also have multiple instances of custom types.

// using a built-in type to create - string

type str string // this is an instance of the type string

// using a custom type

var newStr str // an instance of a custom type str that we created above

Receivers in Golang

This brings me to receivers. A receiver is an instance; the instance of a particular type. Now we can rephrase the method definition above - Method in Golang is simply attaching a function to a receiver. Some people call methods in golang receiver functions. To explain further, attaching a function to a receiver or an instance of a type means that the function becomes a method of that type. This function has access to all the properties of the instance/receiver. The instance/receiver can also be a variation of a pointer.

type myType struct {

  model string

  year int

}

// instance without pointer

func (myReceiver myType) myMethod() (string, int) {

  myReturnString := myReceiver.model

  myReturnInt := myReceiver.year

  return myReturnString, myReturnInt

}

//instance of a pointer

func (myReceiver *myType) myMethod() (string, int) {

  myReturnString := myReceiver.model

  myReturnInt := myReceiver.year

  return myReturnString, myReturnInt

}
  • I created a new type of type struct

  • I created a new method

  • The method has a receiver/instance of type myType

Pass by pointer Vs Pass by value

Now that we understand the methods are in golang, I will explain how arguments are passed. Arguments are either passed by value or by pointer - this simply means passing the actual value or passing a pointer (the memory address of a value).

The arguments/parameters are used in functions to share information between different scopes. These parameters are divided into 2 - actual parameters and formal parameters. Formal Parameters are parameters passed to a function at the point of declaration (function declaration).

func argType(formalParameter string) {

// do something

}

Actual Parameters are parameters passed to a function at the point of call (function invocation).

func main() {

  var actualParameter = “some parameter”

  argType(actualParameter)

}

Example

func passByValue(name string) string {

    name = "Gwen"

    return name

}

func passByPointer(name *string) *string {

    *name = "Gwen"

    return name

}

func main() {

      var name = "Sharon"

    fmt.Println("name at declaration is: ", name)



    returnNameByValue := passByValue(name)

    fmt.Println("returnNameByValue: ", returnNameByValue)

    fmt.Println("name after passedByValue is: ", name)





    returnNameByPointer := passByPointer(&name)

    fmt.Println("returnNameByPointer: ", returnNameByPointer)

    fmt.Println("name after passedByPointer is: ", name)

}
=====> 

Result

name at declaration is:  Sharon

returnNameByValue:  Gwen

name after passedByValue is:  Sharon

returnNameByPointer:  Gwen

name after passedByPointer is:  Gwen

From the above code, it's clear that when we pass an argument by value, the actual parameter cannot be changed from within the function even when the formal parameter has changed. This means that the value of the actual parameter (it has its own memory address) has been copied into an entirely new memory address (the memory address of the formal parameter).

When To Use Pointers

The case is different when we pass an argument by pointer - the formal parameter is a memory location of the actual parameter. The formal parameter points to the actual parameter - if the value at a location is altered within a function, the value changes wherever the actual parameter is used. You can use a pointer whenever you want to make sure the exact value is passed across your application.

The GO primitive types - int, string, float, bool, array and struct - are all passed by value. Slices, maps, channels and functions are all passed by pointers.

You must be wondering by now why I have not mentioned passing arguments by reference, well, GO as a language does not support the reference type. What we refer to as reference types are all pointers - they all refer to underlying pointers.

Reference Types

A reference is an alias and it also holds the memory address of a value. Below is an example from Dave Cheney's post that explains what reference types are in c++. You would also find a similar reference type under the hood in Java.

#include <stdio.h>

int main() {

        int a = 10;

        int &b = a;

        int &c = b;

        printf("%p %p %p\n", &a, &b, &c); // 0x7ffe114f0b14 0x7ffe114f0b14 0x7ffe114f0b14

        return 0;

}

Variable b references variable a (It stores only the memory address of variable a - variable b is an alias of variable a), variable c references variable b (It stores only the memory address of variable b - variable c is an alias of variable b). The result shows that they both point to the same memory location - “0x7ffe114f0b14”. A new memory location is not created. Alias: otherwise called: otherwise known as - another name for something. This simply means we have given variable a 2 other names - variable b and variable c.

In pointers, each time you assign a pointer to a new variable a new memory address/location is created. Another alias is not created. See below:

var a = 10

var b = &a

var c = &b

fmt.Println("a: ", &a) // a:  0x1400001a2b8

fmt.Println("b: ", b) // b:  0x1400001a2b8

fmt.Println("c: ", c) // c:  0x1400000e040

We cannot define variables with reference in Golang, it will throw an error. See below:

func referenceCheck()  {

    var a = 10

    var b, c &int

    b = a

    c = b

}

error: Cannot take the address of 'int'

Type 'int' is not an expression

Other notable differences:

  • References cannot be declared without initialisation as seen in this example:
int a = 10;

int &p = a; // It is correct

// but

int &p;

p = a; // It is incorrect as we should declare and initialize references at single step

On the other hand, pointers can be declared without initialisation:

func pointerCheck() {

    var a = 10

    var b *int

    b = &a

    fmt.Print(b) // 0x14000122218%  

}

References cannot be reassigned:

int a = 5;

int b = 6;

int &p = a;

int &p = b; // This will throw an error of "multiple declaration is not allowed"

// However this is a valid statement,

int &q = p;

The variable p, as stated earlier in the definition of a reference, is declared and initialised to be an alias of variable a (It stores only the memory address of variable a) and therefore it can no longer be used as an alias of another variable b (It can no longer be used to store the address of variable b). Although, as seen in the example above, it can be assigned to another reference variable q.

However, you can reassign a new address to a pointer; equating to a new variable entirely.

var a = 5

var b = 6

var p *int

p = &a

fmt.Println(p) // 0x1400012c218

fmt.Println(*p) // 5

p = &b

fmt.Println(p) // 0x1400012c220

fmt.Println(*p) // 6

If you found this article helpful, share it with your friends and colleagues! Thank you for reading and Stay tuned for more on pointers!

References