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
}
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
}
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)
}
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
}
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!")
}
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)
}
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 inbreak
,continue
,goto
, orreturn
— the unnecessaryelse
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)
}
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)
}
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 :=
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
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
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.
Top comments (3)
One of the most common uses for
if ..; .. { }
for me has been map access.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 thatif
block I don't care about the stored value.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
😀use your first code sample everywhere