- Okay, next we've gotta learn about struct types,
or really, user defined types,
the language would be useless if you couldn't define
your own data definitions, right?
Your data types.
And Go, again, is not being novel here.
Lots of languages do that,
so there is some important things, here,
I want to share with you.
Not necessarily teach you, what a user defined type is,
but, kinda, how they exist in Go.
And there's a great quote here by Martin Thompson,
who really coined the phrase, mechanical sympathy.
Look him up if you don't know him.
"Implicit conversion of types is a Halloween
"special of coding.
"Whoever thought of them deserves their own special hell."
We'll come back to this implicit conversion, here.
And Martin Thompson,
I just, I really love that quote.
All right, let's do a little code review here.
And let's start with some basics here
around our user defined types.
So, there it is in Go.
In line 10 of defining a named type called example.
It's a struct type, a composite type.
It's made up of three other existing types.
Bool, int16, and float32, there it is.
Nothing really special going on.
And you see on line 20 that I'm creating a value
of type example, named e1, setting it to its zero value.
You won't hear me use the word object in Go.
We create values in Go.
You'll continue to hear me say that.
Listen to the language and the words I'm using.
I'm going to be very consistent with them
and it's going to help us throughout the class.
So, again, on line 20,
we are constructing or creating a value of type example
named e1 set to its zero value.
But let's go back here and talk about the type declaration
because I want to focus a lot if performance matters
and these things matter,
with the idea that we optimize for correctness.
First, and at the same time, there's some interesting
things about structs that I want to share.
And if we're really going to understand cost,
I told you we have to understand allocations.
And so then, the first question I'm going to ask you is,
how much memory is allocated
for the e1 variable on line 20?
I mean, any time we construct a value, of type example,
how much memory has to be allocated?
Now, you're first guess might be,
"Well, Bill, it's a composite type."
So, you know I could just do the following.
I could say, "Okay, the first field is a flag,
"so, Bill, you know, that's a boolean, right?
"And that boolean would just then be one byte.
"And then, Bill, I've got a counter, which is int16.
"So, my counter, Bill, that's an int16,
"so that's two bytes."
Draw a little line for the two bytes.
"And then, Bill, you know we've got pi here.
"And that's a 4-byte value, Bill, you know.
"And what was that, a float32, Bill."
You know, if I add these three things up,
I'm gonna get a total of seven bytes.
"So, Bill, it seems to me that every time I construct
"a value of type example, it's going to be seven."
Now, I don't think it's a bad guess,
it's very realistic.
The problem is, is that that's going to be wrong.
And the reason that's going to be wrong is,
'cause we have the concept of alignments.
And they come from the hardware.
And alignments are there to make reading and writing
memory as efficient as possible.
As we move across the hardware in this class,
we're going to see that there are these different
boundaries, memory boundaries,
that the hardware is dealing with.
And one of these boundaries is,
you know the hardware's ability to pull data
in and out, into the hardware.
And so, what we end up with are these machine
word boundaries.
And I just want you to get a general sense
of what's happening here, with alignments,
and why they're in place.
And then, why this really isn't a 7-byte value,
but we're going to find out that it's an 8-byte value.
So if we take memory on these word boundaries.
Here we go, in these word boundaries.
Then the idea is this, the hardware has the ability
let's say within one instruction, to read and write
this word boundary.
And then, read and write this word boundary.
So the idea is that if we would allow a 2-byte value,
let's say we had a 2-byte value,
and we allowed that 2-byte value to be placed in memory,
across this boundary like this.
Let's say that's our value.
We've just now allowed it to align in memory,
in such a way that it crosses over these two boundaries.
Then it means it would take the hardware
one and two operations to read it in,
and then, one, and two operations to write it out.
I mean, not efficient, and really sets us up
for other nasty things later on.
We don't want to do that.
The hardware doesn't want to do that,
and we don't want to do that.
So, the idea around alignments is,
there's no reason to allow this value
to cross over these word boundaries here
when it can easily fit inside of a single word boundary.
So, the idea of alignments, is to say,
let's make sure that all values, wherever they are,
fall within these word boundaries.
So if I have a 2-byte value,
then they always fall inside the boundary.
Now, what that means is, is that we're going
to end up with this other concept of padding inside
of our structs.
So let's go back to our struct, for a second,
and start to understand what alignments and padding do.
So, the way this is going to work, is the following.
If you have a 1-byte value, like a bool,
well that will fit inside of a word boundary
no matter where you put it.
It could never cross over, because byte is our basic
unit of memory, read and write, that will fit inside
of any word boundary cleanly.
Well, when you get to a 2-byte value, like counter,
now as I showed before,
we could cross over these boundaries.
So what we need to do is, make sure that any 2-byte value
always aligns within a single word boundary.
How do you do that?
Well, you make sure that that value always falls
within a 2-byte address scheme.
In other words, if we look at the last digit of any address,
it always falls on a multiple of two,
in terms of its address.
It falls within, so address zero, address two,
address four, address six, address eight,
you get it.
And if we have a 4-byte value,
then it has to fall on a 4-byte alignment.
Address zero, address four, address eight,
you get it?
And if it's an 8-byte value,
like arrange64, then it's gotta fall in that full boundary
address zero, address eight.
So, what we're doing, is looking at the size of a value
to determine what its alignment is,
and then making sure that it falls properly
in memory within those boundaries.
So, if I go back and we look at our example,
let me draw that back up.
What we originally said was,
okay, here's our flag, right?
That's our one byte.
And then here was that counter,
and that was two bytes.
And then here was pi.
Right?
And that was the four bytes.
But we said this really isn't a seven byte.
One, two, three, four, five, six, seven.
It's really eight.
So let's start mapping this out by address.
If the flag byte starts at address zero,
which is quite possible,
then let's go back to counter,
counter is a 2-byte value.
Which means it has to fall on a two byte alignment.
Which means its address has to be within a two byte.
So, zero, two, four, six, eight.
Which means that counter must start on address two.
If it's going to be properly aligned.
It has to always fall within a multiple of two.
Well, that's interesting because if flag is one byte,
and falls on address zero,
what would happen to the byte at address one?
That's our padding.
This byte exists and it, unfortunately,
has to be skipped over.
It's part of the value itself,
but it's not used.
The fact that we've got bool int16,
the fact that the alignment of int16
has to be on address two,
means that this byte here, at address one,
gets skipped over, this becomes padding.
Now you can see where you've got one, two, three,
four, five, six, seven, eight.
There it is,
it's an 8-byte value.
All right, now, there's a couple things here before
we make this a little bit more complex, okay?
I always want to focus on optimizing for correctness.
Which means, I don't care about padding right now,
unless it is a problem.
The only way padding could be a problem initially,
is maybe we've got so much padding in the struct
that it's allocating much more memory than we want it to.
And therefore, maybe we've got a larger footprint in memory.
But now we're talking about micro-optimizations based
on a memory profile.
How is that possible?
Well, think abut this for a second.
What if I did this instead?
Not int16 but int32.
Now what do we have?
Now what we have is not one byte of padding,
here, but now we have three bytes of padding, here.
Because this now has to start on address four.
int32, 4-byte value, 4-byte alignment.
Now, you got three bytes of padding here.
Hey, what if this is int64?
Well, if that's int64, then what?
Now this has to start, not on four,
this has to start on eight,
which now means we've got seven bytes of padding, here.
You can see how something like this
could add up very, very quickly.
I mean, again, we're talking about micro-optimizations
and nothing we would do during coding,
but imagine we had a struct where size was important.
And though the field names would have to be different,
we did something like this.
Now you've got seven bytes of padding,
and seven bytes of padding,
and seven, we've got an extra 21 bytes of padding,
or memory, for a value of type example
that we really don't necessarily need.
Again, I want to optimize for correctness,
so if this is a better way to look and read the struct,
I'll take the 21 bytes until a memory profile
is telling us we shouldn't.
Now, if we truly want to micro-optimize the padding away,
what we need to do is,
order fields from largest to smallest.
When you order fields from largest to smallest,
what that will do,
I'm just playing around here,
just to give you that, kind of, visual clue.
What that will do is, is reduce padding all the way down
if there needs to be any, to the bottom of the struct.
Because alignments are always
going to find evenness throughout the struct.
Because there's another part of this alignment equation.
And that is a struct must also properly align
based on its largest field.
So, this struct right now,
its largest field is an 8-byte value.
So there's also an 8-byte alignment
that has to happen on the struct.
There's going to be padding at the bottom
of this structure, right here.
If I got rid of int64 and there was just int32,
now the entire struct has to fall on a multiple of four.
Now it has to be a 4-byte padding.
You see, you've got that stuff too.
But I promise you that if I see a struct
that's laid out where the fields,
from its largest size to its smallest size,
that's going to raise a flag for me.
I'm going to ask you,
why are you pre-optimizing this struct, right now?
Do you have a memory profile?
Do you have something that's telling you
that we're using up too much memory?
If the answer's no, I'm going to ask you to reorder
the fields based on
correctness.
What is a better way to read it?
Let's group things together that belong together.
Instead of going right into this idea
of optimizing for performance.
And you might say, "Well, Bill, why doesn't the language
"do this for you?"
Because the language isn't going to go behind your back
and make changes,
because then we would lose our ability to understand
the impact that we're having.
You get to control your memory layouts.
You get to make them as precise as you need them to be.
And it's not the language's job to go behind
the back and try to pre-optimize things for you, either.
There are some languages that might do that,
Go isn't going to do that.
So, the idea again, around
understanding the size of a struct,
means we have to understand alignments and padding.
And now we know that, based on the size of a field,
and this would be anywhere in memory.
You don't really feel it until we get to the struct types.
But our int16s have to fall on a 2-byte alignment.
Our float32 has to fall on a 4-byte alignment.
Our bools can kind of be put anywhere,
and that can cause places for padding.
So, we really got an 8-byte value, here.
So on line 20 when we construct our e1 variable
or value of type example,
that's eight bytes of memory cost.
Now we know that is.
And I wanna show you on line 27,
what line 27 is showing you,
is how we do construction when we don't want zero value.
And this is going to be a very common syntax in Go.
We're going to call this a literal construction.
So over here, you see literal construction
of the name type example.
We're initializing field
counter in pi.
You see the syntax with the colon.
You also see a comma at the end of every line,
Go is very much about convention over configuration.
It's very opinionated here.
Go funct is going to play a big role in your life here,
and so, these are opinions that you have.
Whether you like it or not,
this is the way it's going to be.
You can see, the use of the short variable
declaration operator during this construction, as well.
And Go is not being novel,
we're going to use the dot operator,
value dot field, value dot field.
Now, you might see this in a lot of code in Go.
You might see the use of literal construction,
to create a value set to its zero value.
There is nothing wrong with this.
I don't do it again.
Because I'd rather use var to show zero value construction,
like I showed you there, on line 20.
Line 27 is also doing zero value construction,
in this case.
But what I tried to tell you before is,
just because literal construction here,
literal empty construction here,
as struct is creating zero value,
that's not necessarily the case all the time.
So I'd rather use var.
But there's nothing wrong with that,
if that's what you want to do.
Just be consistent,
but in this code that we're going to go through,
we're going to use a literal construction
most of the time when we want initialize
to something other than zero value, 'kay?
The only time you might see empty literal construction
from me, is when we're not assigning it to a variable.
You might see it on a return.
That will be absolutely fine.
You might see it on a function call.
That will be the exception to empty literal construction.
But anytime we're assigning it to a variable,
I'm not going to do that at all.
Okay.
So, that's our basic, kind of, struct or layout,
or user defined struct.
But let's take a look at something
that I think is a little bit more interesting.
Go also has the concept of an anonymous struct.
We'll call these literals in the language.
Any time you see a compiler message about a literal value,
or literal type,
what we're really talking about is an unnamed type.
So, there are things in this language that are named
and there are things that are unnamed.
What you see on line 14,
is us declaring a variable name e1,
based on the literal struct type.
I can't give it a name because there isn't any.
We're using var so again, it's set to its zero value.
You know, readability is about consistency.
And you can start seeing our leveraging var
to have a very consistent look and feel around zero value.
And you can see on line 21,
I'm displaying the value of e1,
which will all be set to its zero value.
And here, what you're seeing,
is I'm doing literal construction.
Actually this is, again, type declaration,
so we're declaring on the fly, a struct.
It's a literal struct and has no name.
And right below it, you now see the literal construction,
'cause we're not going to set it to zero value.
You'll see a lot of this in Go.
These literal types can come in really handy
for things like, when you're doing,
let's say unmarsheling on a web API.
A piece adjacent might come in,
you have to do some unmarsheling,
you need some type information,
but it's not necessarily good to name it.
Because this only need to be used in this one place.
And we don't want to name something
we're only using in one place.
That would be pollution.
We get the ability to find these structs literally
where they're needed and use them right then and there.
It also really enhances readability since this is struct,
it is leveraged the same way.
Okay.
Let's focus on this e2 struct here,
or the e2 variable,
because I want to talk about this, as well.
Let me take this struct declaration that we have.
And let's do something very interesting.
Let's go ahead and define two types.
I'm going to have type bill
and let's create a type named alice.
Now, I'm going to hit the format buttons so we can do this.
I want you to notice something about bill and alice.
They are really, technically, identical.
They're identical and compatible.
They have the same memory layout,
the same field names,
and ideally, if I have a value of type bill or alice,
I should be able to assign them to each other
without any integrity issues, right?
There are none.
Their memory layouts are identical.
All right, so let's do the following then.
I'm going to come back here
and I'm going to create a variable of type bill.
And a variable of type alice.
Both set to their zero value.
And I'm going to go ahead and assign alice to bill.
Now the compiler will complain
if I don't use these variables.
And if the compiler, again, focusing
or the language, again, focusing on readability.
Don't declare a variable if you're not going to use it.
Doesn't help with readability,
so I'm just going to display it.
But here's the question,
we know bill and alice are identical.
We know they are compatible,
I do this assignment,
and it shouldn't be a problem
I know it, the compiler knows it.
But look at what the compiler says,
it says sorry, Bill, but you can't use A as type alice,
as type bill in the assignment.
I'm sorry, but we're not going to do this assignment.
Now, there are lots of other languages
that would've done this assignment.
And when languages do this assignment,
we call it an implicit conversion.
We're going back to what Martian Thompson
said about implicit conversion
being like a Halloween special.
Implicit conversion has caused a tremendous amount
of pain for software over the decades.
And, no, it's not going to cause us pain,
necessarily with our struct types.
Where it traditionally has caused pain,
is when B is an int, a signed int.
And A is an unsigned int.
And the compiler allows the signed and unsigned variables
to be assigned to each other.
We know now there's going to be a loss of precision,
and therefore, there's going to be data corruption.
And so, though there are languages enterically,
that have done implicit conversion
when there is data that is identical and compatible,
it's caused more problems than good.
And Go says, look, when we're working
with this concrete data, if the data is like this,
bill and alice, right, we're based on these name types,
I'm sorry, but I'm not going to do implicit conversion
because historically it's caused more problems than good.
But what if we really need to do this assignment?
This is were conversion comes in.
I love the idea of conversion in Go,
because what it does, is it shows your intention.
It's a way of telling the compiler,
look, it is my intention that A does get assigned to B.
And now there can't be any mistake.
The compiler can't say, well,
I thought you wanted to do this.
No, no, no, no.
Now the compiler's saying, if you really want to do this,
then you explicitly, not implicitly, explicitly
tell me through the conversion syntax.
Show me that this was your intent.
And there it is, we see conversion syntax.
And now, if I do this, yes,
we're going to be able to compile through.
This is a beautiful, a beautiful piece of the language.
Holding us in our code,
to integrity and holding us to responsibility.
When we're doing these types of assignments.
But I've got an interesting question for you.
We've got an e2 variable here, based on a struct
that also is identical and compatible with bill and alice.
What if I go ahead and I put e2 here?
What will the compiler do?
Will the compiler allow this, or still ask for conversion?
Well, what your going to notice is the compiler
doesn't ask for conversion here.
And now, the big difference between A and e2 is that A
was a name type, e2 is a literal type.
When the type is named,
there's going to be no implicit conversion.
You're going to have to show your intent
through conversion.
But when the value like this,
isn't named, it's a literal type,
now we have this flexibility on assignment.
Now again, it's kind of silly to do it with struct types.
Ideally, you'll see this type of assignment
with literal values, when we deal with functions.
A function in Go is a value, it's a literal value.
It's not a named type.
And we pass functions to functions all over Go.
Especially, we start talking about web APIs
and middlewares and things like that.
So, it makes you aware it's not really cumbersome.
So I love the balance that the language is giving us here.
If we're dealing with values of a name type,
there is no implicit conversion.
You've gotta use your conversion syntax,
you've gotta show your intent.
When there's, when we're dealing
with that literal type value
then we're gonna get a little bit more flexibility.
Well, it's not really implicit there
so much as we know that these things are compatible.
We allow it to happen.
And everything is safe.
And we maintain these safe levels of integrity
throughout our code.
So remember that Go's not being novel here,
the key word struct is our core way of defining
user defined data in the language.
We don't have the key word class,
like you might have seen in other languages.
We have struct.
Struct defines that data.
There's no implicit conversion
when we start talking about the concrete data name types.
We've got literal types.
And the conversion syntax comes in handy here,
when we're talking about doing that explicit
conversion, okay?
Then we also talked about the idea
that I don't want to optimize for performance here.
We always want to optimize for correctness.
If I go back to the first example,
you can see I want the fields to be laid out correctly.
In other words, readability first.
Grouping things together,
but I want you to be able to understand the cost of things.
And understanding how much memory's going to be allocated
for a particular type helps you with understanding
cost and therefore, we talked about alignments,
We talked about padding.
And we talked about that only unless you have a benchmark,
that's showing you're using up too much memory,
would I want to optimize that struct to minimize padding.
I cannot stress this enough,
because I've had too many clients come in after this lesson
and restructure everything to reduce padding to the bottom.
Please don't do that.
I'm teaching you this so we can look at cost
in terms of allocation.
Not so we can restructure everything.