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.

As an aside, if you’re interested in taking a more comprehensive approach to learning Go, take a look at my Go Mastery course. With that said, back to logging.

  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
}

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, ""
}

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
}

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

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
}

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

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.

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

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

Which will print something like:

{make:Toyota, year:1996}

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

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

#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.

Thanks for reading, now take a course!

Interested in a high-paying job in tech? Land interviews and pass them with flying colors after taking my hands-on coding courses.

Questions?

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!

Subscribe to my newsletter for more coding articles delivered straight to your inbox.

Related Work