- a name
- a type
- a value
It will store somewhere in the memory.
func main() {
var foo, bar int = 23, 42
fmt.Println(foo, bar) // will print the value
fmt.Println(&foo, &bar) // will print the address
}
&
can be read as address of
.
Every new variable has been given an address, and with that, we can locate that in the memory. This address will be the value of the pointer if we assign it to a pointer like below
func main() {
var foo, bar int = 23, 42
p := &foo
q := &bar
fmt.Println(p, q) // will print the address of foo, bar
// 0xc00001c0a8 0xc00001c0b0
}
p
is holding the address of foo
.
func main() {
var foo int = 23
p := &foo
fmt.Println(*p)
// any guess
}
23
we define above. *
can be a little confusing at first as it can be used in two ways. - Before a type (
*int
) - Before a variable (
*p
)
Before a type
pointer type
and the int
as its base. Before a variable
*p
it’ll print 23
because it’s the value of the variable p
is pointing to. It’s also called Dereferencing
. So we can say that the value of p
is the address of foo
and *p
is the value at that address which is the value of foo
.
So what if we want to change the value of *p what will happen then. Any guess…
func main() {
var foo int = 23
p := &foo
fmt.Println(*p) // 23
*p = 42
fmt.Println(*p)
// any guess
}
42
.
func main() {
var foo, bar int = 23, 3600
p := &foo
fmt.Println(*p) // 23
p = &bar
*p = *p / 36
fmt.Println(bar)
// any guess
}
bar
variable? Quick note: We can putbar
in*p
because p’s type is a pointer and the base type is int, if it’s not int then it’ll return a run time error.
*p
so the value of the bar will be modified. So the value will be printed 100.
So why do we need pointers anyway?
Good question, right? If we just want to modify bar
s value then we can just modify bar
right? Then why??
Well, It’s efficient to store a value in one place and access it from multiple places.
Let’s understand with an example
Suppose we have four different functions and all the functions want to access bar
and want to modify it. So bar
will be modified in multiple places. This way of accessing a variable from multiple places using pointers is more efficient than creating a local copy of the variable without using a pointer.
To understand the situation more clearly, we need to understand Memory Allocations
first. Let’s understand that first… Memory Allocations
What is a goroutine?
agoroutine
is an independent path of execution. we can also think of it as a very lightweight thread that is managed by go runtime.
frame
. Let’s see this in an example for a better understand
func main() {
a := 6
AddN(a)
}
// AddN will add n to the result and print its address and value
func AddN(n int) {
r := 0
r += n
fmt.Println(&r, r)
}
main
& AddN
, when we run the main function we get a frame on the stack. The current running frame is called the Active Frame
. a
variable inside the active frame? So the straight forward answer would be we can’t access it. instead, we have to copy the value of a
into the new active frame and inside the active frame that value is going to be called n
and we can modify n
add it then print it, and do whatever we want with it but because we’re making the changes inside the active frame it will not change anything else in the program outside of this frame. So the mutation will only happen inside this isolated frame. AddN
function call ends and the active frame goes back to the main function a
will still be 6 but what if we want to change a
itself in the main function we want to get our hands on a
and not just the copy of it well this is where we start talking about pointers. a
in the main function from the function by saying go and changing the value at that specific address.
func main() {
a := 6
squareAdd(&a)
}
// AddN will add n to the result and print its address and value
func squareAdd(p *int) {
*p *= *p
fmt.Println(p, *p)
}
p
so the type of the input is *int(star int)
. The star here is not a dereferencing operator as we discuss above. Star int itself is just one whole token. We want to square the value of what’s at that address so we need to put a star in front of p if we want to say the value at p which in this example is going to be a
and then let’s print out p which is an address and the value of what p is pointing to by saying star p(*p
). a
because &
means that you’re passing in the address of a
. squareAdd
function. Instead of copying a
we are copying the address of a
and assigning it as a pointer p
in the frame and that pointer is pointing across the boundary of the frame and this is how we can modify the value of a
in the currently active frame by using *p
. Garbage collector
in detail in a future post. Stay tuned for that. Now let’s continue… AddN
it was fine there’s no way a
can get mutated but when we using pointer semantics we need to be careful because there is more possibility for the variable to be mutated in a way we didn’t intend. Heaps
Return value
package main
type person struct {S
name string
age uint
}
func NewPerson() person {
p := person{
name: "dummy person"
age: 60
}
fmt.Println("new person --> ", p)
return p
}
func main() {
fmt.Println("main --> ", NewPerson())
}
Return Pointer
package main
type person struct {S
name string
age uint
}
func NewPerson() *person {
p := person{
name: "dummy person"
age: 60
}
fmt.Println("new person --> ", &p)
return &p
}
func main() {
fmt.Println("main --> ", NewPerson())
}
NewPerson
function and 2nd code returns a pointer from NewPerson
function. NewPerson
initializes the person struct with dummy values and then returns it. After that, we call the NewPerson
function from the main function and print the result. What happening behind the scene is, Go runtime assign the main function as an active frame in the stack of memory. Then when we call the NewPerson
, a new frame is created in the stack of memory and allocates p
, and then changes the values in p. Because of the isolation of the NewPerson
frame, we can not send p
to the main function instead we will be making a copy of it and pass to the main active frame so that’s what happens when we return a value.
But instead of returning a value, let’s return the address of p
which we showed in the above example. The point to be noted here is the function still works the same way as before but instead of copying the value this function going to make a copy of the address of p
to the main function frame, we can notice something important here at the same time something weird as the NewPerson
finishes executing here the New Person
active frame is going to become invalid so the address we copied into the active frame is going to be useless we don’t know what that going to point to in the memory. So that can be a huge problem if we can’t resolve the address and this is where heaps
going to help us solve the problem.
Note Heaps
is not the same as the data structure we study in cs 101 data structures, they share the same name but completely different things.
So the compiler will analyze that and conclude that there’s going to be a problem so it’s going to copy m to the heap
then the NewPerson
function will return the address of p
in the heap and after return when the address of p
is copied to the frame of the main function. So now we can access p
with that address.
In the above, we print the address of the p
to check if they share the same address from NewPerson
function and the main function. So our problem is solved but we’re doing this in the cost of heap allocations which can be a burden for the garbage collector and it can cost us performance.