AlgoMaster Logo

If-Else Statements

Last Updated: May 22, 2026

High Priority
11 min read

Most programs need to make decisions. Apply a discount when the cart total crosses a threshold, reject a checkout when stock runs out, mark a customer as VIP after their tenth order. Go expresses these decisions with if, else if, and else, and the syntax is tighter than what you'll find in many other languages. This lesson covers the rules, the operators that go inside conditions, and the common patterns Go developers use daily.

The Basic If Statement

An if statement runs a block of code only when a condition is true. The condition goes after the if keyword, and the block goes inside curly braces.

The condition cartTotal > 50.0 evaluates to true, so the body runs and the program prints Free shipping applied. After the if block ends, execution continues with the next line.

Two things about that example are worth highlighting because they differ from many other languages.

No parentheses around the condition. You write if cartTotal > 50.0, not if (cartTotal > 50.0). Parentheses around the whole condition are allowed by the parser but gofmt strips them out, and idiomatic Go code never uses them. If you've used C, Java, or JavaScript, this will feel bare at first. It stops feeling bare quickly.

Braces are mandatory. Even if the body is a single statement, you must wrap it in { and }. There is no "one-line if" in Go. The opening brace also has to sit on the same line as the if. Try putting it on the next line and the compiler will reject the file.

Here's what won't compile:

The compiler reports expected '{', found 'fmt'. Go is strict about this on purpose. A whole class of bugs in C-style languages comes from a brace-less if plus a later edit that adds a second line under it. By forcing braces, Go takes that bug off the table.

Else and Else If

When you want to run one block on true and a different block on false, add an else clause. The else keyword must sit on the same line as the closing brace of the if.

For three or more branches, chain them with else if:

Go checks the conditions top to bottom and runs the body of the first one that's true. The rest are skipped. The final else is optional and acts as the fallback when nothing else matched. Once a branch runs, control jumps past the entire chain.

