JavaRush /Courses /Go SELF /Multiple assignment and :=

Multiple assignment and :=

Go SELF
Level 6, Lesson 5
Available

1. Multiple assignment is interesting

When you’re just starting out, it seems like: “Well, assignment is simple — write a = ..., then b = ..., and you’re done.” The problem appears when the values are connected: you need to update several variables so that they “move” at the same time and don’t interfere with each other. This is where Go gives you a very handy tool: multiple (parallel) assignment.

In Go, you can assign multiple values to multiple variables at once:

a, b = 10, 20

This is read as: “put 10 into a, put 20 into b.”

Key idea: the right-hand side is evaluated completely

The right-hand side is evaluated completely, and only then are the results distributed into the variables on the left. This means you can do updates that are easy to break in a “step-by-step” version.

Let’s look at an example that feels a little like magic, but is actually honest:

package main

import "fmt"

func main() {
	x := 1
	y := 2

	x, y = y, x+y
	fmt.Println(x, y) // 2 3
}

The important part is that x+y is computed using the old values (1+2), not using the already updated x. That’s why the result is predictable: x became 2, and y became 3.

Swapping values without a temporary variable

When a programmer first sees a swap without a “third cup,” they usually think: “This is witchcraft, call a compiler exorcist immediately.” In reality, it’s just multiple assignment.

Instead of this (classic) approach:

tmp = a
a = b
b = tmp

in Go you write:

package main

import "fmt"

func main() {
	a := 5
	b := 9

	a, b = b, a
	fmt.Println(a, b) // 9 5
}

This is useful in tasks where you want to “normalize” input. For example, the user enters two numbers, and you want the first one to be less than or equal to the second.

Mini-app: range normalizer

To make the examples more concrete, let’s agree on this: we have a mini program that reads two integers from and to, and it wants to work with the range in the correct order: from <= to. If the user enters them the other way around, we’ll neatly swap them.

Note: here multiple assignment is not a “trick for the sake of a trick,” but a direct tool for the logic.

package main

import "fmt"

func main() {
	from := 0
	to := 0

	fmt.Scan(&from, &to)

	if from > to {
		from, to = to, from // swap
	}

	fmt.Printf("range: %d..%d\n", from, to) // for example: range: 3..10
}

Notice that inside if we use =, not :=. This is not an accident, but protection from a very nasty mistake that we’ll get to soon.

2. := and the “at least one new name” rule

You already know the short declaration :=: it means “declare a variable and assign a value right away.” But today we care about the subtler part: what happens when there are multiple names on the left.

In the current scope (in the current block {...}), the := construct is allowed only when at least one name on the left is declared for the first time in this block.

That is:

  • if all names already exist in this block, := is forbidden;
  • if some names already exist but at least one new one appears, := is allowed, and the existing variables simply receive new values.

This rule sounds dry, but in practice it saves you from ambiguity: the compiler must understand whether you are creating a new variable or updating an old one.

Example: one new name — works

package main

import "fmt"

func main() {
	x := 10
	y := 20

	x, z := y, 30 // z is a new name, so := is allowed
	fmt.Println(x, y, z) // 20 20 30
}

In this line, two actions happen at once: x is reused, and z is created.

Example: all names already exist — does not work

package main

import "fmt"

func main() {
	x := 1
	y := 2

	// x, y := 3, 4    // not allowed: there is no new name in this block
	x, y = 3, 4     // correct
	fmt.Println(x, y) // 3 4
}

If you uncomment the line with :=, the compiler will refuse to compile the program. And that’s good: it prevents you from “accidentally redeclaring” something you meant to update.

3. Shadowing: updating the wrong thing without noticing

Now for the “tastiest” and most unpleasant part: shadowing (hiding a variable).

Shadowing is a situation where, in an inner block, you create a new variable with the same name as in an outer block. Inside the block you see the new variable, while the outer one remains unchanged. The bug is tricky because the program can look “logical” and even print “correct” numbers... but only inside the block.

