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

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
}

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
}

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

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
}

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

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

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

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)

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

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

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 :=

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

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

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!

Just getting started with Go?

Go is an awesome language whether you are new to programming or have experience in other languages, but learning a new language can be a struggle without the right resources.

To help save you time and get you off to a great start, I have created a guide to learning Go that you can get for FREE! Just sign up for my mailing list (down there ↓) and I'll send it to your inbox.

You will also receive notifications when I release new articles and courses that I think will help you our while learning Go.

Avatar of Jon Calhoun
Written by
Jon Calhoun

Jon Calhoun is a full stack web developer who also teaches about Go, web development, algorithms, and anything programming related. He also consults for other companies who have development needs. (If you need some development work done, get in touch!)

Jon is a co-founder of EasyPost, a shipping API that many fortune 500 companies use to power their shipping infrastructure, and prior to founding EasyPost he worked at google as a software engineer.

Related articles

Spread the word

Did you find this page helpful? Let others know about it!

Vote on Hacker News

Sharing helps me continue to create both free and premium Go resources.

Want to discuss the article?

See something that is wrong, think this article could be improved, or just want to say thanks? I'd love to hear what you have to say!

You can reach me via email or via twitter.

Recent Articles All Articles Mini-Series Tags About Me Go Courses

©2018 Jonathan Calhoun. All rights reserved.