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
linkedin.com/in/emmanuelabajo - explained the underlying references in Java to me