- Alright, so now that we've learned the guidelines around
when to use value and pointer semantics,
let's go back to that other code I showed
where we were looking at a slice of user values
and a iteration over that.
Now, again, I always wanna create a slice of values
as my core data, and that is the point of truth,
those two values right there inside the slice literal.
But we talked before, for a value of type user,
should we be using value semantic or pointer semantics?
I mean, is it reasonable to make copies of people?
And the answer really, for me, is no.
When we really should be using pointer semantics
for that user value.
I wanna show you how mixing semantics can cause a huge
amount of pain.
Look on line 50.
Notice that I'm using value semantics
for my user collection.
Already that's a smell, we shouldn't be making copies
of users, right?
Because think about it, we're making a copy
of the slice here, which is fine,
but now we're operating on a copy of the data.
On the first iteration, we're not working with this data,
we're working with a copy of this data.
That's what we want when we're dealing
with value semantics, so that's not what's wrong.
What's wrong is we shouldn't be working with a copy at all,
we should be working with Ed directly or Erick directly,
but we're not, and here's the mix.
I'm now using a pointer semantic API against a copy
of the data, and if I run this, guess what?
Ed's email address or Erick's email address isn't going
to change, the copy's going to change.
And this code is very hard to read and reason about.
This is why I cannot stress enough,
we must have semantic consistency throughout the code base
so we don't get into this kind of trouble.
Now, one of the things I wanna make sure you understand is
that methods are really made up.
They really don't exist.
All we have is state and all we have is functions.
Methods give us this syntactic sugar
that a piece of data has behavior.
That's what it's there for, to give us syntactic sugar,
this belief system that data has behavior,
and there are times when it's very important
for data to have behavior.
Again, we've got to learn when should data have behavior
and when it shouldn't, and I want data having behavior
to be the exception, not the rule.
This is one of the things you've got to kinda fight
because you've been trained,
always allow data to have behavior.
No, no, no, no, no, no, no, no.
Functions, for me, are always going to be better choice
when it's reasonable and practical to use them.
So, let's talk about a couple of things here in this code
as we continue to go through it.
I want you to look at this type data.
There it is, data, and data has two acts of behavior,
displayName, which is using our value semantics,
and setAge, which is using our pointer semantics.
The other thing, I wanna mention something really quick.
Setters and getters are not an API.
If I see in a code review any function like this,
or in this case, method like this, called set,
we've gotta have a conversation.
Setters and getters just bloat a code base,
require more testing, and really add no value.
An API really should provide something.
We're gonna talk about that more during design.
But setting and getting is not providing anything,
and I really want to avoid these types of methods.
But here we are, we've got setAge
using our pointer semantics
and displayName using our value semantics.
One of the things I want you to see very quickly,
that is, if I define a new type,
let's just say I even call it Bill,
does this new type Bill have behavior like data does?
Remember, we're saying this new type,
this new name type, Bill, is based on data,
yet I want you to understand that behavior,
there is no behavior associated with Bill data.
All of the behavior is only still associated with data.
Why is this?
One, because these methods are not declared inside the type,
like you would see in a class-based system,
they're declared outside the type.
This is Go, again, separating state from behavior.
We're not really wanting to combine it,
we're keeping it separate.
Your syntax is also keeping it separated.
So, understand that even though Bill is based on data,
the basing is based on its memory model,
not on this idea of behavior.
Okay, so I've got this data type right there.
I've got displayName, I've got setAge, there we are,
and what I've shown you here is how we call methods.
There's nothing new here.
I'm gonna create a value of type data,
there it is on line 29, so nothing new here.
Here's our data value, right?
There it is, I've put Bill inside of it.
Let's do that, there's Bill, and then on line 36,
I called displayName using d, I call setAge using d.
Again, Go will adjust to make the call.
But what I really wanna share with you,
'cause you'll see this in stack traces anyway,
but the reality is that, again, methods are made up,
it's syntactic sugar.
This is giving us a belief system
that d has this behavior displayName,
d has this behavior setAge, but when you call d.displayName
or d.setAge, really what you're doing is calling a function.
That receiver really is defined as a parameter
'cause it really is a parameter.
It's actually the first parameter
to the function displayName.
So, when you call d.displayName,
the compiler's actually gonna make a call
to data.displayName, that's the full function name,
passing the receiver d as the first parameter.
Notice here, when we call setAge,
the name of the function leveraging the pointer semantics
as part of its declaration, *data, parentheses, .setAge,
that's the real name of the function.
And here what you're seeing is Go adjusting
to make the call.
This is why Go can adjust to make the call,
because it's really just data being passed in.
Now, you are never, never, never, ever,
never ever allowed to call a method like this.
Don't do this, okay?
Please, if you do it and you say that Bill showed you this,
I'll throw you under the bus.
I'll deny this is even in the video.
You can't do this, okay?
When we're calling methods, you gotta call methods
with that syntactic sugar.
But if you looked at a call stack on some sort of panic
and you're seeing method calls,
they're gonna look like this.
So, my point here is that methods are syntactic sugar,
they give us this belief system
that a piece of data has behavior.
It does give data behavior, but we really have underneath,
technically, are just functions.
The receiver truly is a parameter,
and it's really the first parameter.
Now, I wanna drive home a little harder now the idea
of value and pointer semantics with this,
with the idea that, if you know your semantic,
if you know what semantic's in play, you know behavior.
This is everything, this is where I've been trying
to get you since we stared this video.
So, let's take a look at this code here.
Now, on line 52, what are we doing?
I'm getting the method displayName,
already bound to d, there it is, already bound to d,
but I'm not making the method call.
Look, there's no parentheses.
I'm assigning it to a variable named f1.
Functions in Go are values, they are literally typed values,
and that means that we can pass a function
by its name anywhere we want in our program
as long as, again, type information is the same,
the function's signature.
But here, I'm creating a variable.
I'm creating a level of indirection, if you think about it,
to be able to access and call displayName.
I'm creating a level of indirection.
I'm gonna be able to make the method call
like you see on line 55 through the variable itself.
But in order for that 55, line 55,
that function call through the variable to work,
we need to set up some things here.
So, f1 is a reference type and it's just a pointer,
but if it's just a pointer, it's gotta point to something.
And it does, it points to a data structure
that is a two-word data structure.
Now, the first word here is gonna point to the code for,
in our case, right now, displayName.
Remember, displayName really is a function.
So, we now have a level of indirection here.
I wanna specify this, this is important.
I have a level of indirection,
two pointers to get to the code,
but remember, I can't call this code,
I can't call displayName without d.
The whole point here is I need d, I need d.
But what d am I using?
This is where the semantics are critical.
Let's go back and look at displayName for a second.
displayName is using value semantics.
Value semantics means that every piece
of code is operating on its own copy of the data.
Value semantics are at play.
Copies of the data.
You know what that means?
It means that this f1 variable is not allowed
to reference d, it has to be given its own copy of d.
This variable always is operating on its own copy
because displayName is making sure
that we're using value semantics.
Guess what, also?
We have indirection here, and any time in Go
where you have indirection,
we're gonna have some interesting
things around escape analysis,
but right now, we've already made a copy, we made a copy.
You know what that means?
Allocation, it has to allocate.
We don't know about this at compile time,
this is happening at runtime.
What you're seeing here at the function
is really our first introduction to decoupling in Go.
f1 is decoupling not only the code we wanna execute,
but the data we need to execute against it.
And since we're making a copy of d, there's an allocation.
Okay, great, so now when we call f1 on line 55,
we have an indirect line to the code
and we know what data, so therefore on line 58,
if I make a change to the data here,
if I go, okay, you're no longer Bill anymore, you're Joan,
are we gonna see that change?
Absolutely not, we're not operating on this d,
we're operating on our own copy, why?
Value semantics were at play.
Why was value semantics at play?
Because displayName uses value semantics on
the receiver choice.
Okay, great, let's do this again.
Look at this code here.
Now we're gonna leverage setAge into a variable named f2.
Okay, no problem.
Here's f2, right here, we know it's a reference type,
it's a pointer.
It also is going to have its own internal data structure.
We know that this is gonna point to the code for setAge.
We've got that, we've got our double indirection,
which is giving us our levels of decoupling.
But what semantic is at play for setAge?
Well, we know that setAge is using a pointer receiver,
pointer semantics.
Sharing the data, not making a copy.
What does that mean?
It means that now this pointer doesn't point
to its own copy, this pointer points to the original,
original, so when we use f2
to make the method call on line 73,
we're operating against the original data
that's been shared, which means on line 76,
if we change Joan to Sammy, guess what?
We see the change, pointer semantics are at play.
Now, this is something that's very interesting.
We have double indirection again to get to the data,
and the escape analysis algorithm has
what we would call a flaw in that,
once we have indirection like this, double indirection,
the escape analysis algorithms can't track whether
or not this value can stay on a stack or not.
In other words, even though there's no reason
for d to end up on the heap, it has to now allocate.
So, there's a cost to decoupling in Go,
and it's really important, we're gonna continue to say this.
When you decouple a piece of concrete data in Go,
you're going to have the cost of indirection
and allocation.
There's no way around it right now, no way around it.
And so, we have to make sure that if we're decoupling,
that the cost of indirection and allocation is worth it.
We do not wanna blindly decouple code in Go.
We wanna do it because it's the right thing to do,
it's the right engineering choice.
It's reasonable and practical.
Because this cost, we know, is great.
Remember, this is number two on our lack
of performance list in our programs.
Allocation is number two, so we don't wanna take these
allocations lightly, but if the decoupling is adding value,
then I'll take this cost all day long
'cause if I can make sure that I can minimize
cascading changes throughout a code base,
if I can make sure that I can work
with different pieces of concrete data
without blowing up a code base,
well, I'll take the cost of allocation all day long.
Then it's worth it.
Alright, so our methods are giving us the ability
to give a piece of data behavior.
We still have to learn, when is it reasonable
and practical for data to have behavior?
I want this to be the exception and not the rule.