10

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

 4 years ago
source link: https://hackernoon.com/logging-for-gophers-idiomatic-log-strategies-in-go-golang-ocr3tz3
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
2q2MF3F.jpg!web

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

AZZrimF.jpg!web

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!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK