- We just reviewed around how mechanical sympathies
of using arrays and slices and maps a little bit,
and we showed how these mechanical sympathies
are real, as it relates to hardware that handles caching.
I wanna try to sum up a lot of what we've talked
about up to this point, while using arrays,
and to get back into some of the conversations
we started earlier around value and pointer semantics.
There's a lot here in this section I wanna cover
that's gonna give us a really strong base foundation
as we move forward and learn about it in a design
and our data and our data structures.
Let's start with this piece of code here.
We're not doing anything special on line 14.
We're declaring an array of five strings,
again five being that constant of kind int.
The compiler knows about the size of an array
in compiled time, because the size of an array
must be known in compiled time.
You can't use a variable for an array.
We will be able to do that with our slices
we'll see in the next section.
When I see line 14, what I see is 40 bytes of memory.
Remember, a string is a two-word data structure.
We're on our playground.
What I see is starting an index one, two, three, four,
I see five strings laid across contiguously,
all set to their zero value which makes them empty strings.
This is what we have after line 14.
This is what I see.
This is our fruits array.
I wanna focus on line 15 for a second.
What we see on line 15 is a literal string named apple.
That's also a string that's gonna be appointed
to a backing array of five bytes apple
and we're gonna have the number five.
It's a literal string, it's a constant.
That data will be in our data segment.
Let's just imagine right now that we have our string,
we've got this pointer, there it is.
One of the questions I always like to ask in a class
is what is the cost, where we're talking
about engineering costs and benefits,
what is the cost of this assignment on line 15?
Remember, an assignment is a copy operation,
so the question is what is being copied on line 15?
This string is really the combination
of two data structures.
It is the string value and a pointer to the backing array.
What's really interesting here is that the cost
of this is just the two-word data structure.
All we're going to be copying is the A, or 16 bytes,
depending on the platform of memory that's associated
with the string value, which tells us right away
something very special.
Remember, strings, I'm sorry pointers are for sharing.
Pointers are for sharing, and if you look here,
this pointer is giving us the ability to share
the backing array across these two distinct string values.
Remember, pointers are for sharing and that sharing
gives us a level of efficiency.
I only need one of these backing arrays
for the two string values that we have.
Go only uses a four statement for all of its iteration.
You can use the four in a traditional way,
like I'm showing you on line 31 here.
You can also use four (mumbles),
where you just specify the condition.
But, the four range is a very special
and powerful iterator in Go.
The reason why it's so powerful is because the four range
comes with two different semantics.
Here we go again with semantics.
There are value semantics and pointer semantics
associated with the four range.
Remember, again, when I talk about value semantics,
what I mean is that every piece of code
is operating on its own copy of the data.
As we transition across program boundaries,
in between functions, as we do things,
we're operating on our own copy of the data.
What are the benefits (mumbles)?
Again, we get the sense of immutability, isolation,
mutation is isolated, and we don't create
these side effects.
But, we also have the ability to have pointer semantics.
What is pointer semantics, again?
It means that we're sharing.
We're gonna get efficiencies.
But, because we're sharing it, mutation has to be dealt with
either through synchronization, orchestration.
We have to understand the effects mutation can have
because they can create side effects
throughout our application.
What you're seeing on line 22 is the value semantics
form over the four range.
There's a few things happening here.
We're gonna be ranging over this array
of strings called fruits.
On every iteration, we're gonna get the index position,
zero, one, two, three, four.
This local variable to the four loop.
Notice, we're using the short variable declaration operator.
This local variable to the four loop is going
to be a copy of every value that we're iterating over.
In other words, our fruit variable,
that ends up also being a two-word string value
and a copy as well.
After that, if you notice, we now have
three string values on the board,
all sharing very efficiently this one backing array.
We're not done yet, because I want you
to look on line 23.
On line 23, we're calling print, and when we call print,
look at what we're doing in the second parameter.
We are passing fruit, but what are we really passing,
what is the data?
We're passing a copy of the value of fruit.
What does that mean?
It means in the stack frame below us, we now get a second,
or now one, two, three, four, our fourth string value
that is sharing the backing array.
Think about this.
I've got this on our stack frame here,
this on our stack frame, this on the data segment,
and now this is in the stack frame below us.
Four string values sharing, with efficiency,
the backing array.
What I really want you to notice is how powerful this is.
I've got five values on the board.
One, two, three, four, five.
Yet, because we're using our value semantics here
and our pointer semantics here, think about it.
The only thing that ever would have to be on the heap,
if anything, and nothing here is on the heap,
it's a literal string.
But let's say it wasn't a literal string.
Let's say that this string was created dynamically.
The only thing that would have to be on the heap
is this one thing.
Everything else can stay on our stack.
I wanna stress that our job is to identify and balance
when do we use value semantics,
when do we use pointer semantics,
and to balance that and to try to minimize
allocations the best we can.
Look at how powerful the string is.
The string value is designed to be leveraging
value semantics, it's designed to be copied.
When something's designed to be copied
we have a great chance of keeping it on the stack.
The only thing that has to allocate to the heap is this.
What this means is is that the garbage collector
doesn't have to deal with one, two, three, four, five
values, it only has to deal with one.
There's a chance that that value also maintains
a longer lifetime.
Do you see what we're talking about here?
Your value and your pointer semantics are important,
they're critical, they're critical to everything you do.
For optimizing for correctness and performance.
We have to continue to talk about these semantics
as we continue to talk about code, and start to really
appreciate the balance that we have to have and, again,
when to use those value semantics
and when to use those pointer semantics.
It's critical.
I wanna share with you the idea that these semantics
really go deep into the language.
This is not anything that I'm making up.
I'm not making this up to make a point.
These things are real.
The value and the pointer semantics are very real.
I wanna show you that with this particular
piece of code right here.
What we're gonna be able to do is predict the output
of this code because we understand the semantics.
Remember, mechanics is about how things work underneath.
Semantics are about how things behave.
Before I run any of this code, I wanna add an extra
print line here so we can just kind of break
the output up.
What we're gonna start with is on line 12.
On line 12, we're declare that array of strings again.
Here, we do it again.
Let's just map this out on the board.
We kinda already have it here.
We can reuse this no problem.
What you notice right now is we have this array
of friends, it's the five strings, again.
The first one there is gonna be Annie, I'll just put A.
The next one over there is Betty, and then we've got
Charles, you see that, and those integers are gonna
be five, five, six, so five, five, six.
No big deal.
You're getting a sense of that.
What's important right now is this.
Look at what I do on line 13.
On line 13 I say display the string in index one,
which is Betty, so we should see Betty
when we run the program.
There it is.
Now, let's go into the four.
What you're seeing for the first time is what I would call
the value semantics form of the four range.
This time, all we're asking for is the index
position on every iteration.
Remember before, we were using the value semantics.
We're asking for the index position and a copy.
What does value semantics mean?
We're always operating on our own copy.
This is the pointer semantic form of the four range.
On the very first iteration of this loop,
we change out Betty to be Jack.
We change this out.
This now says Jack.
Great.
Then, we iterate again.
We get to index one.
We replace Jack with Jack again.
It's very inefficient.
It's not the point of the algorithm.
What's the point is that on line 18, once we're iterating
over one, I ask to display the value at index one.
We should see Jack.
If I were to run this program, we would see
in the output Betty and Jack.
Not surprising.
Exactly what we would expect.
Let's take the same algorithm, the same piece of code,
but this time let's use the value semantic form
of the four range.
We start up all over again.
We've got Annie, we've got Betty, we've got Charles,
we keep going, and I display the string
at index one again, which should be Betty.
The one thing that we've changed in this code is we're now
using the value semantic form of the four range.
Notice again, I go in, and on the first iteration
I change Betty to Jack.
No big deal.
We change it again.
Everything looks the same.
We do it a second time, and on the second time we print V.
We would assume that V would also be Jack,
because we changed it out before
we got to the second iteration.
Notice the output.
It still says Betty, and then it says Betty again.
What is going on?
Understand this.
Value semantic mean that we're operating
on our own copy of the data.
When I say this four range is using value semantics,
what it means is the four range in this form makes a copy
of the array it's iterating over.
It's not iterating over this array friends.
Before the iteration started, it made its own copy of this.
It made its own copy.
Which means that it's pointing to its own Betty and Charles.
We're not iterating over this, we're iterating over
the copy of this.
That's what value semantics are all about.
That is the behavior.
I've told you over and over again, if you don't understand
your mechanics, and more importantly don't understand
your semantics or your behavior, you cannot predict
how this code's gonna run and therefore the impact
that it's going to have.
Very, very interesting.
It's gonna be important to choose the right form of the four
range based on the data that you're operating on.
What we're gonna learn at some point is the data
drives the semantic, and whatever semantic we're supposed
to use for the data, we have to be consistent
and clean throughout our code base.
We're gonna continue to see this over and over again.
Let me show you something that I never wanna see in code.
I've tried to tell ya several times that if you
mix semantics, code becomes complicated and hard to read.
Look at this code here.
We're using the array of five strings, there it is,
we display Betty like we did before, no change.
But look, I am going back and using the value semantic
form of the four range.
However, I'm not making a copy of the array,
like I did before, this time the value semantic form
of the four range is making a copy of the array's address.
That means that when we iterate on this four range
using value semantics, we're actually doing it
over pointer semantics, which means that when
we update friends we're updating it indirectly
through the pointer, and we still see Jack.
This code is so nasty and confusing it just makes me sick.
We cannot be mixing semantics like this.
This is when code gets ugly, it gets confusing,
it gets impossible to read and impossible to predict.
I'm gonna be stressing over and over and over again
that we do not want to be mixing semantics.
We're gonna look at our data, we're gonna understand
our data, we're gonna choose our data semantic,
and then all of the code we write will follow
and respect the semantic, not the other way around.
For those of you who might be a little new to arrays,
remember that we said that the array provides
that contiguous block of memory.
Look at this code here.
I've got that same array of five friends.
We're gonna range using our value semantics,
but what I'm gonna do is ask for the address
right here at the end of every single element
and what that is in memory.
What you're gonna see is when we display the addresses here,
you'll see that we're at 150, 158, 160, 168.
You're gonna see two things in this output.
One, every one of these elements are next to each other.
They are on a contiguous block of memory.
The other thing you're gonna see is the predictable stride.
Remember, a string on the playground is eight bytes.
Two words, every word is four, four plus four is eight.
What you're seeing is an eight byte step through the array.
That is our predictable stride.
Eight bytes.
You can see it on the address.
We also talked before, remember, about alignments
so we shouldn't be confused that an eight byte value
falls on a zero eight byte alignment.
Everything that we've talked about is coming together,
but what's important, again, here is that we do
have a contiguous block of memory.
We see those addresses are next to each other.
We see the predictable stride.
Again, the hardware loves this because the prefetchers
are gonna be able to come in and make those great
predictions over the cache lines that this data's on
and we should get some better performance
because it's about how efficiently we get data
into the processor, not how fast we pump it.