AlgoMaster Logo

If with Init Statement

Last Updated: May 22, 2026

Medium Priority
9 min read

Go lets you tuck a short statement into an if before the condition, separated by a semicolon. That small piece of syntax shapes a huge chunk of real-world Go code, especially anywhere errors, map lookups, or type assertions show up. This lesson covers the form, its scoping rules, and the patterns where you'll see it every day.

The Basic Form

A regular if looks like the one from the previous lesson: condition first, body in braces.

The init form adds a short statement before the condition. It runs first, then the condition is evaluated. The two parts are separated by a semicolon.

Two things happened on that one line. First, cartTotal := 89.50 declared and assigned a new variable. Second, cartTotal > 50.0 was evaluated as the condition. If the condition is true, the body runs and can use cartTotal directly.

The init statement isn't limited to declarations. Anything that's a valid simple statement works there: short declarations (:=), assignments, increment/decrement, function calls. In practice, almost every if with an init you'll write or read uses := to declare a new variable, because that's what makes the form so useful.

Scope: The Variable Lives Inside the If

The variable declared in the init statement is scoped to the entire if/else if/else chain, and nowhere else. Once the chain ends, the variable is gone. You can't use it on the next line.

If you uncomment the second fmt.Println, the compiler refuses to build with undefined: cartTotal. The variable existed only between the { and the } of the if, and the closing } ends its life.

That tight scope is the whole point. A value that's only relevant to one check, like a function result you want to test, doesn't need to hang around in the outer function. The init form keeps temporary values from leaking into surrounding code where they'd add noise and risk being reused by accident.

Here's what the lifetime looks like as a picture.

The diagram traces a single value through the construct. Outer code can't see x at all. The init creates it. Both branches of the chain see it. Once the closing brace runs, the name is gone, and the outer scope is back to not knowing it ever existed.

The Variable Is Visible in Else Too

The init variable is visible in the else and any else if clauses, not just the first branch.

itemCount is declared in the init, then used in the first condition, the second condition, and both bodies. Each branch can read or modify it. After the whole chain ends, itemCount is gone.

You can think of the entire if/else if/else construct as one block that the init variable belongs to. The braces of the individual branches don't reset its scope, they just delimit which branch's code runs.

The Idiomatic Error Pattern

The most common use of if with init in Go is checking errors returned from functions. Go's standard pattern looks like this:

The init statement declares two variables, price and err, from the function's two return values. The condition checks err. If parsing failed, the if branch reports the error. If it succeeded, the else branch uses price.

This shape, declare-and-check on one line, is the canonical way to handle a call that returns a result plus an error you only care about briefly. Most Go developers prefer it over the alternative:

Both work. The init version is preferred when neither price nor err needs to be used after the check. If you do need price later, after handling the error, write it without the init form so the variable lives in the outer scope.

A more realistic example, parsing several pieces of customer input where each step has its own check:

Each branch's init brings in fresh variables for that step. price is in scope for the rest of the chain, and quantity is in scope from the else if onward.

In production Go, the more common pattern is to use early returns instead of else if:

Here, the init form fits less well because price and quantity are needed after their checks. Use the init form for variables you only need during the check itself. Use the plain form when the values flow into later code.

Comma-Ok with Maps

Go's map lookup returns two values: the value at the key, and a boolean saying whether the key was present. This pairs perfectly with the init form. You can look up a key and decide what to do based on whether it was found, all on one line, without leaking the lookup result into the surrounding code.

The first lookup finds "keyboard", so ok is true and the if branch runs. The second lookup misses, ok is false, and the else branch runs. In both cases, count and ok exist only inside their if chain.

The distinction between "key not in map" and "key in map with the value-type zero value" matters here. stock["mouse"] is 0, but "mouse" is in the map. Without the ok check, you can't tell those apart, since a missing key also returns 0.

"mouse" is present, so ok is true even though the count is 0. The init form makes this kind of presence-aware lookup a one-liner. The init form makes this style of presence-aware lookup a one-liner.

Comma-Ok with Type Assertions

Type assertions on interface values use the same comma-ok pattern, and they fit just as well into the init form. A type assertion asks "is the value inside this interface actually of type T?", and the two-value form gives you the typed value plus a bool telling you if the assertion held.

Each branch tries one type assertion in its init. If it works, the typed variable (name or price) is available in the body. If it fails, ok is false and we move on. The third call passes an int, which neither assertion matches, so the final else runs.

The relevant piece for this lesson is the shape: if v, ok := x.(T); ok { ... }. That shape is everywhere in real Go code.

