DEV Community

Jon Calhoun
Jon Calhoun

Posted on • Updated on • Originally published at calhoun.io

When Should I Use One Liner if...else Statements in Go?

This article was original posted at calhoun.io

After using Go for a few weeks, chances are you are going to run across a single-line if...else statement. Most often than not, the first one you see will be used for error handling and will look something like this:

if err := doStuff(); err != nil {
  // handle the error here
}
Enter fullscreen mode Exit fullscreen mode

The first bit of the if is known as an initialization statement, and it can be used to setup local variables. For functions that only return an error, the initialization statement is an incredibly useful tool. The error is scoped only to the if block that handles it, and our code ends up being a little easier to read.

But when is it appropriate to use this approach rather than breaking your code into multiple lines? Are there any rules or guidelines for when you should or shouldn't use the one-liner if statement?

In this post we are going to explore situations where using an initialization statement is appropriate, ones where it isn't, and explain some of the subtle differences between the two.

Break up long expressions

This first section might be a bit controversial. Everyone has a different opinion on whether we should break long lines into multiple lines, what the max character count is, and everything else in between. The Go Wiki has a section that discusses this topic, and I generally agree with it, so I suggest you check it out. I'll wait...

Okay, back from reading? Great! Now let's look at some examples.

First up we have the case of the long function name. The best solution to this is to rename the function, but you may be using another library where that isn't possible. Let's face it, every once in a while a Java developer gets his hands on some Go code and we can't spend all our time cleaning up after them.

// rather ugly
if err := thisHasAReallyLongSuperDescriptiveNameForSomeCrazyReason(); err != nil {
    // handle the error
}

// better
err := thisHasAReallyLongSuperDescriptiveNameForSomeCrazyReason()
if err != nil {
    // handle the error
}
Enter fullscreen mode Exit fullscreen mode

Run it on the Go Playground → https://play.golang.org/p/gxLEZU_Uhmw

Assuming we can't rename the function, I tend to place these calls on a single line. Again, that isn't a set-in-stone rule, but it is rare to find a situation where using a few lines isn't clearer than putting that monster of a function call on a line with other logic.

Another situation where you might encounter extra long if statements are functions that accept many arguments; this is especially true if you are passing in hard-coded strings.

Once again this can be solved in a variety of ways, such as breaking the arguments onto new lines and not using the one-liner syntax. A few examples are shown below, and we will discuss them a bit more after you see the code.

// pretty ugly
if err := manyArgs("This is a fairly long message about doing some stuff", "and we might provide even more data"); err != nil {
    panic(err)
}

// Fix 1: better, but the condition of the if statement is easy to miss
if err := manyArgs(
    "This is a fairly long message about doing some stuff",
    "and we might provide even more data"); err != nil {
    panic(err)
}

// Fix 2: probably the best solution if using the "one-liner" format
if err := manyArgs(
    "This is a fairly long message about doing some stuff",
    "and we might provide even more data",
); err != nil {
    panic(err)
}

// Fix 3: my personal preference here is not using the "one-liner" format
err := manyArgs(
    "This is a fairly long message about doing some stuff",
    "and we might provide even more data")
if err != nil {
    panic(err)
}
Enter fullscreen mode Exit fullscreen mode

Run it on the Go Playground → https://play.golang.org/p/bB1lvtn5dpJ

In Fix 1 it is a little more clear what is going on, but I dislike it because the condition - err != nil - is easy to lose in the rest of the code.

Fix 2 is a big improvement. It is easy to forget about the comma after the second argument, but the upside is once you add the comma you can add new arguments to the function call without needing to change the existing code. This is pretty handy when calling variadic functions and adding another piece of data to it. Another perk here is that the condition is on its own line, making it much easier to read when just glancing at the code.

Finally we have my personal favorite, Fix 3; this is similar to the fix we used before - we make the function call on its own line, then create the if block afterwards using that data. This just reads cleaner to me because the if term is closer to where it is relevant - the condition clause. Adding some distance between the clause and the if keyword doesn't bug me when it is a true one-liner, but on multi-liners like this it just doesn't feel as readable. YMMV.

Variable scopes

We've seen a few reasons to break up our if statements, but we also need to discuss the ramifications of breaking the initialization clause out of the if statement. That is, we need to discuss the scope of variables.

When we declare a new variable in the initialization portion of an if statement, the variable is declared within the scope of the if statement. This means that it is only available during the lifetime of that if statement. An example of this shown below.

func main() {
    if x, err := demo(); err != nil {
        // handle the error
    }
    // this doesn't work because x is scoped to the if block
    fmt.Println(x)
}

func demo(args ...string) (int, error) {
    return 1, nil
}
Enter fullscreen mode Exit fullscreen mode

Run it on the Go Playground → https://play.golang.org/p/jj9MDxYINFh

If we want to access the x variable in the previous example, we need to declare it outside the if statement otherwise it goes away once the if statement is completed. This is also why we can have an existing variable named err and then redeclare it inside of an if statement. The new variable is in a new scope, so it is permitted by the compiler.

func main() {
    var err error
    if err := demo(); err != nil {
        fmt.Println("Error in if block:", err)
    }
    fmt.Println(err) // this will be nil still!
}

func demo(args ...string) error {
    return errors.New("on no!")
}
Enter fullscreen mode Exit fullscreen mode

Run it on the Go Playground → https://play.golang.org/p/tASeTl2tlmK

Warning: Scoped variables with the same name as variables in an outer scope can lead to incredibly confusing bugs at times. Using them with if blocks is typically fine, but they can be especially problematic when you have package-global variables and create a new, scoped variable inside of a function with the same name. := is a helpful shorthand for declaring new variables, but it is important to understand how scoping works when using it.

