Go, chapter by chapter · Lesson 3 of 62 · about 30 min
Variables, scope & constants
Every way to declare a value, where each name is visible, and why constants behave differently from variables.
On this page · 11 sections
Four ways to declare a variable#
var a int // type only: a starts at 0 (its zero value)
var b int = 10 // type and value
var c = 10 // value only: type inferred as int
d := 10 // short form: declare + infer, functions onlyAll four create an int variable. In practice:
- Use
:=inside functions almost always. It is short and the type is obvious from the value. - Use
var x Twhen you want the zero value to start with, such as a counter or an empty builder. - Use
varat package level, where:=is not allowed.
= assigns to an existing variable; := declares a new one. Writing x := 1 twice in the same scope is an error ("no new variables on left side"). The exception: a, err := f() is allowed if at least one name on the left is new.
Zero values#
There is no uninitialized memory in Go. Every type has a zero value that new variables start with:
| Type | Zero value |
|---|---|
| numbers | 0 |
string | "" |
bool | false |
| pointers, slices, maps, channels, functions, interfaces | nil |
| structs | every field set to its own zero value |
Good Go code designs types so the zero value is immediately useful. A strings.Builder or sync.Mutex works without any setup.
Naming#
- Names start with a letter or underscore, followed by letters, digits or underscores.
- Use
camelCase. Short names for short scopes (i,n,s), descriptive names for long ones (maxRetries). - A name that starts with a capital letter is exported: other packages can use it. Lowercase names are private to their package. This is Go's only visibility rule.
_is the blank identifier. Assigning to it throws a value away:_, err := f().
Scope: where a name is visible#
A name is visible from where it is declared to the end of the block ({ … }) that contains it.
// Scope sketch (not a full program)
var appName = "kurs" // package scope: visible in every file of the package
func main() {
total := 0 // function scope
for i := 0; i < 3; i++ { // i exists only in the loop
total += i
}
// fmt.Println(i) would not compile: i is out of scope here
}Blocks nest, and an inner block can declare a name that already exists outside. The inner one shadows the outer one until the block ends:
| Line | Code | Outer x | Inner x |
|---|---|---|---|
| 1 | x := 1 | 1 | |
| 2 | if true { | 1 | |
| 3 | x := 2 | 1 | 2 (new variable) |
| 4 | x++ | 1 | 3 |
| 5 | } | 1 | gone |
| 6 | fmt.Println(x) | prints 1 |
Constants#
A constant is a value fixed when the program compiles. It cannot change, and it can only hold numbers, strings, booleans or runes.
const rate = 16000.0
const greeting = "halo"Constants are often untyped: const big = 1 << 40 has no type until you use it, then adapts. That is why time.Second * 2 works without conversion. It also means constant arithmetic is exact: 0.1 + 0.2 == 0.3 is true for constants even though it is false for float variables.
iota: numbering made easy
Inside a const block, iota starts at 0 and increases by one per line. Lines without a value repeat the previous expression:
type Level int
const (
Beginner Level = iota // 0
Intermediate // 1
Expert // 2
)This is Go's way of making enums.
Grouping declarations#
Related declarations can share one keyword:
var (
host = "localhost"
port = 8080
)
x, y := 1, 2 // several at once
x, y = y, x // swap, with no temporary variableMore examples#
Reading input: the book's temperature converter
The book's chapter ends with a program that reads a number from the user. fmt.Scan reads space-separated values into the variables you point it at. Here the input comes from a strings.Reader so the program runs the same everywhere; with real input you would pass os.Stdin (and fmt.Scan reads from it by default).
package main
import (
"fmt"
"strings"
)
func main() {
input := strings.NewReader("98.6 212 -40")
for {
var f float64
if _, err := fmt.Fscan(input, &f); err != nil {
break // io.EOF: no more numbers
}
c := (f - 32) * 5 / 9
fmt.Printf("%6.1f°F = %6.1f°C\n", f, c)
}
// Feet to meters, the chapter's exercise.
const metersPerFoot = 0.3048
feet := 10.0
fmt.Printf("%.0f ft = %.3f m\n", feet, feet*metersPerFoot)
} 98.6°F = 37.0°C
212.0°F = 100.0°C
-40.0°F = -40.0°C
10 ft = 3.048 mShadowing: the same name in an inner scope
A := inside a block creates a new variable, even if an outer one has the same name. The outer one is untouched. Plain go vet does not report it, but the shadow analyzer and linters such as staticcheck do, because it is a classic source of "why didn't my variable change?" bugs, especially with err.
package main
import (
"fmt"
"strconv"
)
func main() {
x := 1
if true {
x := 2 // a NEW x, only visible inside this block
x++
fmt.Println("inner x:", x)
}
fmt.Println("outer x:", x)
var n int
var err error
if s := "42"; s != "" {
n, err := strconv.Atoi(s) // BUG: shadows the outer n and err
_, _ = n, err
}
fmt.Println("n after the bugged block:", n, err)
if s := "42"; s != "" {
n, err = strconv.Atoi(s) // fix: plain assignment to the outer variables
}
fmt.Println("n after the fixed block:", n, err)
}inner x: 3
outer x: 1
n after the bugged block: 0 <nil>
n after the fixed block: 42 <nil>Constants: untyped, typed and iota expressions
Untyped constants are exact and take whatever type the context needs, which is why const big = 1 << 62 can be used in arithmetic that would overflow if it were evaluated step by step at runtime. iota can appear inside expressions, and _ skips a value.
package main
import (
"fmt"
"time"
)
type ByteSize int64
const (
_ = iota // skip 0
KB ByteSize = 1 << (10 * iota) // 1 << 10
MB // 1 << 20: the expression repeats with the next iota
GB
)
type Weekday int
const (
Sunday Weekday = iota
Monday
Tuesday
)
func (d Weekday) String() string { return [...]string{"Sun", "Mon", "Tue"}[d] }
func main() {
fmt.Println(KB, MB, GB)
const huge = 1 << 100 // fine: untyped constants are exact
fmt.Println(huge >> 98) // 4, computed exactly at compile time
const timeout = 2 * time.Second // an untyped 2 times a typed Duration
fmt.Println(timeout, Monday, Tuesday+0)
var f float64 = 3 // the untyped constant 3 becomes a float64
var i int = 3
fmt.Println(f/2, i/2)
}1024 1048576 1073741824
4
2s Mon Tue
1.5 1Weekday has a String() method, so fmt prints its name instead of its number. That is the fmt.Stringer interface you will meet in the structs and interfaces lesson.
The full program#
package main
import "fmt"
const idrPerUSD = 16000.0 // untyped constant (example rate)
type Level int
const (
Beginner Level = iota // 0
Intermediate // 1
Expert // 2
)
var (
appName = "kurs" // package scope
runs int // zero value 0
)
func convert(idr float64) float64 {
runs++
return idr / idrPerUSD
}
func main() {
var idr float64 = 2_000_000 // underscores make big numbers readable
usd := convert(idr)
fmt.Printf("%s #%d: Rp%.0f = $%.2f\n", appName, runs, idr, usd)
x := 1
if x > 0 {
x := 2 // shadows the outer x
x++
fmt.Println("inner x:", x)
}
fmt.Println("outer x:", x)
a, b := "first", "second"
a, b = b, a
fmt.Println(a, b, Expert)
}kurs #1: Rp2000000 = $125.00
inner x: 3
outer x: 1
second first 2To read input from the keyboard instead of hard-coding idr, replace the line with var idr float64 followed by fmt.Scan(&idr). The & passes the variable's address so Scan can fill it in; you will learn exactly why in the functions and pointers lesson.
Summary#
:=declares and infers inside functions;varworks everywhere and gives zero values.- Every type has a zero value; nothing is ever uninitialized.
- Capitalized names are exported; lowercase names are private to the package.
- Names live until the end of their block, and inner declarations shadow outer ones.
- Constants are compile-time values, often untyped;
iotanumbers enum-like constants.
Check your understanding
Answer before scrolling back up. If you miss one, reread the matching section, then try again.
Q1.Where can you use
x := 5?Q2.What is printed?
x := 1; if true { x := 2; _ = x }; fmt.Println(x)Q3.What does
var count inthold before you assign anything?Q4.In
const ( A = iota; B; C ), what is C?
Practice in Go
Write a table test first, then the solution. On LeetCode, choose Go as the language. Open a hint only after 15 minutes stuck.
Show hint
Kelvin = C + 273.15 and Fahrenheit = C × 1.80 + 32.00. Return a
[]float64with both.- Currency converterbuild
Read an amount in rupiah with
fmt.Scan, convert it to dollars using a constant rate, and print it with 2 decimals.Show hint
fmt.Printf("%.2f\n", usd)formats with two decimals. - Shadowing huntbuild
Write a function where an
errdeclared with:=inside anifshadows an outererr, causing a bug. Then fix it.Show hint
Run
go vetwith the shadow analyzer, or just compare what you expect to be printed with what is.
Finished this lesson?
Mark it done to track your progress. It is saved in this browser only.