Exceptions for Go as a Library
Go (golang) lacks support for exceptions found in many other languages. There are good reasons for Go to not include exceptions. For instance, by making error handling explicit the programmer is forced to think concretely about the correct action to take. Fined grained control over the handling of errors using multiple return parameters is one of Go's strengths.
However, there are cases where Go programs do not universally benefit from the explicit handling of errors. For instance, consider the following code:
func DoStuff(a, b, c interface{}) error {
x, err := foo(a)
if err != nil {
return err
}
y, err := bar(b, x)
if err != nil {
return err
}
z, err := bax(c, x, y)
if err != nil {
return err
}
return baz(x, y, z)
}
If Go had exceptions such code could be easily simplified:
func DoStuff(a, b, c interface{}) throws error {
x := foo(a)
y := bar(b, x)
baz(x, y, bax(c, x, y)
}
Adding Exceptions with a Library
I created a library which adds support for exceptions to the go programming language. This library allow you to write go with exceptions and try-catch-finally blocks. It is not appropriate for all situations but can simplify some application code. Libraries and external APIs should continue to conform to the Go standard of returning error values.
Here is an example of the DoStuff
function where foo, bar and baz all throw
exceptions instead of returning errors. (We will look at the case where they
return errors that you want to turn into exceptions next). We want DoStuff to
be an public API function and return an error:
func DoStuff(a, b, c interface{}) error {
return exc.Try(func() {
x := foo(a)
y := bar(b, x)
baz(x, y, bax(c, x, y)
}).Error()
}
Catch Blocks
Now let's consider the case where we want to catch the exception log and reraise it:
func DoStuff(a, b, c interface{}) error {
return exc.Try(func() {
x := foo(a)
y := bar(b, x)
baz(x, y, bax(c, x, y)
}).Catch(&exc.Exception{}, func(t exc.Throwable) {
log.Print(t)
exc.Rethrow(t, exc.Errorf("rethrow after logging"))
}).Error()
}
Rethrow will chain the Throwable t
with the new *Error
created such that
if/when the exception reaches the top level you know exactly how it was
created and where it was re-thrown.
Throwing Errors
Ok, what about interacting with regular Go APIs which return errors? How can
we turn those errors into exceptions? The easy was is to use the
ThrowOnError
function which is a sugar for:
if err != nil {
ThrowErr(ErrorFrom(err)
}
So converting out original DoStuff
function we get
func DoStuff(a, b, c interface{}) { // Throws
x, err := foo(a)
exc.ThrowOnError(err)
y, err := bar(b, x)
exc.ThrowOnError(err)
z, err := bax(c, x, y)
exc.ThrowOnError(err)
exc.ThrowOnError(baz(x, y, z))
}
This package also supports: catching user defined exceptions, catching
multiple exception types, Close
which works like the "try with resources"
construct in Java 7+, (multiple) finally blocks, and a choice between
propagating exceptions with Unwind
or retrieving the error/exception with
Error
and Exception
functions.
One Gotcha! The Try()
function creates a *Block
struct. To execute the
block you must either call: Unwind
, Error
, or Exception
. Unwind
executes the block, if there is an exception coming out of the block it
continues to cause the program stack unwind. Error
and Exception
execute
the block, but return the exception as a value to deal with in the usual Go
way.
Finally Blocks
Finally blocks are a great feature of exceptions. They allow you to have a block of code run unconditionally after a try and catch block even if there was an un-handled exception or re-raised exception. In this example I use a finally block to log the timing information of the DoStuff function.
func DoStuff(a, b, c interface{}) error {
start := time.Now()
return exc.Try(func() {
x := foo(a)
y := bar(b, x)
baz(x, y, bax(c, x, y)
}).Catch(&exc.Exception{}, func(t exc.Throwable) {
log.Print(t)
exc.Rethrow(t, exc.Errorf("rethrow after logging"))
}).Finally(func() {
end := time.Now()
log.Printf("Do stuff took: %v", end.Sub(start))
}).Error()
}
Automatically Closing Resources
One common situation is to open a resource (like a file) and want it to close no
matter what. In Go this is often accomplished with a defer
function. However,
we can accomplish the same thing with finer granularity using the Close
function which acts like a Try
but automatically closes a created resource
when the block exits:
Close(func() io.Closer {
f, err := os.Create("/tmp/wizard")
ThrowOnError(err)
return f
}, func(c io.Closer) {
f := c.(*os.File)
_, err := f.WriteString("wizardry")
ThrowOnError(err)
}).Unwind()
In the above code, the file created in the first function will always be closed at the end of the *Block even if WriteString had an error. This get more interesting if you imagine passing the resource to other functions which could throw exceptions.
User Defined Exception Types
To create a user defined exception simply create a struct which inherits from
the exc.Exception
struct by embedding it as the first member.
type MyException struct {
exc.Exception
}
Then you can catch MyException with Exception. eg:
exc.Try(func() {
Throw(&MyException{*Errorf("My Exception").Exception()})
}).Catch(&Exception{}, func(t Throwable) {
log.Log("caught!")
}).Unwind()
This should work with heirarchies of exceptions allowing your code to declare specific exceptions for specific errors.
How does it work?
Go provides panic
and recover
as built-in
functions. panic(.)
allows you
to cause the program to halt and the stack to "unwind". This means stack frame
by stack frame the each function is halted and any deferred functions are run.
Let's look at an example:
package main
import (
"fmt"
)
func a() {
defer func() {
fmt.Println("defer a")
}()
fmt.Println("a")
b()
}
func b() {
defer func() {
fmt.Println("defer b")
}()
fmt.Println("b")
c()
}
func c() {
defer func() {
fmt.Println("defer c")
}()
fmt.Println("c")
panic("c panic")
}
func main() {
a()
}
Output:
a
b
c
defer c
defer b
defer a
panic: c panic
goroutine 1 [running]:
panic(0x128360, 0x1040a140)
/usr/local/go/src/runtime/panic.go:481 +0x700
main.c()
/tmp/sandbox797297488/main.go:28 +0x180
main.b()
/tmp/sandbox797297488/main.go:20 +0x140
main.a()
/tmp/sandbox797297488/main.go:12 +0x140
main.main()
/tmp/sandbox797297488/main.go:32 +0x20
(try it on the playground https://play.golang.org/p/0Vp4jq0978)
So panic by default kills your program but gives you a nice stack trace. It also
runs your defer
ed functions which allows some cleanup to happen. Recognizing
this was rather limited and their are times even in Go when it is nice to
recover from what is usually a fatal error (like in webservers) Go also provides
recover
.
The recover
function can be thought of as a limited panic catching function.
It can only meaingfully be used inside of defer functions. When a recover
is
found it will stop the panic (that is stop unwinding the stack) and allow the
function the defer
ed recover
is in to exit normally. Let's look at another
example:
package main
import (
"fmt"
)
func a() {
defer func() {
fmt.Println("defer a")
}()
fmt.Println("a")
b()
fmt.Println("end a")
}
func b() {
defer func() {
fmt.Println("defer b")
recover()
}()
fmt.Println("b")
c()
fmt.Println("end b")
}
func c() {
defer func() {
fmt.Println("defer c")
}()
fmt.Println("c")
panic("c panic")
fmt.Println("end c")
}
func main() {
fmt.Println("start")
a()
fmt.Println("end")
}
Output:
start
a
b
c
defer c
defer b
end a
defer a
end
(Try it on the playground: https://play.golang.org/p/WTzKywhey2)
This time, instead of the program crashing it exited normally. Functions, a
and main
, who invocations occurred before b
which invoked recover
exited
normally. However, using recover like this will stop all errors and not explain
what the problem is. Luckily, recover reports whether or not a panic was
recovered and what the argument to the panic was. Here is an example:
package main
import (
"fmt"
)
func a() {
defer func() {
fmt.Println("defer a")
if e := recover(); e != nil {
fmt.Println("recovered", e)
}
fmt.Println("end defer of a")
}()
fmt.Println("a")
b()
fmt.Println("end a")
}
func b() {
defer func() {
fmt.Println("defer b")
if e := recover(); e != nil {
fmt.Println("recovered", e)
}
fmt.Println("end defer of b")
}()
fmt.Println("b")
c()
fmt.Println("end b")
}
func c() {
defer func() {
fmt.Println("defer c")
}()
fmt.Println("c")
panic("c panic")
fmt.Println("end c")
}
func main() {
fmt.Println("start")
a()
fmt.Println("end")
}
Output:
start
a
b
c
defer c
defer b
recovered c panic
end defer of b
end a
defer a
end defer of a
end
(Try it on the playground: https://play.golang.org/p/agxBcqgCb8)
This time the value passed into panic was returned by the call to recover
in
b
. In a
which contains the same code as b
to recover the panic nothing is
returned by recover
.
Implementation
To implement exceptions, panic
is used to throw the Throwable
objects. Then
a special function exec
is created to execute the try
functions:
func (b *Block) exec() (err Throwable) {
defer func() {
if e := recover(); e != nil {
switch exc := e.(type) {
case Throwable:
err = exc
default:
panic(e)
}
}
}()
b.try()
return
}
This function simply calls the function passed into Try()
. However, exec
also
registers a defer
which changes the returned value of exec
if a Throwable
is discovered. Thus, we can throw exceptions with panic
(no changes
necessary!) and catch them with recover
. The final piece is implementing the
semantics of try, catch, finally. This is accomplished by a function called
run
:
func (b *Block) run() (Throwable) {
err := b.exec()
if err != nil {
t := reflect.TypeOf(err)
for _, c := range b.catches {
if isa(t, c.exception) {
err = Try(func(){c.catch(err)}).exec()
break
}
}
}
for _, finally := range b.finallies {
finally()
}
return err
}
The run
function first executes the try
and gets the error if any. If there
is an error it tries to find a catch function to handle it (using the isa
helper function to identify the catcher). Then whether or not a catch function
was found, all finally
functions are run in order of declaration. The final
trick is the catch
functions are run inside of a Try
block and exec
directly in case they re-throw exceptions. That is it! Check out the source
code for more
details.
Conclusion
There are good reasons why Go does not include exceptions at the language level.
However, at times their absence can become annoying. So I created this little
library that adds them to Go. You probably shouldn't make public APIs based on
it, but it may help you write certain internal code more clearly. One clear use
case is during a refactor adding Throw
call in a deep internal function which
should have always returned an error. Then at the entry points to the API simply
wrap up in a Try().Catch().Error()
which would accomplish the same thing as
adding a returned error throughout the library.
I hope you enjoy this little experiment of mine and don't send me too much hate
mail! I know that exceptions are controversal in the Go community. This library
tries to demonstrate how close panic
and recover
are to exceptions and how
to emulate exceptions using panic
and recover
.