- Behavior as context allows us now
to use these custom error types,
but to stay decoupled.
Let me show you that.
Let's start again with our type as context example.
Here in this function right here, this method,
I am making a call to read string
based on this interface value called reader.
We can imagine that we've abstracted a network call.
And when we make this call over the network,
we should get a line of data back
or we might get an error.
So we check to see if there's a concrete value
stored inside the error interface,
and if there is, notice what I'm doing here.
I'm doing a type as context,
and I'm checking the different types
of concrete error values that we have in the net package.
Pointers to OpError, pointer to address error,
pointer to DNSConfigError.
There's types, lots of types of errors
that can occur in the network,
and the net package tries to cover them all.
So this type of context looks pretty cool,
because I can now filter down
based on what was stored inside of the error interface
on that call, which error we got.
And you can see that I'm calling temporary
on every one of these concrete values
because you're told that if there's
a temporary method like this, you should call it.
Remember, errors are just values in Go.
They can be anything we need them to be,
both in state and behavior.
This temporary method is beautiful,
because if it's temporary, then we know
that we're still in a state of integrity, just keep going.
If it's not, if it's not temporary, we've lost integrity.
Maybe that listener has gone down, that socket has dropped.
And now we have to make sure that we can recover.
Okay, this is great, but again we're moving
from a decoupled to a coupled state.
If we make any changes to these concrete types,
this code's gonna break,
especially something from a net package
where lots of applications
are dealing with networking today.
This could create cascading changes
across large, different code bases
that we don't even own.
Okay, so what we prefer to do
is try to maintain this level of decoupling.
Let me switch over to the net package for a second.
We'll do a search golang/net,
and I'm gonna see the net package come up here.
And what I'm gonna do is search for OpError.
I want to look at OpError.
OpError is the most common type of concrete type
we might be using for error handling in the net package.
It's our most common error type.
All right, I do want you to notice something
that I didn't mention before.
There is a naming convention for a custom error type.
And that's that it ends in the word Error.
And you'll see that in custom error types.
OpError, you know, and we saw that also as well over here
with address error, DNSConfigError.
So that's a naming convention I want you to follow
when you're defining a custom error type.
Now I made all of this orange on that search,
so we're on line 414.
So let me just get rid of that.
We'll go over to 414 so we can see it a little bit better.
Now here we go.
Here is the OpError type,
and we'll try to bring it all into view.
And you can see that this is a type
that is exported with five exported fields.
You're able to inspect all of the details
you want from the networking error.
The net package has exported everything.
But if I scroll down, you're gonna notice
that this is the implementation of the error interface.
Look at that, it's using pointer semantics
for the implementation, not a surprise.
You can also see how complex it is.
The string is gonna be very complex.
Again, we don't want to have to parse this string
to figure out what's going on.
Now all of those types will have this error implementation
using the pointer semantics,
but what I want to focus on next
is the temporary method.
Let's see if we can find the temporary method for OpError.
Oh, look, there it is.
Look how complex this method is.
Thank God they implemented this,
because look at all of the type assertions.
I mean, I wouldn't have been able to figure this out.
So this temporary method returning to a fault
is brilliant, and it's really, really important,
because it's gonna allow us to simplify
whether or not there is or is not an integrity issue
or something as complex as a networking issue.
But I want us to go back to this code for a second,
because if we go back here,
you should see this pattern
where we're type asserting
to the concrete to call temporary,
type asserting to the concrete to call temporary,
to call temporary.
Really, the common behavior here is temporary.
This is all I care about.
Technically, I almost don't even care
what the concrete type is if there's a temporary method.
So let's apply the ideas of behavior as context
and clean up this code.
We can start off with the idea of this,
let's take a look at this.
Look at this code here.
This becomes very interesting to me.
Look how the net package has an interface called temporary.
They have inside this code base
defined a level of decoupling
through this temporary behavior.
But also notice that it's unexported.
What does that mean?
It means that we don't have access
to this interface type from our code.
But it would have been nice to have access to it,
because we could have leveraged it for our own decoupling.
But guess what, it would have been a mistake
to have this exported,
because the only piece of code
that needs to implement temporary
is inside the net package.
They own the different custom error types.
They have the need to be coupled.
We're not implementing temporary
on behalf of the net package.
It's for internal use.
So you remember that Go's
about convention over configuration.
There's nothing stopping us from defining
our own temporary interface, is there?
No, and isn't it true that any concrete type
that has the temporary method inside the net package
also implements or satisfies our temporary interface?
It absolutely does.
Again, the beauty of Go is static code analysis, right?
The beauty of Go, convention over configuration,
is we now implement our own interface
that the concrete values inside the net package
also implement, right, that data,
the values and the pointers.
So look at what I do.
We're gonna go from these three cases, right,
the three cases on the error
where we're going from decoupling
to that concrete,
and I'm gonna move it all down into one case.
There it is.
This time when we do the type assertion,
I'm not gonna ask
what the concrete data is inside of E.
What I'm gonna ask is does that concrete data
also implement my temporary interface?
I am now staying within a decoupled state.
And when I do that now,
I go to just one case, that's it.
I don't care what the concrete data is.
All I care about is that it implements temporary.
And temporary will tell me whether or not
I have an integrity issue.
This is beautiful, right?
The fact that an error value can have not just state
but behavior, can allow your call or your user
to maintain decoupling in error handling,
even when we're using these custom error types.
So here's a general rule that I want you to follow.
If your custom error type can have
any one of these four methods,
it doesn't have to have all of them,
just one, then I want the custom error type
to be defined as unexported with unexported fields,
because if it's unexported with unexported fields,
you're forcing that your user can never, ever go
from a decoupled state to that concrete, right?
We can't type assert to the concrete.
You're really helping them out,
even though they might feel like they're being restricted.
Here are the four methods, okay?
Temporary, time out, not found,
and not authorized, that's it.
There isn't really another one.
If you can add any one of these four,
we can maintain decoupling with custom error types
by making the custom error type unexported.
Remember, temporary covers a tremendous amount.
Temporary is really kind of this blanket statement
that you have an integrity issue or you don't.
And if people have complained
that you've made the custom error type unexported
and you have a valid temporary method,
just tell them, look, we're not going to export the type,
even if you feel like you need
to inspect it directly yourself.
Let's fix temporary to be more accurate.
Where it's brilliant is
we can fix temporary to be more accurate
without creating a cascading code change,
because this code doesn't change
if we change the implementation of temporary.
If anything, this code gets better
because it's more accurate.
Now if you have a custom error type
like the JSON package did,
you remember those custom error types
that we talked about before for the JSON package,
time out, temporary, not found, not authorized.
They don't work for those types.
Those types have to be exported.
The user may need to move
from decoupling back to the concrete.
It's a scary situation, but that is what it is.
Most of the time, that temporary method
is going to work for you.
So I don't have a problem with the custom error types
as soon as the error variables don't work,
but I'd love to make sure that those custom error types
are unexported with unexported fields,
and that we apply method sets of behavior
that the user can bind to
to maintain that their error handling
is done from a decoupling state.