Go's "multiple return values" feature, can be used for several purposes. Among them for failure reporting to the function caller. Go has two conventions for failure reporting, but currently, no clear convention for which to use when. I've encountered different programmers that prefer different choices in different cases. In this article, we will discuss the two, and try to make the process of choosing our next function signature more conscious.
Let's introduce the two failure reporting conventions.
It is common to see a function signature that returns an error, like os.Chdir(dir string) error
,
or a pair of value and error, like os.Open(name string) (*File, error)
.
A function that returns such a value, actually says: "I can't handle what just happened, you handle it".
Usually, when a called function has such a signature, a gopher will check the error value
and handle it.
f, err := os.Open("my-file.txt")
if err != nil {
// handle error
}
// use f
This is one of the nicest things in Go! The error handling is explicit, clear, and done in an "indent error flow". A nice article about how a gopher should handle errors can be found in Dave Chaney's blog. My favorite part in the article is "Only handle errors once".
Another way to report the caller about a failure, is to return a bool, or a pair of a value and a bool, Like
http.ParseHTTPVersion(vers string) (major, minor int, ok bool)
.
By convention, such a function will use named return values.
The last returned value in the function signature will be called ok
of type bool
and will indicate
if the function failed.
When a called function has such signature, a gopher will check the ok
value after the call:
minor, major, ok := http.ParseHTTPVersion("HTTP/1.1")
if !ok {
// handle failure
}
// use minor and major
Here again, the handling of the failure is very clear, and is done in an indented block as well.
I am about to write a new function, what will be its signature?
If the function is guaranteed to complete successfully, and this is either by having no way to fail, or by handling failures in it, there is no reason to return either an error or an OK bool. But what if the function can fail, and its scope can't deal with that failure? as previously discussed, we have two options here. Let's consider them carefully.
When my function is very simple, I can use the OK bool. It can fail, but when it
fails, the caller knows why. Good examples from the go language are not function calls - but languge syntax, which use the OK bool
convention:
- The interface assertion:
val, ok := foo.(Bar)
, if theok
value isfalse
, it is clear thatfoo
is not of typeBar
, we don't have any other option here. - The map's key testing:
val, ok := m[key]
, ifok
isfalse
, it is clear to the caller that the key is not in the map.
An example that demonstrates the downside of such signature is the
implementation of the ParseHTTPVersion function.
The function returns a false
value in 4 places, so at least 4 different inputs can result in a false
value
due to different reasons. The caller has no information about the failure, just the fact that it happened.
On the other hand, when opening a file, there could be many reasons for failure, the file might not exist,
the user might not have permissions for the file, and so on,
and this is why os.Open
returns a file object and an error
pair.
In case of a failure, the returned object in the error
interface will contain the reason, and the caller
can understand how he could handle the failure.
We can come to the straightforward conclusion that returning an error
is more informative.
A value that implements the error
interface, can be called with its Error()
function
and a string that represents the error will appear to the perplexed gopher.
Additionally, we can say that the group of functions that OK bool
can be used
as the failure indicator is a subset of the group of functions that error
can be used as their
failure indicator. In any case where an OK bool is chosen, it can be replaced with an error
interface.
But not the other way around, if we replace an error
with OK bool
, we loose information,
and the caller could not handle all the cases he could before.
Popularity: A simple grep on the standard library (not too accurate) shows that there are
about 7991 functions that use the error
interface as a return argument,
and about 236 functions that use a last returned argument named ok
of type bool
.
Your functions are your APIs. And as we all know, we shall not break them. Even the breaking of internal functions
that are used widely in your package can result in painful code refactoring. The function's API consists of its name,
its arguments, and the return values.
Assuming you are writing a very simple function, that might fail, and because it is so simple, you ought to
give it an OK bool
return value. A very simple indication of success that makes elegant if
-statements after
the function call.
In most cases that would be a wise decision, in terms of APIs.
But in some "border" cases of functions that are not as simple as they first seem, more logic is needed
to be injected into the function, and more failure cases are introduced. Still, at every failure case it will
return false
, assuming we are not willing to break API.
The calling function, that might log the failure, will have no choice, but to notify that error: 'foo' failed
.
which will result in a program that is much harder to debug. If the calling function is trying to handle the failure,
it might handle some cases in a wrong way, just because it didn't get all the information.
Using the error
interface, is much more flexible. Since it is an interface, you can return anything that implements
it, and any caller will know what to do, and will have more information about what exactly failed in your function.
error
is an interface, which has a performance penalty over a bool
variable. This performance
penalty can be considered minor, depending on the function purpose. This might be a good reason to
prefer an OK bool
return type. An interesting debate about interfaces performance can be found
here.
When some functions return error
as a failure indicator, and some return an OK bool
,
a function that is a bit complex and uses several function calls can look a bit ugly:
a, err := foo()
if err != nil {
log.Println("foo failed:", err)
}
b, ok := bar()
if !ok {
log.Println("bar failed")
}
err := baz()
if err != nil {
log.Println("baz failed:", err)
}
In this article, I propose making conscious decisions in selecting functions failure reporting type. It would be awesome to have good guidelines or conventions for the failure indicator right return type.
As I can see it, and in my opinion, the default choice for failure indication should be the error
interface.
It is explicit, flexible, and can be used everywhere - even where you might want to use the OK bool
type.
Guidelines to prefer an OK bool
over error
:
- It is very very very clear what
ok == false
means. - It is performance critical to prefer
bool
overerror
.
I would love to hear your opinions about this subject. If you agree, disagree, or have in mind more aspects of this problem, please comment below.
If you find this interesting, please, ✎ comment below or ★ star above ☺
For more stuff: gist.github.com/posener.
Cheers, Eyal
@microwaves, in your case, the returned value is not an indication of failure, as far as I can understand.
Usually, in those cases, when a function has several return values, and it can be unclear, I use named return values.
For example, your function will be much clearer with this signature: