- Alright, so we just looked at the base mechanics
for value and pointer semantics.
These are going to be very important going forward.
Again, I want to make sure that we can read code
in Go and understand the cost
and the impact that code is having.
Memory is going to be a big part of that.
Now, there's a big thing here around these semantics,
the value semantics and the pointer semantics,
and it's the cognitive load
around understanding how to manage memory.
One of the most beautiful things about Go
is it takes that burden off of us.
We don't necessarily have to worry about things
'cause we make a mistake with where things are located
and how we access it, it can cause
very very big problems for us.
So our value and our pointer semantics are gonna be big
for us, and we're gonna be able to read and understand
how things behave, and again, let's talk about that again.
Value semantics have the benefit of being able
to mutate memory and isolation within our own sandboxes,
but it has the cost of inefficiency.
We're gonna have lost a copy of the data
as we cross over these program boundaries.
Pointer semantics, however, fix the efficiency issue.
We're just gonna have one piece of data,
we're gonna share it, everybody can mutate it,
everybody's gonna see it, but it comes
with the cost of side effects and a lot more work
for us to make sure that we're not corrupting data
or things aren't being changed behind the scenes
without a piece of code or a Go routine later on knowing it.
And this is complex stuff,
I'm just not gonna hide it for you.
But, if we balance our value
and our pointer semantics properly,
leveraging the aspects of the language
helping the cognitive load over memory management,
it's going to be a lot better for us.
So I want to show you another example here
of these semantics, and how the language is really here
helping some of our cognitive load.
This is also gonna be important in our ability
to understand the impact and cost
our code is having on the machine.
So I've got a user defined type here.
It's username and email, and what I do
is I have two functions, one called create user v one,
and one called create user v two.
Now, as I shared before, we don't have constructors in Go.
Yay, we don't want that, right?
It hides cost, but what we do have
is what I call factory functions.
Factory function is a function that creates a value,
initializes it for use, and returns it back to the caller.
This is great for readability, it doesn't hide cost,
we can read it, and lends to simplicity
in terms of construction.
Now I don't want to call these functions
constructors because that's baggage.
I call them factory functions.
And I've got two factory functions in this piece of code,
version one and version two of the create user.
Let's start with version one of the factory function.
Now the first thing I want you to do is look
at the return type for the function.
It returns a value of type user.
Immediately that clues us into the understanding
that this factory function is using value semantics.
Throughout this class, I'm going
to ask you what semantic is in play.
This is critical to everything as you will see.
And when we talk about v one, value semantics are in play
because the caller's going to get their own copy
of the value after this function returned.
Now if we were to map out visually
how this function's behaving, we would probably think
that this is what's going on.
We're in main, now we call v one.
There it is, version one.
And right away in this function
on line 25 through 28, we have literal construction.
We're constructing a value of type user,
using our literal construction.
We end up with a variable named u and a value
that has at least Bill inside of it.
There it is, we're seeing construction,
and right now, I'm drawing it within the frame
that the active goroutine.
Here's our active frame at this point.
Our goroutine is operating within this frame right here
when we do the construction.
Now I want you to look on line 30.
The ampersand operator is very powerful
from a readability standpoint.
Ampersand means sharing, and now when I read line 30,
what I see is we are sharing the user value
down the call stack to the print function.
At that point, we have another frame for print,
and we see here that we are
sharing that value down the call stack.
Now when print wants to display it,
it's gonna do it through an indirect pointer here,
it's doing it through indirection,
but it's okay because when this is the active frame,
remember all of the memory associated
with the active frame up is valid.
This is perfectly clean to share that pointer down.
Life is good.
Eventually, this is going to return.
This is gonna be the active frame again.
Our goroutine will operate on here.
Life is good.
And then on line 32, notice that we're doing a return u.
This is the value semantic call, right?
We are making a copy of u and passing it back up
across our program boundary back up the call stack,
which now means that when this goroutine
is now operating here, here's our active frame,
here's our goroutine right here.
This goroutine now is operating on its own copy of u,
there it is, Bill.
Which is fine because, again,
that memory that we were using
below the active frame, we didn't need it anyway
because we're operating on our own copy.
This is classic classic value semantics,
and you can see the levels of integrity we get
because the goroutine is operating on its own copy.
It's gonna operate on its own copy.
We did make a copy of it,
but this is now the point of truth.
We don't care about anything that's happening below.
Again, we can't worry about this data here,
and it doesn't matter because when main makes another call,
it's gonna wipe out all of this data.
This is all basically ready for use again.
But we're seeing our value semantics pretty strong here.
Now let's look at version two of this piece of code.
If you notice here, version two, look at the return,
it's not using value semantics anymore.
Version two is using pointer semantics.
Notice what it's saying is it's going to give the caller
a copy of the address that we are constructing.
This is very common stuff to do in Go.
Let's draw out what this looks like on the board
and get a sense of what's happening here.
So, I'm gonna clean up my stack right here.
We're gonna have our main right here.
We're gonna have version two.
Now, this code looks identical to version one.
There is a couple of subtleties,
but it really kind of looks identical.
Look on line 39 through 42, it looks
like we're performing the same exact construction.
It looks like we're constructing a value
of type user, initializing it to Bill.
We're sharing it down the call stack again.
Again here, this is our active frame,
our goroutine is operating right here.
We share it down the call stack again like we saw before.
We're gonna share it.
The active frame was here, we come back up.
But this time, on the return,
we're not making a copy of the value.
We're making a copy of the value's address.
Again pass by value where the data
is either a value or an address.
Now it looks like, from reading the code on the surface,
this is what we're gonna end up with right here.
Now, if this was truly the case,
we would be in a tremendous amount of trouble.
Think about what I've just drawn on the board.
As we perform the return and share the value
up the call stack and once this becomes the active frame,
this is a very very bad scenario.
If this was really happening, we'd have an integrity issue
because when main makes the next function call,
it's gonna take a frame,
and it's gonna wipe all of that out.
Oh my goodness, think about it.
When we share up the call stack,
what we're doing is basically putting ourselves
in a situation where the pointer is pointing
to a piece of data that can be corrupted.
Side effects, oh my god,
this stuff gets very very complicated.
So this absolutely cannot be what is happening,
and it's not, and if we didn't have the compiler coming in
and dealing with this, this is something that you
would have to deal with as a developer,
which is why it's so complicated,
and we've made so many mistakes in the past.
So, what's happening here is the compiler
is really really powerful.
The compiler is able to perform static code analysis,
and in this case what the compiler will do is perform
what's called escape analysis.
And this static code analysis called escape analysis
will determine whether a value gets to be placed
on the stack, which is what we want,
or it escapes to the heap.
Notice that our first priority is
that a value stays on the stack.
And this is because that memory is already there.
It's very very fast to leverage the stack.
Also stacks are self-cleaning,
which means that the garbage collector
doesn't even get involved until a value
escapes a stack and ends up on the heap.
And in Go, that is what we call an allocation.
An allocation in Go is when an escape analysis determines
that a value cannot be constructed on the stack,
but has to be constructed on the heap
because to keep it on the stack,
like in this particular case would have an integrity issue.
And now the garbage collector has to get involved
with anything that allocates to the heap.
Now you might be saying but why does the garbage collector
not have to deal with stacks.
How are stacks self cleaning?
Let's go back to zero value for a second.
Remember every time we make a function call,
we take a frame and we clean it.
Zero value helps to make sure that every single byte
and every single bit is set to where it's supposed to be.
There's nothing left random.
You make another function call, what happens?
You make another function call, we take another frame,
we clean it, take another frame, you clean it.
See, stacks are self-cleaning
because as you make function calls,
the stack is being cleaned all the way down,
but as we've come back up and returned,
we leave that memory.
It would be inefficient to clean memory
on the way up because we don't really know
if we need it again.
We have to have some levels of efficiency.
So memory is left alone on the stack as we go up
and cleaned on the way down, so the garbage collector
doesn't have to get involved.
The stack can give us a tremendous amount
of performance because the memory
is already allocated and it's self-cleaning.
We really want to try our best
to leverage our value semantics
and to keep values on the stack
because again, one of the benefits
of not only the isolation and the immutability
and the reducing our side effects,
but in many cases can also give us better performance
because once something is allocated,
the garbage collector has to get involved.
Again, we gotta learn how to balance
our value and our pointer semantics.
So if none of this is happening
with the code I just showed you, what is happening?
This is one of the most beautiful things about Go.
The Go syntax abstracts away the machine.
It's really powerful, very very interesting.
It abstracts away the machine,
so you don't necessarily have to care where data is,
but when performance starts to matter
and debugging starts to matter,
it does begin to become important
to understand the escape analysis and where things are.
So let's walk through this code with the understanding
of what's really happening thanks to escape analysis.
So, I've got my stack.
I've got main.
We're now in v two, and suddenly what happens
during construction on line 39?
Well the way escape analysis works
is it doesn't care about construction.
Construction in Go tells you nothing.
What tells us everything is how a value is shared.
Sharing tells us everything, and because of line 46
the sharing of the value up the call stack,
that's gonna clue us in that escape analysis
is not gonna construct u user on the stack.
But it's going to go and construct
this user value out on the heap.
Interesting, construction is happening immediately
on the heap.
Now, what gets super interesting to me is this.
U represents a value of type Bill,
but it represents a value of type Bill
that's not on the stack frame, but on the heap.
And what we know is that if you want to access
any sort of data that doesn't exist inside your frame,
remember this is the active frame,
our goroutine is operating here.
If you want to access anything that is not on this frame,
you can only do that through a pointer.
This is really amazing stuff.
From a syntax perspective, u is a value on the heap.
You get to keep your code simple
by manipulating the u value through your value semantics;
however, underneath the covers,
the compiler knows well, I've to have that escape,
so even though the syntax at the coding level
is a value of type u, we'll convert that underneath
to a pointer to be able to access to the heap value.
How cool is that?
The syntax and the language is abstracting the details
and that cognitive load from you,
but it's important that you know that,
but we're really working with u is truly a value
on the heap that you get to work with
as a value and not as a pointer,
which historically you'd have to deal with yourself.
Now when we share u down the callstack,
we're making a copy of that heap address,
and when we get to the return on line 46,
we're making a copy of the heap address, whatever that was.
And so we no longer have an integrity issue.
When we're now back up here as the active frame,
right, and this is our goroutine now,
well, it doesn't matter.
None of this memory, again, is in play.
It's invalid, but it doesn't matter
because we have a pointer to the actual value
that's out there on the heap.
Yay, but there's a cost to this, right?
And that cost was an allocation.
This value now is out on the heap,
and now the garbage collector has to manage it.
So, there's a guideline here that I want to provide
because the ampersand is a very powerful
readability operator, and you don't wanna walk away from it.
Let me ask you this question.
If this is the only code that I showed you,
and I'm zooming into the return here.
If this is the only code that I showed you,
return ampersand u, what do you know already
based on what I've taught you?
You know that there is a cost to this line of code
because even if you don't know what u is,
you now know that it's potentially on the heap.
In fact, it's on the heap.
It's being shared up the call stack,
and therefore, escape analysis is gonna say allocation,
which now means the garbage collector.
You know the cost of this line
because you can read it, ampersand u.
But what if I show you this?
What if I get rid of the ampersand?
Now what does this line of code share with you?
The answer to that question is
it tells you nothing, nothing.
It doesn't necessarily tell you anymore what the cost is.
You have to be able to read the rest of the code.
We just walked away from some readability
because I have to read all of the code
to understand the cost, why?
Because I can do this, and for me,
this is clever code.
This time during the construction,
I am telling the compiler I don't want you
to be a value of type user.
I want it to be a pointer
to the value that we are constructing.
This, to me, is a total nightmare.
We are using pointer semantics during construction,
even though we're creating a variable,
and now we've made this code much harder to read,
and we're really also mixing semantics
as we go along the way.
Anytime you mix semantics we're going to have a problem.
So, here is a general guideline.
I never want to use pointer semantics during construction.
I want to use value semantics during construction.
If you're going to assign that value
that you're constructing to a variable
so we can leverage the highest levels of readability in Go,
we can show sharing down, we can show sharing up,
you can do your own escape analysis.
When we do this, we're walking away from readability.
The only time I want you to use pointer semantics
on construction is if you're gonna do that on a return.
Okay, we're not assigning it to a variable,
we're doing it on a return, or again,
you're gonna do it inside a function call.
We're not assigning it to a variable.
We're doing it within the scope of a function call.
That's where that syntax makes sense.
But if your goal is to assign a value you're constructing
to a variable, please, please
use value semantic construction.
Construction doesn't tell you
where something is in memory, only how you shared.
So if you start the life of a variable as a pointer,
you're walking away from readability,
and you're walking away from this idea
that construction tells you nothing.
This is what we really wanna do.
And you wanna see some really nasty code,
we talk about mixing semantics.
Imagine you saw code like this,
and I've seen this before.
This stuff really makes me cry.
Look, we're using value semantics on the return,
pointer semantics on line 39 on the construction,
and you're going back to value semantics on line 46
because you gotta get back to it.
And what's really interesting about this code is
that value never escapes the heap.
We're using value semantics,
yet your code is using pointers.
Oh, I wanna just duct tape my head
when I see this kind of stuff.
This is not good.
So, we want to make sure that we're using
the right semantics and semantic consistency
all of the time.
We don't want to construct values
to variables using pointer semantics.
We want to leverage the ampersand in the right place.
Now what's also very powerful here is
you're gonna use go build to build your binaries,
and when you use the gc flags on the go build calls,
and we'll do this as well when we're on go testing later on,
but when you use this set of flags on building and testing,
what you're gonna get is not a binary,
but what we're gonna get is the escape analysis report.
We're gonna come back to this during profiling
because I want to show you how powerful it is
to have this report because when we look at a profiler,
it can only show us what is allocating.
It can't tell us why.
This report tells us why something is allocating.
So I don't want you using it while you're coding.
We're gonna use this as it relates to profiling.
But notice down here, what it basically tells us is
that we're u to the heap and it's because of the return.
Again, the return is causing a share up the call stack,
which means you can't leave it on this frame
'cause once we come back here,
this is the active frame, it has to escape to the heap.
We now have an allocating,
we now need garbage collection involved.