The placement rules are strict. The } else {, } else if cond {, and the opening { of an else if all have to be on the same physical line as the closing brace before them. Put else on its own line and the compiler complains. This is gofmt territory: write it however you want, run gofmt, and the formatting becomes correct.

Comparison Operators in Conditions

The condition inside an if must be a value of type bool. Comparison operators are the most common way to produce one. Go has six of them, and they work on any types where the comparison makes sense.

OperatorMeaningExample
==Equal toprice == 19.99
!=Not equal tostatus != "shipped"
<Less thanstock < 5
>Greater thantotal > 100.0
<=Less than or equal toquantity <= 0
>=Greater than or equal torating >= 4

All six work on numbers (int, float64, and the rest of the numeric family). The two equality operators (== and !=) also work on strings, booleans, and any type that's comparable. The four ordering operators (<, >, <=, >=) work on numbers and strings, where strings compare lexicographically (byte by byte).

That last if works, but inStock != false is the long way around. Because inStock is already a bool, you can just write if inStock. We'll come back to that in the truthiness section.

One trap to watch for: comparing operands of different types is a compile error in Go, even when the values look like they should match. Comparing an int to a float64 directly won't compile. You have to convert one to the other first.

The conversion float64(stock) produces a new float64 with the same numeric value, and now both sides of the comparison have the same type. Untyped constants like the literal 5.0 don't have this problem, because the compiler infers their type from context.

Logical Operators

Real decisions usually combine multiple conditions. Go gives you three logical operators for that.

OperatorMeaningReads As
&&Logical ANDBoth sides must be true
||Logical ORAt least one side must be true
!Logical NOTFlips true to false and vice versa

Both && and || short-circuit. With &&, if the left side is false, the right side is never evaluated. With ||, if the left side is true, the right side is never evaluated. This matters for both correctness and performance.

The first condition combines two checks with &&. Both have to pass for the body to run. The second uses ||: either a coupon or a big enough cart triggers the discount. Since hasCoupon is false and the cart total is 120.0 (not >= 200.0), neither side is true, and the else runs. The third uses ! to invert a bool, which reads naturally as "if no coupon".

Short-circuiting is the reason the following pattern is safe even when the slice is empty:

The left side, len(orders) > 0, is false. Because of short-circuiting, the right side orders[0] == "Order-1" is never evaluated. If it were evaluated, indexing into an empty slice would panic. Reordering those two checks would crash the program. The && operator's left-to-right order matters.

No Implicit Truthiness

This is one of the biggest differences between Go and languages like JavaScript, Python, or C. In Go, the condition of an if must be exactly a bool. Nothing else counts.

You cannot write if x where x is an int and expect "any non-zero value is true". The compiler rejects it.

The compiler error is non-bool ... (type int) used as if condition or a similar message naming the actual type. You have to write the comparison explicitly:

orders is a nil slice, so len(orders) is 0 and the third condition is false. customer is a nil pointer, so the fourth condition is false too.

The trade-off here is verbosity for clarity. Code that means "if the count is greater than zero" reads exactly like that. There's no ambiguity about whether if count would treat -1 as falsy (it wouldn't in JavaScript), or whether if name would treat the string "0" as truthy (it would in JavaScript, but not in PHP). Go simply forbids the question.

The flip side: variables of type bool go straight into the condition, no comparison needed.

Writing if isVIP == true compiles, but it's not idiomatic. The variable is already a bool. if isVIP says exactly the same thing with less code. Same for if !isVIP instead of if isVIP == false. Most linters flag the longer form.

Validation and Guard Clauses

A guard clause is an early check at the top of a function that handles bad input and returns immediately. It's one of the most common uses of if in Go code, and it's especially common because Go functions often return both a value and an error.

Notice the pattern. Each guard clause checks one thing, returns early on failure, and lets the happy path stay flat and easy to read. The body of applyDiscount doesn't end with a giant nested if/else tree. Instead, every check that could go wrong has its own if with an early return, and the real work happens at the bottom.

This is sometimes called the "no nesting" or "early return" style. Compare it to the alternative:

Both versions work. The first is the one you'll see in real Go codebases. The errors are next to the conditions that produce them, and the main logic is at the bottom with no indentation. The second version pushes the real work into a nested block, which is harder to follow.

There's even a gofmt-friendly style rule about this. After a return, continue, or break, you don't need an else. Write the early return, then continue with the rest of the function. The else would be redundant.

The linter staticcheck flags the first form. The second reads better and indents less.

Decision Flow for a Tiered Discount

When you have several conditions that lead to different outcomes, drawing the flow makes the structure obvious. Here's the tiered cart discount from earlier as a flowchart.

The diagram shows what the if / else if / else if / else chain does at runtime. Each diamond is one condition. A Yes exit takes the matching action and skips the rest. A No exit moves to the next check. If every check fails, the final else branch runs. The order of the checks matters: writing the >= 50 check before the >= 200 check would give every big spender a 5% discount instead of 20%, because the first matching branch wins.

Translating that flow back into Go:

The order of the branches is the order of the checks in the diagram. With ranges that overlap (200, 100, 50 are all greater than 50), writing the largest threshold first is essential. This is a frequent source of bugs in real code, so it's worth slowing down to verify.

Combining Conditions for Real Decisions

A single product listing on an online store might decide whether to show a "Buy Now" button based on stock count, whether the item is discontinued, and whether the user is signed in. That's three pieces of state combined into one decision.

A few things make this readable. Each condition expresses a single business rule. The most common path (item available, signed-in user) comes first. The ! reads naturally as "not", as in "not discontinued". And the final else handles whatever wasn't covered, which in this case is "signed out".

When chains get longer than four or five branches, it's often worth splitting them. Extract a helper function:

The helper uses guard clauses, one per rule, and each returns immediately. The main function reads like a table of test cases. This style scales much better than nesting more else if branches into the original chain.

For longer chains where every branch compares the same variable against different constants, Go's switch statement is usually clearer.

The = vs == Mistake

One of the most common bugs Go beginners hit is using = (assignment) where they meant == (equality). In languages like C and Python, the = in an if is either legal (it returns the assigned value) or it silently produces unexpected behavior. In Go, the compiler stops you cold.

The compiler error is something like expected '==', found '='. Go's if condition must be an expression that produces a bool, and an assignment statement isn't an expression at all. The compiler tells you exactly what you typed and what you probably meant.

The double = is the equality operator. The single = is the assignment operator. Reading the code out loud helps: "if status is equal to shipped" maps to ==, while "set status to shipped" maps to =. Practice helps too; you'll stop making the swap after a few weeks of writing Go.

The Dangling Else and Why Go Doesn't Have It

Languages that allow brace-less if statements run into the "dangling else" problem. Consider this C-style code:

Which if does the else belong to? The indentation suggests the outer if, but the grammar attaches else to the nearest unmatched if, which is the inner one. So do_y() runs when a is true and b is false, not when a is false. This is one of the oldest sources of subtle bugs in C and its descendants.

Go sidesteps the whole question by requiring braces. There is no way to write an if without { }, so the structure is always visible. The equivalent Go code has to commit to one of two readings:

Whichever you mean, the braces make it explicit, and a reader can tell at a glance which if each else pairs with. This rule, more than any other, is why Go code tends to look uniform across teams.

A Few Patterns You'll Use Constantly

Some if patterns come up so often in Go code that they're worth recognizing on sight.

Error checks after a function call:

The function returned a value and an error. You check the error, return early if it's non-nil, and otherwise use the result. You'll see this pattern in almost every Go example.

Nil checks on pointers and slices:

The guard clause stops the function from dereferencing a nil pointer. Without it, c.Name on a nil pointer would panic. This is the standard use of if in defensive Go code.

Empty checks on slices and strings:

len(cart) == 0 works for both nil slices and empty slices, which is exactly what you want. Same for strings: len(s) == 0 and s == "" are equivalent for the empty-string check, and most Go codebases use s == "".

Boundary checks on numbers:

You'll write code like this any time you accept a number from input or from another part of the system. The <= 0 and > 100 together cover the invalid ranges, and the else is the happy path.

A Quick Note on the Init Form

Go also lets you tuck a short statement in front of the condition, so the variable lives only inside the if and its else branches:

This form is useful for keeping variables out of the surrounding scope and is especially common with error handling. For now, know that it exists, and that everything you've learned about conditions, operators, and branches applies to it identically.