- Now we're gonna talk about constants
and constants are really a fascinating part
of the language for me, because of the way
they're implemented, and again remember
Go is all about these costs and these trade offs
so let's talk about the mechanics around
constants I find them really interesting
and I'll show you some of the cooler aspects
of constants and how they work in the language.
You'll end up using a few of them
in almost every program that you write.
Alright, so one of the really interesting things
about constants, for me is that
they only exist at compile time.
Constants
are
something that really have a much different feel
and flavor to than your common variables.
Look at this right here on line 15 and 16.
I am declaring two constants, but these
are constants of a kind, and that's usually
thing I've only seen it, seen it like this
in this language.
Most of the time we think about constants
as being read-only variables,
and it's absolutely not the case in Go.
So, when we look at constants, they can come
into this flavor that you see here, that is of a kind.
Notice that there's no type information
during the declaration of these constants on line 15 or 16.
And again, it's based on the value on the right hand side
of this assignment, whether they will be of kind
integer or kind float.
Right below there, just to show you the contrast,
those are constants of a type, of type int
and of type float64.
Now, the big difference between constants of a kind
and of a type, are that constants of a kind
can be implicitly converted by the compiler.
This gets really interesting, remember we talked about
during the struct types, that there's no
implicit conversion of that concrete data,
but when we start talking about constants of a kind,
that goes out the door.
Now, constants of a type still hold true
that once something is of a type,
then that's it, the implicit conversion goes away.
But constants of a kind, there we get some flexibility
and it's a very powerful mechanism in Go,
I'll show you from code readability.
So there we go, ui, uf, two constants of a kind,
kind int, kind float, and ti and tf,
constants of type int and type float.
Now, if we continue to talk about
the idea of these kinds and what happens there,
there's something very special in Go,
and it's called Kind Promotion.
And this Kind Promotion will tell you
how things promote so floats promote over ints
and types always promote over kind.
So, on line 24, and I had to comment that out
because once I made that constant of a type Uint8
it was bound by the laws of type
and you could only put a value
within the scope of a one biter 8-bit integer.
But what's also really interesting about
constants of a kind, because they're not type based,
they're technically not limited by precision,
and if you look at the specification,
if you look at the specification around constants of a kind,
you'll notice that the specification says
that a constant has to be at least of 256 bits
of precision.
That makes constants of a kind almost like,
or the compiler when it comes to constants of a kind
like a high precision calculator.
Now, if you look at on line 30
I want to show you some very interesting things here.
What we're doing is we're multiplying the value of three
by the value of 0.333.
On the surface we see that we have an integer
and we have a floating point.
Remember, you can't have an implicit conversion
between two different types.
We could argue that these are two different types
an int and a float, but they're constants,
literal values in Go are constants,
they're constants of a kind,
they're technically unnamed constants of a kind.
And so through this idea of Kind Promotion,
that kind integer three will be promoted up to kind float.
We now have like kind on both sides of that multiplication
and var at this point is variable ends up being
a variable of that float64 type.
Very, very interesting, powerful stuff
where we were able to work with literal values
from a kind perspective, we don't have to worry about
doing any explicit conversions
and then this promotion takes care of making sure
that everything is right.
But remember that we're dealing with 256 of precision,
256 bits of precision when we're dealing with
constants of a kind, and when we now convert us back
to a variable where you see answer,
we're moving that down to a 64 bit level of precision.
There will be some precision loss,
but again, remember that floating points
already are already not precise, right,
IEEE754 binary decimals.
Look on line 33, this time we're doing division
between basically a kind int1 and a kind float3.0.
Again, we get the same thing,
that integer promotes up to be of kind float,
and we actually what we would say is
have the exact representation of 1/3.
Sometimes, in the old days we used to call
what we considered constants of a kind to be exact,
they were like these very exact numbers
because we had such high levels of precision
that they were exact.
So, we would look at third truly as 1/3
even though, eventually it turns to 56 bits
but there be some precision loss.
But then on line 36 you could see that
there's no promotion going on.
One is of kind int, three is of kind int,
we do the division, we end up with zero,
because that's what's that's gonna be,
everything stays within the kind integer.
Now, I also told you that we can promote from kind to type
and look at this on line 40.
We're creating a constant of type int8
reside in the value of one,
so it's still a constant, still only exists at compile time,
but it's bound by the int8 type.
And now this is where the power of kind and type comes in.
Look on line 41.
We're taking the literal value of kind int2
multiplying it by the constant of int8,
and now, again, through promotion,
that kind 2 value promotes up to int8,
it now promotes to be a type constant.
Again, we've got to have those like types
across, you know, that expression,
and two ends up being a constant of type int8.
So these are the mechanics of constants of a kind,
constants of a type, and the promotion,
and I want to show you how powerful this stuff is.
But before we do, I want to show you as well
that this idea that constants only exist at compile time
and they have these high precisions,
like a minimum of 256 bits of precision is real.
Now, it's hard to show you this is real when things
only exist at compile time and we have to run a program
at run time, but I want you to look at these constants
as I've declared.
This constant is called maxInt
and it represents the maximum signed integer you could have
for a 64 bit precision, there it is.
But look on line 14, I'm storing a much larger number there,
much larger than our 64 bit variables could ever hold.
Now these are integers, but again,
we have 256 bits of precision.
If we didn't have these higher levels of precision,
we really wouldn't be able to do this assignment at all.
So what I'm really gonna show you is, as I run this,
the compiler accepts line 14 even though
this is a much larger number
we could ever put in a variable.
I mean look, if I take this and say
let's create a constant of type int64 with the same number,
this won't compile, that number is much larger
than a 64 bit can hold
and I've still only scratched the surface
on what I could store in this constant.
I mean I can keep going, I've got 256 bits
of precision here.
So, the compiler is really like this high precision
calculator when it comes to constants.
Let me show you more practical use of constants
and how this kind and type promotion
really makes our life a little bit better.
Look at this constant tier of type Duration.
This is a second way to declare a type here in Go,
what I would say is, the name type Duration is based,
based on int64.
This is not an alias, we have really two distinct
named types here.
We're just using int64 as our base information
or our base memory model for Duration.
And I only want to do these types of things
when the new type has its own representation and meaning,
and it does here in the time package.
Duration represents time.
Doesn't represent an int64, it represents
nanoseconds of time.
I want you to look at these constants
'cause this is a real practical
and clean way of how this idea of type
and kind work together in constants.
So we create a type constant named Nanosecond,
which represents one nanosecond of time,
it's based on a duration, and then if you go to line 16,
look at what we're doing.
We're taking the literal value of kind int1000,
this is a constant, an unnamed constant of kind int,
we're multiplying it by our type constant Nanosecond,
this gets promoted to be of type Duration,
and we end up with another constant of type Duration.
How amazing is this?
This looks simple on the surface,
but the engineering behind this is really, really powerful.
In fact, all of these constants end up being
of type Duration, and represent the proper int64 value
for these incremental units of time.
And more gets into this.
I mean look at this Add method.
We will talk about methods later,
but I want to focus on the parameter here.
Notice how Add takes a value of type Duration, right.
And we might do this, we might say
I only want to pass Duration values into Add.
Well, one of the interesting things about
constants of a kind, again, is that they can be
implicitly converted when there's compatibility.
This is one of the first things that really messed with me
when I started learning Go.
I mean look at this, on line 39
I called the now function of the time package
and I create a variable which gives me the current time.
Now when I call Add on line 42,
notice I'm passing the literal,
unnamed constant of kind int
minus five.
You might say, whoa, whoa, whoa, whoa, whoa,
that's not of type Duration, what's going on,
why would the compiler accept that?
This is one of those trade offs
because values of a kind, right,
constants of a kind, can be implicitly converted,
we can pass that in, it would represent
minus five nanoseconds, and this would really mess you up.
This is one of the reasons why we can't have
enumerations in Go.
We don't want to create types as aliases
to get compiler protection when they're based
on let's say, those built-in types,
which is where constants are allowed to be.
Constants can only be based on the built-in types
because again, they only exist at compile time.
But look at this, this is something that we're gonna do
a lot in Go, the timeout constant, right.
Five is of kind int, there it is,
time.Second is of type Duration,
it's still a constant, but it's a constant of type Duration,
that five int kind promotes up to Duration
and timeout becomes a constant of type Duration,
and that constant of type Duration
supports the API very, very cleanly.
This is really nice and great API design
in leveraging constants of a type
and constants of a kind.
Very, very powerful stuff.
The one thing you could never do is what we see here.
Notice that I'm converting our integer, right,
our constant minus five of kind int
into a variable, or a value of type int64.
I cannot pass minus five to Add
because that is a value based on a name type int64
and it only wants Duration.
So those constants of a kind can be
implicitly converted through those calls,
any value that's based on a type,
we've got to have like types everywhere, everywhere.
Very, very interesting and powerful stuff around constants.
Now there's one last thing about constants
that you'll probably want to use as you're writing
production level code.
This is the idea of the keyword iota, I-O-T-A.
It's really interesting and it can be
very, very confusing at first.
If you notice that I'm using the concept of a block,
we can do this with vars and types
and we do it with imports all the time.
Notice that we're using the parentheses here
to block a bunch of, in this case, constants,
so I don't have to write const again over and over again.
And this iota stuff works very, very well
when it comes to constants.
Now, when we start a block of constants like we did here,
iota starts at the value of zero.
And then every time we use it inside the block,
it will increment by one.
The output of this right here, if I just run it,
you'll see here that it's zero, one, two.
Zero, one, two.
We get this incrementing feature for free
inside the constant block.
Now, most likely, because we want the incrementing feature,
we're gonna do it like this.
We only have to assign iota one time
to the very first constant in the block
and we will automatically, for free, get the incremental.
Again, on the output here, we see here is
two, zero, one, two, same exact output,
the thing is I didn't have to repeat myself
and keep putting iota there.
Now, this is something also that you'll see a lot of.
Maybe we don't want to start at zero,
we want to start at one.
So the first constant, we take we know
what the starting point for iota is zero,
incremented by one initially on that first constant,
and then we'll start at one.
One, two, and three.
And, we do this in the log package,
and sometimes what you want is a set of constants that
bit precision in some sort of flagging system,
like we do at login.
Notice this time what we're doing
is we're using the iota value to shift bits to the left one
and that gives us one, two, four, eight, 16, 32.
You might see that a lot with constants as well.
So, this iota is a very powerful mechanism
if you're creating a set of constants
that are gonna have some unique IDs
and it just kind of let's the language
set all that up for you.
So constants are really, really powerful in Go.
Remember now that there's two types of constants,
constants of a kind and constants of a type.
Your literal values in Go are constants of a kind,
they're unnamed constants, constants of a kind
can be implicitly converted by the compiler,
which means that you can't really have enumerations in Go.
You're not gonna get those compiler protections.
Once a compiler is based on a type,
then the full laws of type are gonna restrict its ability
to be anything other than its particular precision.
Remember, constants of a kind can have up to
256 bits of precision, we got really like a
high precision calculator in Go.
And then again, those literal values
are all constants of a kind.