I’ve gotten a fair amount of mail about a recent blog post titled “Why I’m not leaving Python for Go” [1], in which the author says that Go is great except that “errors are handled in return values”. I thought it would help to write a little about why this is.
In Go, the established convention is that functions return errors; they don’t panic. If a file isn’t found, os.Open returns an error; it doesn’t panic. If you write to a broken network connection, the net.Conn’s Write method returns an error; it doesn’t panic. These conditions are expected in those kinds of programs. The operation is likely to fail, and you knew that going in, because the API designer made it clear by including an error result.
On the other hand, there are some operations that are incredibly unlikely to fail, and the context is such that there is no way to signal the failure, no way to proceed. These are what panic is for. The canonical example is that if a program evaluates x[j] but j is out of range, the code panics. An unexpected panic like this is a serious bug in the program and by default kills it. Unfortunately, this makes it difficult to write robust, defensive servers that can, for example, cope with the occasional buggy HTTP request handler while keeping the rest of the server up and running. To address that, we introduced recover, which allows a goroutine to recover from a panic some number of call frames below. However, the cost of a panic is the loss of at least one call frame from the stack. We did this intentionally. To quote from the original mail: “This proposal differs from the usual model of exceptions as a control structure, but that is a deliberate decision. We don’t want to encourage the conflation of errors and exceptions that occur in languages such as Java.”
The post I mentioned at the start asks “why is an array out of bounds any more cause for panic than a bad format string or a broken connection?” The answer is that there is no in-band way to report the error during the evaluation of x[j], while there is an in-band way to report an error in a bad format string or a broken connection. (The format string decision is interesting by itself but orthogonal to this discussion.)
The rule is simple: if your function is in any way likely to fail, it should return an error. When I’m calling some other package, if it is well written I don’t have to worry about panics, except for, well, truly exceptional conditions, things I shouldn’t be expected to handle.
One thing you have to keep in mind is that the target for Go is programming in the large. We like to keep programs concise, but not at an increased maintenance cost for big programs worked on by large numbers of programmers. The seductive thing about exception-based error handling is that it works great for tiny examples. But diving into a large code base and having to worry about whether every single line might, in ordinary operation, trigger an exception worth handling is a significant drag on productivity and engineer time. I have had this problem myself finding my way around large Python programs. The error returns used by Go are admittedly inconvenient to callers, but they also make the possibility of the error explicit both in the program and in the type system. While simple programs might want to just print an error and exit in all cases, it is common for more sophisticated programs to react differently depending on where the error came from, in which case the try + catch approach is actually more verbose than explicit error results. It is true that your 10-line Python program is probably more verbose in Go. Go’s primary target, however, is not 10-line programs.
Raymond Chen’s articles are the best exposition I’ve seen about the pitfalls of trying to do error handling with exceptions:
http://blogs.msdn.com/b/oldnewthing/archive/2004/04/22/118161.aspx
http://blogs.msdn.com/b/oldnewthing/archive/2005/01/14/352949.aspx
Suffice it to say that the Go developers agree: error is so important we made it a built-in type.
Russ
P.S. Occasionally you see panic and recover used as a kind of non-local goto, similar to longjmp and setjmp in C. This is fine too, but only as an internal detail of your package. If callers need to know, you’re still doing it wrong.
[1] http://uberpython.wordpress.com/2012/09/23/why-im-not-leaving-python-for-go/