There is nothing Goish about log.Fatal
If “Goish” describes something that’s characteristic for the Go language,
log.Fatal
is off the wall.
In this article I will use some time to describe what I think are characteristic concepts or features in Go, and I will use some time to motivate why log.Fatal does not belong. Finally I will present what I believe to be a better pattern and I will finish up with how this pattern could be made less verbose and more Goish in Go 2.
What is “Goish”?
I believe there are a few concepts in Go that are so fundamental that they help define what Go is, or perhaps, what it means to write go.
For the purpose of this article, I will call this notion Goish. To determine what that really is, let’s start by talking around some of the most characteristic Go language keywords.
Let’s start with the most obvious one. The go
keyword is designed to allow programmers to rely on easy-to-use concurrent execution. We will not dive into tremendous depths on this one, but together with its underlying scheduler implementation, the go-routine concept and syntax is part of what makes Go good for high-throughput applications. Put extremely short, we can say that this helps with Go’s simplicity and efficiency.
Thedefer
keyword on the other hand, allows for a way to clean things up when a function returns or panics. Therecover
keyword, when used within a deferred function, helps define Go’s error handling paradigm. In short, this paradigm separates between normal errors and critical panics. Generally speaking, it leads to explicit error handling on all function that might return an error. On the other side of the coin, it’s made pretty clear that there is no need for error handling for functions that don’t return an error type. Both of these points are in sharp contrast to the Exceptions pattern. All of these concepts are meant to help with Go program’s safety and stability.
Not all features in Go relates to specific keywords or libraries though. A completely different feature that helps define Go, is it’s language level memory safety. Go is garbage collected, which helps prevent memory leaks, and there are only a few ways where you may access unsafe memory. In addition, we got the Go Memory Model to define memory synchronization guarantees in a concurrent environment. You could argue that the way these features are implemented, impose trade-offs that helps balance Go’s simplicity and efficiency with reasonable safety and stability.
To sum it up, maybe part of what Goish means, is a feature or construct that helps develop stable and efficient programs easily.
What is log.Fatal?
With the definitions done, let’s get onto the main subject of this article, and look at log.Fatal. According to the documentation, it’s just a shortcut for log.Print(v); os.Exit(1)
. Remember that os.Exit exits the go program immediately, without running any defer statements.
This means that log.Fatal
(and log.Fatalln
) is essentially semantically equivalent to a non-recoverable panic, without a stack-trace, that prevents any deferred functions from running. This doesn’t sound very Goish to me.
Using log.Fatal in an imported package
This is a nice way of screwing with anyone that might want to use your package, including yourself. As a package author, you must consider that the main package might at some point need to start or open something else that requires a defer function to be run, even if the log.Fatal is just part of some type’s initialization. It would generally be better to panic (and optionally recover), then to use log.Fatal.
PS! Depending on wether it’s a reusable package or not, you might want to consider if it should log anything at all, or how it should be done.
But it’s OK to use it in the main function, right?
Sometimes maybe, but most of the time it’s probably best not to. Consider this main function for instance:
func main() {
// parse command-line params through flags.Parse into a struct.
cfg := parseConfig() // Initialize some DB connection.
session, err := mgo.Dial(cfg.DBURL)
if err != nil {
log.Fatal("could not connect to database:", err)
}
defer session.Close() // Initialize API handler, and serve /api.
api := newAPIHandler(session.DB())
http.Handle("/api/", http.StripPrefix("/api/", api))
log.Fatal(http.ListenAndServe(cfg.SrvAddr, nil))
}
The first log.Fatal is OK, as there is no defer statements queued yet, but the second log.Fatal will prevent the database session from being closed if there is an error returned from ListenAndServe
. Unfortunately, the latter line is actually taken from the official go documentation, making it more likely that programmers might do such mistakes in production. You probably shouldn’t useListenAndServe
in production btw.
Who cares about closing a database sessions?
Let’s play the devil’s advocate for a while, and come up with a contradicting statement:
A program might be killed for any reason, so we can’t rely on defer. Therefore, isn’t it acceptable that defers doesn’t always run on fatal errors?
However, consider this; if we didn’t care about it running, why did we care to write it as a deferred statement in the first place? It’s not really about guaranteeing safety, but about increasing it.
I will give a more motivating real-life example for where you really do want your deferred statements to run as often as possible. If you don’t care for this, feel free to jump to the next headline.
On my University, back in 2012, we wrote an autonomous robot with its high level logic written in Go. While I might write more about this robot later, for now, all you need to know is that this robot had a proprietary CAN-bus motor-controller that was wired to the robot’s propulsion system. To manage this controller, we had a self-written driver in C and Cgo running on a small Intel x86 tablet.
As drivers often do, this driver needed some initialization. I don’t have the original code at hand at the moment, but for the purpose of this article, let’s say that we had a package motor
with something like this:
packacge motor// Init initializes the propulsion motor driver or panics.
func Init(device string) {...}// Halt attempts to message the motor controller to halt the robot.
func Halt() {...}
It didn’t take long before we put in defer motor.Halt()
right after where motor.Init
was called in the main package. We also put in some code to capture interrupts (Ctrl+C). Although you can not rely on any software to always terminate gracefully, adding this features made development much easier as the robot would virtually always physically halt on fatal errors and panics. Putting log.Fatal anywhere inside this application would be likely to give extremely annoying results.
I want to underline that this was not defined as a safety feature for us; the safety feature was a big red physical button that cut the power. I still believe that the defer function contributed to increased safety by simply reducing the average time it took for the robot to halt on errors. It also greatly reduced the distance we had to run inside the Lab, and running in the Lab is a hazard in itself…
Enough with the motivation, how can I fix it ?
I have a relatively simple pattern that I use for Go 1.x programs to ensure graceful shutdowns on fatal errors. Let’s update the previous HTTP server example to use this pattern.
func main() {
var exitCode int
defer func() {
os.Exit(exitCode)
}() // Note that the flag package might still call os.Exit(2) :-/
cfg := parseConfig() session, err := mgo.Dial(cfg.DBURL)
if err != nil {
log.Print("could not connect to database:", err)
exitCode = 1
return
}
defer session.Close() api := newAPIHandler(session.DB())
http.Handle("/api/", http.StripPrefix("/api/", api))
if err := http.ListenAndServe(cfg.SrvAddr, nil))}; err != nil {
log.Print(err)
exitCode = 1
return
}
}
It’s a bit more verbose than log.Fatal(), but much safer. In addition to this of-course, you should consider setting up a listener for interrupt signals, so that defer statements also run when a program is terminated by Ctrl+C.
How could this improve with Go 2?
Besides removing log.Fatal
and log.Fatalln
, there is really two features the Go language could add to make this pattern simpler.
Return the return code
The first suggestion is one that is already present in C, but never made it to Go: an integer return from the main function.
func main() int {
return 0
}
While there probably was good reasons for not including this initially, returning the exit code really is more Goish in that it it makes it easier to write stable programs that also terminates gracefully. It is also more similar to how errors are already handled via returns, so it got the potential of having a Goish feel to it as well.
We don’t have to return plain integers of-course, we could gopherize it up if we want, e.g. by introducing a new built-in type exit int32
, alongside constants like os.ExitUsage = exit(2)
and os.ExitGenericError = exit(1)
, or something like that.
If we update our HTTP server example yet again, we can see that it’s now quite readable.
func main() exit {
// Maybe the flag.Parse() returns an error in Go 2.
cfg, err := parseConfig(); err != nil {
return os.ExitUsage
} session, err := mgo.Dial(cfg.DBURL)
if err != nil {
log.Print("could not connect to database:", err)
return os.ExitGenericError
}
defer session.Close() api := newAPIHandler(session.DB())
http.Handle("/api/", http.StripPrefix("/api/", api))
if err := http.ListenAndServe(cfg.SrvAddr, nil))}; err != nil {
log.Print(err)
return os.ExitGenericError
}
return os.ExitSuccess
}
It’s also worth noting that the existing os.Exit(int)
could potentially be removed from the API with this change.
Capture interrupt signals by default
It would be of great help if defer statements were run by default on interrupt signals. I have no idea how this might be implement, but if cancellation where to be a built-in part of the language as suggested elsewhere, maybe there is a way. Doing so would have ripples though.
Conclusion
It’s hard to come up with any defensible reason why log.Fatal
should keep existing in Go 2.0. We have demonstrated an alternative pattern to use in Go 1.x, and talked about some changes that could make this pattern more natural in Go 2.0. Have fun coding!