Before you go, check out these stories!

0
Hackernoon logoPower-Up Your Golang Logging: Idiomatic Log Strategies in Go by@wagslane

Power-Up Your Golang Logging: Idiomatic Log Strategies in Go

Author profile picture

@wagslaneLane Wagner

Bitcoinist, libertarian, atheist, cryptography fan, and founder of http://qvault.io

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
}

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)
}

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)
 }

Rule #2 – 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
}

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
}

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()) 

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

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

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)
}

Will print something like:

{1996 0x40c138}

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)
}

Which will print something like:

{make:Toyota, year:1996}

fmt.Println()

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

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

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")

Thanks For Reading!

Tags

The Noonification banner

Subscribe to get your daily round-up of top tech stories!