- So, type as context, now, applies when
we're gonna need our own custom error type.
I mean, think of networking issues, right?
I mean, how many problems can happen on the network?
Just one, right?
We all wish.
There's tons of things that happen on a network,
so an error variable isn't gonna be enough
to say there was a network issue.
We're gonna need to know what was that network issue?
And the more detail we can get about the network issue,
the more we'll get to know whether
we can recover from it or not.
So, type as context becomes very important
when we start dealing with custom error types.
Now, what I wanna show you are two custom error types
from the JSON package.
This one is called Unmarshal Type Error.
Notice it's an unexported type
with two exported fields, value and type.
I want you to notice on line 22, as well,
that we've implemented, or at least the standard library's
implemented the error interface, using pointer semantics.
Again, pointer semantics are going to be the
default, there will be exception to e's value.
I'm gonna come back and show you that.
Also, look at the implementation
of the error interface, here.
Remember that the implementation is for logging, right?
It allows the concrete value to be an error value,
but it also is for logging.
And one of the things I want you to always look for
is if every field in the custom error type
is being used in the logging message.
If we're missing a field from the logging message,
I would question the relevance of that field.
You wanted to create a custom error type,
you added this field 'cause you thought
it was gonna help the user, but it wasn't enough
to log information about it?
Ehhhh, you know, that could be a smell.
So, we're gonna get an error of this type,
when you don't pass a pointer into an Unmarshal call,
because Unmarshalling and decoding requires,
you know, pointer semantics, shared access.
This custom error type is used when we have
an Unmarshalling error in the JSON package.
That's when you have a document that says,
"This field is a string, but you defined it as
an integer in your, you know, your concrete type."
Then we're gonna get the Unmarshal type error.
That's what this error is for, right?
That's why we have value and type,
but here's the second custom error type
that we have in the JSON package.
It's called Invalid Unmarshal Error.
Look at this, when you don't pass the address
of something into the Unmarshal call.
Again, I want you to notice that we're using
pointer semantics, here, for the implementation,
and I want you to notice that the
message coming out's a little bit more complex.
Okay, we've got these two user-defined, right,
custom error types, part of
the standard library's JSON package.
And I want to show you this function called Unmarshal.
I've taken a snippet of code from the JSON package
for the Unmarshal call.
The one thing I wanna show you here is the second parameter.
The second parameter's based on the empty interface.
The empty interface tells us nothing,
because any piece of data, any value or pointer,
satisfies it, because it doesn't have to have any behavior.
I want to be very careful
when we're using the empty interface.
Don't use it to write generic APIs.
We should be using the empty interface
when we have to pass data around,
where that data can be hidden without problems,
or in this case, where we're gonna use the reflect package,
because what we wanna do is at Runtime,
or dynamically inspect the concrete data.
This is great if you wanna do model validation.
Reflect packages are fantastic for, like,
model validation, or like what we're doing here
with some unmarshalling.
Now, if you notice, we're using
the reflect package, ValueOf function call.
This is how we always start a reflection,
and we're basically saying,
is the concrete data inside of V?
Is it a pointer, and is it nil?
Really, we're saying, if it's not a pointer,
or it's nil, then we're gonna return
this error value, right here.
Again, we've implemented the interface
with pointer semantics we always have to share.
So, we're gonna either store an invalid
unmarshal error pointer in the error interface value,
or we're going to share this unmarshal type error
inside the error interface value.
Okay, and we're going to be processing from
a decoupled state v interface,
but now the question is, what we wanna know is,
what type of value is stored?
What type?
Type is context.
Was it a pointer to this type or a pointer to this type?
Now, Go gives us a speckle, special, that's funny.
Special mechanism to do this type as context,
and it happens on the switch.
So look at what we do.
We call unmarshal, we get back the error interface value,
we ask, is there a concrete type stored inside
the error interface value?
Oh, there is?
Then we do a type assertion on the switch.
Notice that the type assertion is using the keyword "type".
It is the only place where we could do
a type assertion on the keyword "type", inside of a switch.
And what this is letting us do is type as context.
What it's saying is, okay, type assert,
what is inside of the error interface value?
And if it happens to be a pointer of this concrete type,
then let's execute this code,
and e will be a copy of that, right?
There's always going to be a copy on the type assertion,
the copy of the pointer.
Or, if e is a pointer of this type,
then that's what we have.
Let's execute this case and e will be that.
Type as context, it's really cool, right?
The conditional logic is based on
what type of value is stored,
and thanks to the type assertion,
we get a copy of what was stored,
and now we can process our error handling
from that, you know, concrete place.
But, for me, that is also a problem.
We wanna maintain, to the extent we can,
error handling from a decoupled state,
because once we switch from the decoupled state
to the concrete, then any improvements we make
to error handling against those concrete types,
could cause a cascading effect of change
throughout our program.
So, even though this is super cool, right?
This idea of type as context for error handling,
it's kind of dangerous, because we're setting ourselves up
for some potential cascading changes,
throughout the code base.
But type as context can be very powerful
when you need to move concrete data
across program boundaries, where both sides
need to work with the concrete data, itself.
Then this idea of type as context can come in
really, really handy to move data across program boundaries,
maintaining levels of decoupling.
I'm just afraid, when we're using it here,
with error handling, 'cause I'd really rather
be processing the error interface value directly,
or at least maintain levels of decoupling.
So then, how can we maintain this idea
of custom error types without moving into the concrete?
This is where we're gonna start talking about
this idea of behavior as context.