Base Go packages
The Go standard library is generally great, but some parts have replacements that are just plain better and remove frustrations that you may have not even realised were frustrations. Here are my recommendations for every Go program.
I wouldn’t recommend that anyone use the standard library version of these for any purpose, since better alternatives exist.
This list may expand in the future.
gorilla/mux
The standard router is fine, but very low level. Here’s some of the features that makes it vital.
Filter on HTTP method
With the standard router you have to manually check that the method is
what you expect it to be, and if the same endpoint has both GET
and
POST
then you have to route that yourself. With gorilla/mux it’s as simple as:
r := mux.NewRouter()
get := r.Method("GET").Subrouter()
post := r.Method("POST").Subrouter()
get.HandleFunc("/", handleRoot)
get.HandleFunc("/items", handleListItems)
post.HandleFunc("/items", handleUploadItem)
You can also assert that headers are in place, for example to check
X-Requested-With
because some API endpoints should not be allowed in
cross-domain XHR requests. Adding it to the router instead of manual
checks simplifies code and reduces risk of forgetting to add the
check.
Pattern URLs
With the standard router you have to set up a prefix handler and parse the URL yourself… and have your “/” handler handle all 404s that it’ll get.
get.HandleFunc("/items/{item_id:[0-9-]+}", handleGetItem)
[...]
func handleGetItem(w http.ResponseWriter, r *http.Request) {
itemID := mux.Vars(r)["item_id"]
}
sirupsen/logrus
Unstructured logging is frustrating. Merely searching for all log entries in a given time range is a whole project because timestamps need to be parsed, daylight savings taken into account, and then the rest of the line needs to be parsed to extract severity and message.
And that doesn’t even enable multi-line logs, labels and other structured data.
func handleGetItem(w http.ResponseWriter, r *http.Request) {
log := logrus.WithFields(logrus.Fields{
"client_address": r.RemoteAddr,
},
)
itemID := mux.Vars(r)["item_id"]
if secretItem(itemID) {
log.Warningf("Attempt to access secret item %q", itemID)
code := http.StatusForbidden
http.Error(w, http.StatusText(code), code)
return
}
}
If outputting in JSON format that becomes:
time="2018-09-15T13:34:18+01:00" level=warning msg="Attempt to access secret item \"42\"" client_address=192.0.2.123
You can even add the item ID as another field, incrementally, with:
log = log.WithField("item_id", itemID)
To have these fields traverse code stacks I’d probably have my extra label data attached to the context, so that I don’t have to pass down both context and log objects.
errors
This is not so much a replacement for a package as filling a need that plain errors have.
The biggest problem standard Go errors have is that they are pretty
much untyped. Yes, they have a type, but then they get annotated as
they traverse up the stack so that in the end they’re more like
freeform strings. Once your os.PathError
is wrapped via standard
means (return fmt.Errorf("failed to read config: %v", err)
) the type
is obliterated.
func readSomething() error {
_, err := ioutil.ReadAll(f)
if err != nil {
return errors.Wrap(err, "reading the thing failed")
}
return nil
}
type stackTracer interface {
StackTrace() errors.StackTrace
}
func main() {
if err := readSomething(); err != nil {
log := logrus.WithFields(log.Fields{
"error": err.Error(),
"cause": errors.Cause(err),
})
if e, ok := err.(stackTracer); ok {
log = log.WithField("stack", e.StackTrace())
}
if e, ok := errors.Cause(err).(*os.PathError); ok {
log = log.WithFields(log.Fields{
"os.PathError.Op": e.Op,
"os.PathError.Path": e.Path,
"os.PathError.Err": e.Err,
})
}
log.Fatalf("Failed to read something")
}
}
Now the original error (or as close to it as possible) can be found
with errors.Cause(err)
. err.String()
is still the most
human-readable full error, but by wrapping errors you don’t have to
choose between annotating, and keeping the error type and exact
value. And also not resort to parsing error strings.