Some functions always succeed at their task.
For example, strings.Contains
and strconv.FormatBool
have well-defined results
for all possible argument values and cannot fail—barring catastrophic
and unpredictable scenarios like running out of memory, where the
symptom is far from the cause and from which there’s little hope
of recovery.
Other functions always succeed so long as their preconditions are met.
For example, the time.Date
function always constructs
a time.Time
from its components—year, month, and so on—unless
the last argument (the time zone) is nil
, in which case it panics.
This panic is a sure sign of a bug in the calling code and should
never happen in a well-written program.
For many other functions, even in a well-written program, success is not assured because it depends on factors beyond the programmer’s control. Any function that does I/O, for example, must confront the possibility of error, and only a naïve programmer believes a simple read or write cannot fail. Indeed, it’s when the most reliable operations fail unexpectedly that we most need to know why.
Errors are thus an important part of a package’s API or an application’s user interface, and failure is just one of several expected behaviors. This is the approach Go takes to error handling.
A function for
which failure is an expected behavior returns an additional result,
conventionally the last one. If the failure has only one possible
cause, the result is a boolean, usually called ok
, as in
this example of a cache lookup that always succeeds unless there was
no entry for that key:
value, ok := cache.Lookup(key) if !ok { // ...cache[key] does not exist... }
More often, and especially for I/O, the failure may have a variety of
causes for which the caller will need an explanation. In such cases, the
type of the additional result is error
.
The built-in type error
is an interface type.
We’ll see more of what this means and its implications for error
handling in Chapter 7.
For now it’s enough to know that an error
may be nil or
non-nil, that nil implies success and non-nil implies failure, and
that a non-nil error
has an error message string which
we can obtain by calling its Error
method
or print by calling fmt.Println(err)
or fmt.Printf("%v", err)
.
Usually when a function returns a non-nil error, its other results are
undefined and should be ignored.
However, a few functions may return partial results in error cases.
For example, if an error occurs while reading from a
file, a call to Read
returns the number
of bytes it was able to read and an
error
value describing the problem.
For correct behavior, some callers may need to process the incomplete
data before handling the error, so it is important that such
functions clearly document their results.
Go’s approach sets it apart from many other languages in which failures are reported using exceptions, not ordinary values. Although Go does have an exception mechanism of sorts, as we will see in Section 5.9, it is used only for reporting truly unexpected errors that indicate a bug, not the routine errors that a robust program should be built to expect.
The reason for this design is that exceptions tend to entangle the description of an error with the control flow required to handle it, often leading to an undesirable outcome: routine errors are reported to the end user in the form of an incomprehensible stack trace, full of information about the structure of the program but lacking intelligible context about what went wrong.
By contrast, Go programs use ordinary control-flow
mechanisms like if
and return
to respond to errors.
This style undeniably demands that more attention be paid to
error-handling logic, but that is precisely the point.
When a function call returns an error, it’s the caller’s responsibility to check it and take appropriate action. Depending on the situation, there may be a number of possibilities. Let’s take a look at five of them.
First, and most common, is to propagate the error, so that a
failure in a subroutine becomes a failure of the calling routine.
We saw examples of this in the findLinks
function
of Section 5.3.
If the call to http.Get
fails, findLinks
returns
the HTTP error to the caller without further ado:
resp, err := http.Get(url) if err != nil { return nil, err }
In contrast, if the call to html.Parse
fails,
findLinks
does not return the HTML parser’s error directly
because it lacks two crucial pieces of information: that the
error occurred in the parser, and the URL of the document that was
being parsed.
In this case, findLinks
constructs a new error message that
includes both pieces of information as well as the underlying parse error:
doc, err := html.Parse(resp.Body) resp.Body.Close() if err != nil { return nil, fmt.Errorf("parsing %s as HTML: %v", url, err) }
The fmt.Errorf
function formats an error message using
fmt.Sprintf
and returns a new error
value.
We use it to build descriptive errors by successively prefixing
additional context information to the original error message.
When the error is ultimately handled by the program’s main
function, it should provide a clear causal chain from the root problem
to the overall failure, reminiscent of a NASA accident investigation:
genesis: crashed: no parachute: G-switch failed: bad relay orientation
Because error messages are frequently chained together, message
strings should not be capitalized and newlines should be avoided.
The resulting errors may be long, but they will be self-contained when
found by tools like grep
.
When designing error messages, be deliberate, so that each one is a meaningful description of the problem with sufficient and relevant detail, and be consistent, so that errors returned by the same function or by a group of functions in the same package are similar in form and can be dealt with in the same way.
For example, the os
package guarantees that every error returned
by a file operation, such as os.Open
or the Read
,
Write
, or Close
methods of an open file, describes not
just the nature of the failure (permission denied, no such directory,
and so on) but also the name of the file, so the caller needn’t
include this information in the error message it constructs.
In general, the call f(x)
is responsible for reporting the
attempted operation f
and the argument value x
as they
relate to the context of the error.
The caller is responsible for adding further information that it
has but the call f(x)
does not, such as the URL in the call to
html.Parse
above.
Let’s move on to the second strategy for handling errors. For errors that represent transient or unpredictable problems, it may make sense to retry the failed operation, possibly with a delay between tries, and perhaps with a limit on the number of attempts or the time spent trying before giving up entirely.
// WaitForServer attempts to contact the server of a URL. // It tries for one minute using exponential back-off. // It reports an error if all attempts fail. func WaitForServer(url string) error { const timeout = 1 * time.Minute deadline := time.Now().Add(timeout) for tries := 0; time.Now().Before(deadline); tries++ { _, err := http.Head(url) if err == nil { return nil // success } log.Printf("server not responding (%s); retrying...", err) time.Sleep(time.Second << uint(tries)) // exponential back-off } return fmt.Errorf("server %s failed to respond after %s", url, timeout) }
Third, if progress is impossible, the caller can print the error and stop the program gracefully, but this course of action should generally be reserved for the main package of a program. Library functions should usually propagate errors to the caller, unless the error is a sign of an internal inconsistency—that is, a bug.
// (In function main.) if err := WaitForServer(url); err != nil { fmt.Fprintf(os.Stderr, "Site is down: %v ", err) os.Exit(1) }
A more convenient way to achieve the same effect is to call
log.Fatalf
.
As with all the log
functions, by default it prefixes the time
and date to the error message.
if err := WaitForServer(url); err != nil { log.Fatalf("Site is down: %v ", err) }
The default format is helpful in a long-running server, but less so for an interactive tool:
2006/01/02 15:04:05 Site is down: no such domain: bad.gopl.io
For a more attractive output, we can set the prefix used by the
log
package to the name of the command, and suppress the
display of the date and time:
log.SetPrefix("wait: ") log.SetFlags(0)
Fourth, in some cases, it’s sufficient just to log the error and then
continue, perhaps with reduced functionality.
Again there’s a choice between using the log
package,
which adds the usual prefix:
if err := Ping(); err != nil { log.Printf("ping failed: %v; networking disabled", err) }
and printing directly to the standard error stream:
if err := Ping(); err != nil { fmt.Fprintf(os.Stderr, "ping failed: %v; networking disabled ", err) }
(All log
functions append a newline if one is not already present.)
And fifth and finally, in rare cases we can safely ignore an error entirely:
dir, err := ioutil.TempDir("", "scratch") if err != nil { return fmt.Errorf("failed to create temp dir: %v", err) } // ...use temp dir... os.RemoveAll(dir) // ignore errors; $TMPDIR is cleaned periodically
The call to os.RemoveAll
may fail, but the program ignores it
because the operating system periodically cleans out the temporary
directory.
In this case, discarding the error was intentional, but the program
logic would be the same had we forgotten to deal with it.
Get into the habit of considering errors after every function call,
and when you deliberately ignore one, document your intention clearly.
Error handling in Go has a particular rhythm.
After checking an error, failure is usually dealt with before success.
If failure causes the function to return, the logic for success is not
indented within an else
block but follows at the outer level.
Functions tend to exhibit a common structure, with a series of
initial checks to reject errors, followed by the substance of the
function at the end, minimally indented.
Usually, the variety of errors that a function may return is
interesting to the end user but not to the intervening program logic.
On occasion, however, a program must take different actions depending
on the kind of error that has occurred.
Consider an attempt to read n bytes of data from a file. If
n is chosen to be the length of the file,
any error represents a failure. On the other hand, if the caller
repeatedly tries to read fixed-size chunks until the file is
exhausted, the caller must respond differently to an end-of-file condition
than it does to all other errors.
For this reason, the io
package guarantees that any read
failure caused by an end-of-file condition is always reported by a
distinguished error, io.EOF
, which is defined as follows:
package io import "errors" // EOF is the error returned by Read when no more input is available. var EOF = errors.New("EOF")
The caller can detect this condition using a simple comparison, as
in the loop below, which reads runes from the standard input.
(The charcount
program in Section 4.3 provides a more
complete example.)
in := bufio.NewReader(os.Stdin) for { r, _, err := in.ReadRune() if err == io.EOF { break // finished reading } if err != nil { return fmt.Errorf("read failed: %v", err) } // ...use r... }
Since in an end-of-file condition there is no information to report
besides the fact of it, io.EOF
has a fixed error message,
"EOF"
.
For other errors, we may need to report both the quality and quantity
of the error, so to speak, so a fixed error value will not do.
In Section 7.11, we’ll present a more systematic
way to distinguish certain error values from others.
34.204.196.206