Why This Form Exists

Three reasons, in order of importance.

Tight scope. A temporary value used only for a single decision shouldn't outlive the decision. The init form keeps that value confined to the if, so the rest of the function isn't cluttered with names that no longer matter. After the if ends, the name is free for reuse and there's no chance of accidentally reading a stale value.

No "did I shadow it?" worry. When you write err := doSomething() at function scope, you have to think about whether err already exists in the outer scope. The init form sidesteps this entirely. The init scope is fresh, and the variables you declare there can't be confused with anything outside.

Reads as one thought. "Try to do X, and if there's an error, handle it" is one idea. Splitting it across two lines, declaration on one and check on the next, breaks that flow. The init form keeps the call and its check together visually, which mirrors how you think about it.

A counter-example shows what gets uglier without it. Imagine three map lookups in a row, each with its own check:

Versus:

The second version doesn't share variables across the three lookups. Each one has its own count and ok, scoped to its own if. There's no reuse confusion, no "did I reset ok between lookups", no stale value risk.

Comparison with Plain If

Side by side, the plain if and the init form cover slightly different needs.

FormUse WhenExample
Plain ifThe value being checked is needed after the if.total := compute(); if total > 0 { ... }; use(total)
if with initThe value is only needed during the check.if total := compute(); total > 0 { ... }
Plain ifThe value already existed before the if.if cart.Total > 0 { ... }
if with initThe check needs a fresh variable not seen elsewhere.if v, ok := m[k]; ok { ... }

A simple way to choose: if the variable's lifetime extends past the if, declare it normally. If the variable exists only to make the check possible, use the init form.

Both compile to the same machine code. The choice is about readability and scope discipline.

Multiple Statements? No.

The init slot holds exactly one simple statement. You can declare and assign in one statement (using :=) and you can have a tuple assignment in one statement (a, b := f()), but you can't chain multiple separate statements with extra semicolons.

If you need two things to happen before the check, do one before the if and the other in the init, or build a helper function that returns what you need in one call.

The single function call in the init returns two values, which keeps the init slot to one statement while still doing useful work.

Mixing Init Variables with Outer Variables

The init slot can read variables from the surrounding scope when computing its right-hand side. It can also assign back to outer variables, though that's rare and usually not what you want.

basePrice and taxRate come from outer scope. finalPrice is declared in the init and used in the condition and body. After the if, basePrice and taxRate are still around. finalPrice is not.

If you wrote finalPrice = (with = instead of :=) in the init, you'd be assigning to an outer variable instead of declaring a new one. That works, but the result lasts past the if, defeating one of the form's main benefits. Stick with := unless you have a specific reason to want the outer-scope effect.

Shadowing Inside the Init

A subtle issue worth flagging. If an outer-scope variable has the same name as the init declaration, the init creates a new variable that shadows the outer one inside the if. The outer one is unchanged.

Two different count variables exist here. The init's count := 5 is a brand-new variable that lives inside the if. The outer count := 100 is untouched. After the if, the outer count is what fmt.Println sees.

This is sometimes useful (you want a local override) and sometimes a bug source (you meant to update the outer variable and accidentally created a new one). The Go vet tool can flag suspicious shadowing if you turn that check on. When in doubt, rename one of the two, or use = instead of := to make the intent explicit.

A Worked Example: Order Lookup

Putting these pieces together, here's a small program that uses the init form three times: once for a map lookup, once for a parsing call that returns an error, and once for a type assertion.

Three init forms, each with its own pair of variables, each scoped to its own if/else chain. None of the temporary variables (name, ok, priceText, found, price, err) leak out to the surrounding loop body. The loop variable p is the only thing that survives between iterations.

In production code, you'd probably split this into smaller functions rather than nest three layers of if. The example deliberately shows the nesting so you can see all three patterns in one place.

When Not to Use Init Form

The init form is so common that it's tempting to use everywhere. A few cases where the plain form is better:

  • You need the value after the `if`. If price or err is used later, declare it outside the if.
  • The init expression is long or complex. Squeezing if x := computeSomethingWithThreeArgs(a, b, c); x > threshold onto one line hurts readability. Split it.
  • Multiple things to compute. The init slot holds one statement. Don't try to cram setup into it.
  • The condition reads better without it. if cart.IsEmpty() { ... } is cleaner than forcing an init for the sake of style.

Idiomatic Go uses the init form where it adds clarity (errors, comma-ok, brief computed checks) and avoids it where it forces awkward one-liners.