The shortest shadowing example

package main

import "fmt"

func main() {
	x := 10

	if x > 0 {
		x := 99
		fmt.Println("inside:", x) // inside: 99
	}

	fmt.Println("outside:", x) // outside: 10
}

Inside if, the line x := 99 does not “update x,” but creates a new variable named x, which exists only inside the if.

If you expected 99 outside as well, that’s the classic trap.

Why it is so easy to do accidentally

Because := is very convenient, and your hand starts writing it “by inertia.” Especially inside if, for, and other blocks. But inside a block you sometimes need to stop and ask yourself: “Do I want to create a new variable or update an existing one?”

Shadowing in the range normalizer

Let’s return to the program with from and to. We want to swap the values if from > to.

Here is the correct version:

if from > to {
	from, to = to, from
}

And here is a version that looks suspiciously similar... but does something else:

package main

import "fmt"

func main() {
	from := 0
	to := 0

	fmt.Scan(&from, &to)

	if from > to {
		from, to := to, from // shadowing: created new from/to inside if
		fmt.Printf("inside: %d..%d\n", from, to)
	}

	fmt.Printf("outside: %d..%d\n", from, to)
}

What is happening here?

Inside if, we really do get the “correct” order and even print it nicely. But outside, everything remains as it was. This is one of those bugs that is perfectly disguised as “but it works, I can see the output.”

if with an init statement: another risk zone

Go has a popular if form where you can do a short declaration before the condition:

if v := something; v > 0 {
    ...
}

This construct is convenient, but it creates yet another chance to accidentally hide a variable.

Watch closely:

package main

import "fmt"

func main() {
	result := 100

	if result := 5; result < 10 {
		fmt.Println("inside:", result) // inside: 5
	}

	fmt.Println("outside:", result) // outside: 100
}

Inside if, result := 5 creates a new result that exists only until the closing }. The outer result is not affected at all.

If you wanted a temporary variable — great. If you wanted to update the outer one — you need to write it differently (usually result = 5, and then the assignment must be before the if).

4. Global variables

So far our program has one function main, and you can keep almost everything “inside.” But as soon as you have several functions, you often need something shared: settings, constants, lookup tables, “debug mode,” common messages. In Go, for that you have package-level variables — they are often called “global,” although more precisely they are package-level.

Such variables are declared outside functions, directly in the file:

package main

import "fmt"

var appName = "Range Normalizer" // global (package-level) variable

func normalize(from, to int) (int, int) {
	if from > to {
		from, to = to, from
	}
	return from, to
}

func main() {
	from, to := 10, 3
	from, to = normalize(from, to)
	fmt.Println(appName, from, to) // Range Normalizer 3 10
}

Pay attention to two important things:

  1. Outside functions, you cannot use :=.
    Short declaration works only inside blocks {...}. At the package level you write var (or const).
  2. Global variables are visible to all functions in this package, so they really help when the number of functions grows: you don’t need to thread the same thing through parameters every time.

But then a classic trap appears immediately: shadowing with global variables. Inside a function, it’s very easy to habitually write := and create a new local variable with the same name — and “accidentally disable” your global one.

package main

import "fmt"

var debug = false

func main() {
	debug := true // shadowing! created a new local debug
	fmt.Println("inside:", debug) // inside: true
}

func other() {
	fmt.Println("global:", debug) // will see the global: false
}

That’s why it’s useful to be careful with global variables: when you see :=, always ask yourself: “Am I creating a new variable or updating an existing one?”

5. Practice: diagnostics and a robust style

Diagnostics via fmt.Printf and inside/outside

When you suspect shadowing (or you’re simply not sure which variable you are touching right now), it helps to print values at block boundaries. This is a classic approach: until you have a complex project, Printf is your best friend.

Here’s a mini technique: print “inside/outside” and see whether the outer variable changes.

package main

import "fmt"