Scoped variables can also be accessed inside of else blocks of an if statement. This can be illustrated by modifying our previous example with the x variable.

if x, err := demo(); err != nil {
    // handle the error
} else {
    fmt.Println(x)
}
Enter fullscreen mode Exit fullscreen mode

Run it on the Go Playground → https://play.golang.org/p/uRfCxR9bLSP

While this is perfectly valid code, it can easily lead to Go code that isn't very idiomatic. According to Effective Go...

In the Go libraries, you'll find that when an if statement doesn't flow into the next statement — that is, the body ends in break, continue, goto, or return — the unnecessary else is omitted.

It is fairly common for an if statement that checks for an error to return as part of handling the error. That is, if we added error handling to our previous code it is likely to look something like this:

if x, err := demo(); err != nil {
    return err
} else {
    fmt.Println(x)
}
Enter fullscreen mode Exit fullscreen mode

We might return the error, panic with it, log it and then call os.Exit, or if in a for loop we might continue, but in all of those scenarios we aren't flowing into the next statement in our code. In these situations it makes more sense to declare our variables and call the demo function outside of the if scope, and then use them in the if statement. Then our code becomes:

x, err := demo()
if err != nil {
    return err
}
fmt.Println(x)
Enter fullscreen mode Exit fullscreen mode

While effectively the same, this code is typically easier to read and makes it much easier to continue using x if we need it later in our function.

Potential annoyances

Now that we understand how scoping works, we need to look at one more potential nuisance that can occur when NOT using one-liner if statements - undefined or already defined variables.

Imagine you had the following code:

err := demo1()
if err != nil {
    panic(err)
}
err = demo2()
if err != nil {
    panic(err)
}
err = demo3()
if err != nil {
    panic(err)
}
Enter fullscreen mode Exit fullscreen mode

Run it on the Go Playground → https://play.golang.org/p/UZtbUjmv1e5

In the first line we declare the err variable, and then when we call demo2 and demo3 the variable is already declared, so we use = instead of :=, which would redeclare the variable.

This code is perfectly valid, but what would happen if we were working on our code and needed to add another function call before the call to demo1? Perhaps we need to call demo2 beforehand.

Well, chances are we would add something like the following code to the top of our function.

err := demo2()
if err != nil {
    panic(err)
}
Enter fullscreen mode Exit fullscreen mode

Run it on the Go Playground → https://play.golang.org/p/tqMQDQ_gmTe

We need to declare err with this function call, because it hasn't been declared yet. Remember, we are adding this before the call to demo1 in our original code, but we now have two declarations of err inside the same scope; this will cause our compiler to complain and our code won't compile.

prog.go:8:6: no new variables on left side of :=
Enter fullscreen mode Exit fullscreen mode

To fix this, we need to go edit the line where we call demo1 and update that code to use = instead of :=.

err := demo2()
if err != nil {
    panic(err)
}
err = demo1()
if err != nil {
    panic(err)
}
// ... unchanged
Enter fullscreen mode Exit fullscreen mode

Run it on the Go Playground → https://play.golang.org/p/3vE1FnA763a

A similar annoyance can occur when you delete existing code. For instance, if we were to no longer need the first function call - currently a call to demo2 - and we removed it from our code, we would need to either manually declare the err variable, or go update the very next function call that uses it to use := instead of =.

var err error
err = demo1()
if err != nil {
    panic(err)
}
err = demo2()
if err != nil {
    panic(err)
}
// ... unchanged
Enter fullscreen mode Exit fullscreen mode

Run it on the Go Playground → https://play.golang.org/p/MkTkEZoUlql

If you expect to be editing code enough that using := will be a nuisance, consider simply declaring the variable ahead of time in your function (like we did in the last example). This is especially common with errors, as you almost always need a variable to capture errors and it isn't uncommon to need to add new code before the first time you declare it.

Sometimes if scoped variables can also help reduce the frequency with which this issue comes up, but as we saw earlier we can't always use these without sacrificing readability to some degree. Unfortunately, there isn't a set of rules we can follow to determine what the best approach is at any time; it ends up being a judgement call. But don't worry - as you get more familiar with Go you will start to gain some intuition that helps you make decisions like this. Until then, just keep practicing!

Looking for practice projects to code in Go?

I am releasing new exercises for my FREE course, Gophercises, almost every week. In each exercise you are presented with a relatively small project that should take you anywhere from an hour to a few hours to complete. Once you take a stab at the exercise you can watch screencast videos where I code a solution and explain what I'm doing as I write my code.

Whether you solve the problem on your own or just want to watch me code a solution, each exercises is curated to teach you something unique. Some are designed to teach you about a new library, others to introduce new coding techniques, and some focus on broader subjects like writing and benchmarking concurrent code.

Discussion (3)

Collapse
lietux profile image
Janne "Lietu" Enberg • Edited on

One of the most common uses for if ..; .. { } for me has been map access.

var data = map[string]string{}
func addToMap(key string, value string) {
  if _, ok := data[key]; ok {
    fmt.Printf("%s already in map", key)
    return
  }

  data[key] = value
}

Alternatively use !ok if you're interested in when it's NOT in the map. For anyone not familiar with _ it basically means "throw this away", i.e. in that if block I don't care about the stored value.

Collapse
joncalhoun profile image
Jon Calhoun Author

You are right - this is another very common use case and you can almost always use the initialization statement since your maps shouldn't typically have names like omgThisMapHasASuperLooongNameThatIsSoAnnoyingToType 😀

Collapse
auct profile image
auct • Edited on

use your first code sample everywhere