Top 6 Golang Logging Best Practices

Let’s 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. Best practices for logging in Go aren’t super obvious, and sometimes we have to take a deeper look to see what the best choice is given Go’s unique error handling situation.

Sorry to interrupt! I just wanted to mention that you should check out my new free Go course. It’s designed to teach you all the fundamentals of my favorite coding language.

  1. Use errors, not strings
  2. Wrap errors up the call stack
  3. Use formatting directives like fmt.Errorf()
  4. Format structs appropriately
  5. Use variadic functions as they were intended to be used
  6. Use the built-in log package

#1 – Use Errors Where Appropriate, Not Strings

Go has a built-in error type, which allows developers to easily differentiate errors from “normal” strings and check to make sure functions exit without a problem in a more explicit 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 }
Code language: Go (go)

Never use a normal string where an error is appropriate! When a string is returned from your function you imply to other developers that when the string isn’t empty it’s just “business as usual”. The error type makes it clear that something is wrong when the error isn’t nil.

For example, let’s pretend we have a function that divides two numbers safely and returns a result.

func divide(a, b float64) (float64, string) { if b == 0 { return 0.0, "can't divide by zero" } return a / b, "" }
Code language: Go (go)

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

func divide(a, b float64) (float64, error) { if b == 0 { return 0.0, errors.New("can't divide by zero") } return a / b, nil }
Code language: Go (go)

#2 – Wrap Errors

Mummy Wrap Errors

Often times out of laziness we simply pass errors up a call chain. 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 }
Code language: Go (go)

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, when the error is eventually printed, it gets really hard to tell where exactly the error originated from. 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 }
Code language: Go (go)

Additionally, if you are working in Go 1.13 or later, then you can look into the more explicit Unwrap method for error chains.

#3 – Use Formatters Like 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())
Code language: Go (go)

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

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

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

#4 – Format Structs Where Appropriate

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) }
Code language: Go (go)

Will print something like:

{1996 0x40c138}

We likely 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) }
Code language: Go (go)

Which will print something like:

{make:Toyota, year:1996}
Code language: CSS (css)

#5 – Use the variadic forms of functions like fmt.Println()

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

fmt.Printf("%s beat %s in the game\n", playerOne, playerTwo)
Code language: Go (go)

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

fmt.Println(playerOne, "beat", playerTwo, "in the game")
Code language: Go (go)

#6 – Use the Built-in Log Package

It’s often tempting to roll your own logging package, but I would advise that in most cases, the standard log package is probably all you need. The standard library defines a type, Logger, which you can use to customize your logging in an idiomatic way. If you don’t want that much power and responsibility, you can do what I usually do and use the standard Print and Fatal functions which just print to standard output along with a formatted date and time prefix.

Best Practices

Glad you’ve made it this far! Learning to properly handle errors in Go is one of the things that sets advanced developers apart from newcomers. Striving to improve the readability and developer usability of your code will make you a better computer scientist, and help you find more worthwhile jobs in the future.

Have questions or feedback?

Follow and hit me up on Twitter @q_vault if you have any questions or comments. If I’ve made a mistake in the article be sure to let me know so I can get it corrected!

Related Work