func main() {
	x := 1
	fmt.Printf("start x=%d\n", x) // start x=1

	if x == 1 {
		x := 2
		fmt.Printf("inside x=%d\n", x) // inside x=2
	}

	fmt.Printf("end x=%d\n", x) // end x=1
}

If you see that “inside” and “end” differ unexpectedly, you almost certainly did shadowing by accident.

A small historical note: the redeclaration rule in Go (where := can be used if at least one new name appears) is a compromise between convenience and code readability, and many debates have been fought over it.

Multiple assignment + input: be careful with _

fmt.Scan returns two values: the number of successfully scanned values and an error. We don’t need to dive deep into errors today, but it’s useful to understand that a function can return multiple results — and that pairs perfectly with multiple assignment.

If you just want to read two numbers and not analyze how many were actually read, you can temporarily ignore the returned values using _ (blank identifier). This is legal and often convenient in learning examples:

package main

import "fmt"

func main() {
	a := 0
	b := 0

	_, _ = fmt.Scan(&a, &b)
	fmt.Printf("a=%d b=%d\n", a, b) // for example: a=3 b=10
}

Note: _ is a “trash can,” not a variable. You can “throw away” a value there, but you can’t read it back.

When := is better, and when = is better

It’s useful to build a simple habit that saves hours: := is good for first initialization, while = is for later updates. It sounds like advice on the level of “drink water,” but it works.

To avoid keeping everything in your head, here’s a small reminder table:

Syntax What it does When to use
x := 10
declares x and assigns 10 when the variable appears for the first time in the block
var x int
declares x with a zero value when you want to explicitly set the type or postpone assignment
x = 10
updates an existing variable x when x is already declared and you are changing the value
a, b = b, a
updates multiple variables at the same time swaps, normalization, related updates

If you are writing inside a block and catch yourself thinking “I just need to update it,” you almost always need =, not :=.

6. Common mistakes

Mistake #1: using := “out of habit” inside if and accidentally doing shadowing.
It’s very easy to write value := value + 1 inside a block, thinking you’re updating the variable. In reality, you create a new variable with the same name that lives exactly until the end of the block. The symptom is usually this: inside if everything prints “correctly,” but after if the value seems to “roll back.”

Mistake #2: not understanding the “at least one new name” rule and being surprised why := sometimes works and sometimes doesn’t.
In one line, x, z := y, 30 compiles because z is a new name. But the line x, y := 3, 4 in the same block does not compile because x and y already exist. This is not a whim, but a language rule: := must declare something; otherwise you need =.

Mistake #3: trying to swap values “step by step” and accidentally losing data.
A construct like a = b; b = a is almost always wrong: after the first line, you have lost the old value of a. In Go, a correct swap is done in one line: a, b = b, a. This is exactly the case where multiple assignment is not decoration, but a correctness guarantee.

Mistake #4: keeping identical names in different blocks without a reason.
Sometimes shadowing happens “by accident,” and sometimes “because it just worked out that way.” But for readability it’s better to avoid identical names if the variables are truly different in meaning. When you see x outside and x inside, your brain is forced to constantly remember the scopes, and that’s an expensive cognitive operation (programmers are people too; sometimes they don’t feel like thinking).

Mistake #5: not checking “inside/outside” while debugging and wasting time on mysticism.
Shadowing is one of those bugs you can catch in a minute with a simple fmt.Printf("inside...") and fmt.Printf("outside..."). If you see a discrepancy, it means a new variable was created somewhere. If you don’t, the problem is elsewhere — and that is also useful information.

1
Task
Go SELF, level 6, lesson 5
Locked
Swap Positions
Swap Positions
1
Task
Go SELF, level 6, lesson 5
Locked
Correct range
Correct range
1
Task
Go SELF, level 6, lesson 5
Locked
Pair steps
Pair steps
1
Task
Go SELF, level 6, lesson 5
Locked
Debug flag
Debug flag
1
Survey/quiz
Variables, types, 6 level, 5 lesson
Unavailable
Variables, types
Variables, types
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION