DEV Community

Discussion on: Attempting to Learn Go - Building Dev Log Part 04

Collapse
 
ladydascalie profile image
Benjamin Cable

definitely wouldn't be "production" quality

If it works, it works! As long as you don't have egregious design flaws, you can always refactor to a clean codebase.

I think a change in habit is really what is needed

That is quite accurate. I remember the early days of using Go within my team. people just starting to use the language would frequently make that same mistake of either just retuning all errors up the call stack, or using panic when they didn't really know how to respond to the failure.

In your case, it very well may be that you do not want to continue processing any further in case of an error, and there's nothing inherently wrong with that. Having detailed messages will help you track down your issues easily.

Here's an interesting tidbit for you to consider as well:

If you've learned about panic and recover, then you'll know that one of the interesting properties of panic is that it will still run defered function calls. If you haven't read up on that, I encourage you to read this article: golangbot.com/panic-and-recover

In your case, that means that if you open a file, and then run defer f.Close(), even if you were to panic later on, your will try and cleanup the file by closing it properly.

However, a naked panic call only takes in a string as a message, and so it is unwieldy to construct proper error messages with it.

The solution here would be to use log.Panicf, which the docs describe as "[...]equivalent to Printf() followed by a call to panic()."

That makes it easier to use, and you can reuse known idioms:

log.Panicf("releveant error context: %v", err).

Of course you will have scenarios where you do not want to panic immediately, and you do want to return the error up the callstack.

In this case, I generally do this, if I do not control the source of the error, or don't care about it very much.

thing, err := something() {
    return fmt.Errorf("wrapped error: %v", err)
}

If I do control it, or care, then I will approach this two ways:

You can easily create your own error types, to make control flow easier on yourself:

type ParseError struct { error }

Simply embedding error into the struct makes this a one liner:

fm, err := parseFrontMatter(input)
if err != nil {
    return ParseError{error:fmt.Errorf("could not parse front matter with error: %v", err)}
}

Now, instantly, you get more context, and you are also able to do this, higher up the call stack:

if err := process(thing); err != nil {
    switch e := err.(type) {
        case ParseError:
             // Do what you need in that case
        default:
             log.Panicf("Unexpected error %T: %[1]v", e)
    }
}

Side note:

This line log.Panicf("Unexpected error %T: %[1]v", e) uses explicit argument indexes, which are documented in the fmt package.

Thread Thread
 
shindakun profile image
Steve Layton

I wish I could like this twice. ;) Thanks so much for the detailed comment and the links! panic and recover are on my list of things to get to, I've only given recover a cursory glance so far. And honestly, I didn't realize log.Panicf() was a thing, don't recall seeing it used anywhere (but could have missed it) though it makes perfect sense that it would since log.Panic() exists. I only have one spot where I gave myself a more detailed error message and that was after the return of parseFrontMatter() and that was only because I hit a weird issue when I originally wrote the function. I wish I had noted it down so I could have included it in the post but forgot.

I hope to fix up the front matter code to handle errors better by next week and you've given me a lot to consider which I think will help with that section and hopefully my code over all.