DEV Community

Lane Wagner
Lane Wagner

Posted on • Originally published at qvault.io on

Logging for Gophers – Idiomatic Log Strategies in Go (Golang)

Inspecting Logs

In this article, I’m going to cover some rules of thumb for logging in go, as well as some functions you may not have heard of that can make your debugging life easier.

Rule #1 – Use Errors Where Appropriate, Not Strings

Go has a built-in error type, which allows developers to easily differentiate errors from “normal” strings, as well as check if there are no errors in a more succinct way. The error type is an interface, that simply requires the type in question to define an “Error()” function that prints itself as a string.

type error interface {
    Error() string
}
Enter fullscreen mode Exit fullscreen mode

Never use a normal string where an error is more appropriate! Strings imply to users of your function that “business as usual” is going on. Errors make it clear that something is wrong.

For example, let’s pretend we are building a REST API. We may want a function that takes a response writer, a message, and a code that can be used to return error codes on erroneous API calls. Here is our first attempt:

func respondWithError(w http.ResponseWriter, code int, msg string) {
    payload := map[string]string{"error": msg}
    response, _ := json.Marshal(payload)
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(code)
    w.Write(response)
}
Enter fullscreen mode Exit fullscreen mode

This will work perfectly. In fact, anywhere where an error type works a string could be used instead. However, if we are interested in writing code that other developers can more quickly understand and make contributions to, we should use an Error type:

func respondWithError(w http.ResponseWriter, code int, msg error) {
     payload := map[string]string{"error": msg.Error()}
     response, _ := json.Marshal(payload)
     w.Header().Set("Content-Type", "application/json")
     w.WriteHeader(code)
     w.Write(response)
 }
Enter fullscreen mode Exit fullscreen mode

Rule #2 – Wrap Errors

Mummy Wrap Errors

Often times, we simply pass errors up a chain, and it can be quite convenient. For example, let’s look at this function that formats hours and minutes into a time message:

func formatTimeWithMessage(hours, minutes int) (string, error) {
    formatted, err := formatTime(hours, minutes)
    if err != nil {
        return "", err
    }
    return "It is " + formatted + " o'clock", nil
}
Enter fullscreen mode Exit fullscreen mode

The problem here is that the formatTime function can be called many other places within our application or library. If all we do is pass along the raw error, it gets really hard to tell where the error was called. Instead, let’s do the following:

func formatTimeWithMessage(hours, minutes int) (string, error) {
    formatted, err := formatTime(hours, minutes)
    if err != nil {
        return "", fmt.Errorf("formatTimeWithMessage: %v", err)
    }
    return "It is " + formatted + " o'clock", nil
}
Enter fullscreen mode Exit fullscreen mode

Additionally, if you are working in Go 1.13 or later, then you can look into the more explicit “Unwrap()” method for error chains: https://blog.golang.org/go1.13-errors#TOC_3.1.

fmt.Errorf()

fmt.Errorf() is similar to fmt.Printf(), but returns an error instead of a string. You may have done this in the past:

err := errors.New("Bad thing happened! " + oldErr.Error())
Enter fullscreen mode Exit fullscreen mode

This can be accomplished more succinctly using fmt.Errorf():

err := fmt.Errorf("Bad thing happened! %v", oldError)
Enter fullscreen mode Exit fullscreen mode

The difference in readability becomes even more obvious when the formatting in question is more complicated and includes more variables.

Formatting Structs

Printing structs can be quite ugly and unreadable. For example, the following code:

func main() {
    make := "Toyota"
    myCar := Car{year:1996, make: &make}
    fmt.Println(myCar)
}
Enter fullscreen mode Exit fullscreen mode

Will print something like:

{1996 0x40c138}
Enter fullscreen mode Exit fullscreen mode

We may want to get the value in the pointer, and we probably want to see the keys of the struct. So we can implement a default String() method on our struct. If we do so, then the Go compiler will use that method when printing.

func (c Car)String() string{
    return fmt.Sprintf("{make:%s, year:%d}", *c.make, c.year)
}

func main() {
    make := "Toyota"
    myCar := Car{year:1996, make: &make}
    fmt.Println(myCar)
}
Enter fullscreen mode Exit fullscreen mode

Which will print something like:

{make:Toyota, year:1996}
Enter fullscreen mode Exit fullscreen mode

fmt.Println()

In the past, I’ve often done the following when logging:

fmt.Printf("%s beat %s in the game\n", playerOne, playerTwo)
Enter fullscreen mode Exit fullscreen mode

Turns out, it is much easier to just use the fmt.Println() function’s ability to add spacing:

fmt.Printf(playerOne, "beat", playerTwo, "in the game")
Enter fullscreen mode Exit fullscreen mode

Thanks For Reading

Lane on Twitter: @wagslane

Lane on Dev.to: wagslane

Download Qvault: https://qvault.io

Star our Github: https://github.com/q-vault/qvault

The post Logging for Gophers – Idiomatic Log Strategies in Go (Golang) appeared first on Qvault.

Top comments (1)

Collapse
 
fpolster profile image
Florian Polster

Shouldn't it be %w in the fmt.Errorf statements to wrap errors?