Go Guide

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
  1. 01Four ways to declare a variable
  2. 02Zero values
  3. 03Naming
  4. 04Scope: where a name is visible
  5. 05Constants
  6. 06Grouping declarations
  7. 07More examples
  8. 08The full program
  9. 09Summary
  10. 10Check your understanding
  11. 11Practice in Go

Four ways to declare a variable#

Go
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 only

All 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 T when you want the zero value to start with, such as a counter or an empty builder.
  • Use var at 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:

TypeZero value
numbers0
string""
boolfalse
pointers, slices, maps, channels, functions, interfacesnil
structsevery 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.

Go
// 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:

LineCodeOuter xInner x
1x := 11
2if true {1
3x := 212 (new variable)
4x++13
5}1gone
6fmt.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.

Go
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:

Go
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:

Go
var (
	host = "localhost"
	port = 8080
)
 
x, y := 1, 2 // several at once
x, y = y, x  // swap, with no temporary variable

More 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).

convert.go
Go
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)
}
Output
  98.6°F =   37.0°C
 212.0°F =  100.0°C
 -40.0°F =  -40.0°C
10 ft = 3.048 m

Shadowing: 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.

shadow.go
Go
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)
}
Output
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.

constants.go
Go
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)
}
Output
1024 1048576 1073741824
4
2s Mon Tue
1.5 1

Weekday 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#

variables.go
Go
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)
}
Output
kurs #1: Rp2000000 = $125.00
inner x: 3
outer x: 1
second first 2

To 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; var works 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; iota numbers enum-like constants.

Check your understanding

Answer before scrolling back up. If you miss one, reread the matching section, then try again.

  1. Q1.Where can you use x := 5?

  2. Q2.What is printed? x := 1; if true { x := 2; _ = x }; fmt.Println(x)

  3. Q3.What does var count int hold before you assign anything?

  4. 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 []float64 with 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 err declared with := inside an if shadows an outer err, causing a bug. Then fix it.

    Show hint

    Run go vet with 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.