Course outline
Course / The terminal and the language

Variables and Types

Reading

The idea

Think of your program's memory as a shelf of cardboard boxes. Each box has a label on the front and holds one value. A box also has a fixed shape: a number box holds numbers, a text box holds text, and you cannot squeeze one into the other without converting it first. That shape is the type. Go makes you decide the shape up front, which is why it can catch mistakes before your program ever runs.

The snippets in this lesson go inside func main() from the last lesson, between its curly braces. The labs work a little differently. Each lab gives you a small function wrapper and its return line, and you fill in the lines between. Functions get a full lesson soon, so for now treat the wrapper as a frame around your boxes.

var and :=

To make a box, you say its label, its type, and what goes in it. There are two ways to write that.

var planet string = "Mars"
moons := 2
fmt.Println(planet, moons)
Mars 2

var planet string = "Mars" spells everything out: the word var, the label, the type, and the starting value. moons := 2 is the short form. The := means "make a new box and fill it", and Go works out the type from the value. Here 2 is a whole number, so moons becomes an int. fmt.Println accepts several values separated by commas and prints them with a space between.

To change a box that already exists, use a plain =:

moons = 3

Use var when you want to be explicit about the type, and := for everyday work inside functions. := only works inside functions. Outside one, at the top of a file, you must use var.

Two compiler errors to recognize:

moons := 2
moons := 5
./main.go:7:8: no new variables on left side of :=

:= always creates a box, so using it twice for the same label fails. The second line should be moons = 5.

func main() {
	count := 10
}
./main.go:6:2: declared and not used: count

Go refuses boxes that are never used, just like unused imports. The exact wording varies slightly between Go versions, but the file, line and column always come first.

Zero values

A box that you create but never fill is not empty or broken. Go fills it with a zero value, a safe default for its type.

var n int
var price float64
var done bool
var name string
fmt.Println(n, price, done)
fmt.Println("[", name, "]")
0 0 false
[  ]

Numbers start at 0, a bool starts at false, and a string starts as the empty string "". The second line shows the empty string: nothing sits between the two spaces that Println adds. Other languages leave unfilled boxes holding leftover garbage. Go never does, so a fresh variable is always safe to read.

int, uint, float64, bool, and string

These five types cover most of what you will store.

var floor int = -2        // whole numbers, positive or negative
var seats uint = 40       // whole numbers, zero and up only
var rate float64 = 4.75   // numbers with a decimal point
var open bool = false     // exactly true or false
var room string = "B-12"  // text, always in double quotes
fmt.Println(floor, seats, rate, open, room)
-2 40 4.75 false B-12

int is the default for whole numbers. uint is "unsigned", meaning it has no minus sign, so it cannot hold a negative number. float64 holds decimals and is the default for numbers with a decimal point. A price is a good example of why types matter: 4.75 in an int box would lose its .75. Strings use double quotes. Single quotes mean something different in Go, so stick to double quotes for text.

Integer sizes and overflow

So far every number box has been a standard size. You can also pick the box size. The number in the name is how many bits (tiny on-or-off switches) the box has, and more bits means a bigger range.

TypeSmallestLargest
int8-128127
uint80255
int32-2,147,483,6482,147,483,647
int64about -9.2 quintillionabout 9.2 quintillion

A plain int is 64 bits on virtually every computer you will use. The math package holds the limits as named values. Bring it in with import "math", next to import "fmt":

fmt.Println(math.MaxInt8, math.MaxInt64)
127 9223372036854775807

What happens when a number does not fit? In a variable, it overflows: it wraps around to the bottom of the range, like going one past the last numbered slot in the box and landing on the first. A uint8 runs from 0 to 255, so one past 255 lands back on 0.

var level uint8 = 255
level = level + 1
fmt.Println(level)
0

No error, no warning. The program runs and quietly gives a wrong answer, which is why picking a big enough box matters. If you write a number that clearly cannot fit, the compiler does catch it:

var small int8 = 200
./main.go:6:18: cannot use 200 (untyped int constant) as int8 value in variable declaration (overflows)

Read it piece by piece: 200 is the value, int8 is the box, and (overflows) tells you it will not fit.

const and iota

Some values should never change: a tax rate, a retry limit, the value of pi. Declare them with const and the compiler refuses to let anything change them.

const maxUsers = 100
const taxRate float64 = 0.08
fmt.Println(maxUsers, taxRate)
100 0.08

Think of a constant as a box with a lid that is glued shut. maxUsers is untyped, so Go lets it adapt to wherever it is used. taxRate is typed as a float64. Constants are also a way to give names to numbers so that your code explains itself.

Often you need a group of related names, such as the colors of a traffic light. iota is a counter that numbers them for you, starting at zero:

const (
	Red = iota
	Yellow
	Green
	Flashing
)
fmt.Println(Red, Yellow, Green, Flashing)
0 1 2 3

Inside a const ( ... ) block, iota is 0 on the first line, 1 on the second, and so on. Lines that have no value repeat the previous line's rule, so Yellow is iota again, now equal to 1. Add a new name later and it keeps counting without you renumbering anything.

Arithmetic operators

Every number box supports the same five calculator buttons: +, -, *, /, and %. +, -, and * add, subtract, and multiply exactly the way you'd expect. / divides — but between two int boxes it does integer division: it keeps only the whole-number part and throws away anything left over. %, read "modulo," gives you back exactly that leftover part, the remainder.

a := 20
b := 6

fmt.Println(a + b) // 26
fmt.Println(a - b) // 14
fmt.Println(a * b) // 120
fmt.Println(a / b) // 3  -- int division drops the remainder
fmt.Println(a % b) // 2  -- just the remainder

// starts a comment: Go ignores the rest of the line, so the notes above change nothing. Dividing a whole number by zero is the one calculation Go cannot do, and it stops your program, which the functions lesson shows in action.

When one line mixes operators, Go works out *, / and % before + and -, just like school maths, and works left to right when the operators are equal. Parentheses always go first:

fmt.Println(2 + 3*4)
fmt.Println((2 + 3) * 4)
fmt.Println(10 - 4 - 3)
14
20
3

3*4 happens before the +, so the first line is 2 + 12. The parentheses force 2 + 3 first. The last line goes left to right: 10 - 4 is 6, then 6 - 3 is 3. When in doubt, add parentheses so the next reader does not have to remember the order.

Integer division also behaves in a particular way with negative numbers: it always cuts the fractional part off toward zero, and % takes the sign of the left-hand number.

fmt.Println(-7/2, -7%3)
-3 -1

-7 / 2 is -3 (not -4), and -7 % 3 is -1. This matters whenever data can go negative, such as a balance or a time difference.

- has a second job. Written between two values (a - b) it's binary subtraction. Written in front of just one value, with nothing to its left, it's a different operator — unary minus — and it flips that value's sign instead of subtracting anything from it:

balance := 40
fmt.Println(-balance)    // -40
fmt.Println(-(-balance)) // 40

Go reuses the same - symbol for both jobs; which one you get depends on whether there's a value sitting to its left. -balance behaves exactly like 0 - balance — both flip the sign — but -balance is the form you'll actually see written, and the one to reach for whenever you need to flip a sign without a second value around to subtract from.

For anything beyond the five buttons, the math package has ready-made functions that work on float64 values, and Go has two built-in helpers, min and max, that pick the smaller or larger of their arguments:

fmt.Println(math.Abs(-3.5), math.Sqrt(16), math.Pow(2, 10))
fmt.Println(min(3, 7), max(3, 7))
3.5 4 1024
3 7

math.Abs drops the sign, math.Sqrt is the square root, and math.Pow(2, 10) is 2 to the power of 10.

Updating a box in place

Programs constantly change a box based on what it already holds, such as adding to a total. You can write it out in full, score = score + 5, or use the shorthand operators that do the same job:

score := 10
score += 5
fmt.Println(score)
score -= 3
fmt.Println(score)
score *= 2
fmt.Println(score)
score /= 4
fmt.Println(score)
15
12
24
6

score += 5 means score = score + 5, and -=, *= and /= work the same way. The very common "add one" and "take one away" have even shorter forms, ++ and --:

score++
fmt.Println(score)
score--
score--
fmt.Println(score)
7
5

score++ means score = score + 1. These are statements on their own line, not values, so x := score++ is an error. Watch the order of the symbols too: score += 5 adds, but score =+ 5 is read as score = +5, which silently sets the box to 5.

Type conversion

Go will not mix box shapes behind your back. Adding an int and a float64 is a compile error:

sum := 9
weight := 2.5
fmt.Println(sum * weight)
./main.go:8:14: invalid operation: sum * weight (mismatched types int and float64)

The fix is to convert one box into the other shape with TypeName(value):

sum := 9
items := 4
average := float64(sum) / float64(items)
fmt.Println(average)
2.25

Without the conversion, dividing two ints gives 2, because integer division throws away the remainder. Converting first keeps the .25. Conversions can lose information too:

cost := 8.95
fmt.Println(int(cost))
big := int64(1000)
fmt.Println(int8(big))
8
-24

Converting a float to an int cuts off the decimals without rounding, so 8.95 becomes 8. Shrinking 1000 into an int8 overflows and wraps to -24. To round instead of cutting off, use the math package: math.Round goes to the nearest whole number, math.Floor always goes down and math.Ceil always goes up. They all return a float64, so convert the result if you need an int.

fmt.Println(math.Round(8.95), math.Floor(8.95), math.Ceil(8.05))
fmt.Println(int(math.Round(8.95)))
9 8 9
9

Converting a number into text is different, because it is not a simple shape change. It needs the strconv package. Add import "strconv" and use strconv.Itoa, which stands for "integer to ASCII" (text):

fmt.Println(strconv.Itoa(2026) + "!")
2026!

The + joins two strings together. Avoid string(65): it does not give "65". It gives the character with code 65, which is A.

Common mistakes

  • Using := outside a function. At the top of a file, use var.
  • Writing x := 5 twice for the same label. The second one must be x = 6, with a plain =.
  • Leaving a variable unused. Go refuses to compile.
  • Dividing two int values and expecting decimals. 9 / 4 is 2. Convert to float64 first.
  • Expecting -7 / 2 to be -4. Go cuts toward zero, so it is -3.
  • Forgetting the order of operations: 2 + 3*4 is 14, not 20. Add parentheses when unsure.
  • Writing score =+ 5 when you mean score += 5. The first one sets the box to 5 without any error.
  • Assuming int(8.95) rounds to 9. It cuts off to 8.
  • Forgetting that overflow is silent. A value that is too big for its variable wraps around without any error.
  • Using string(65) when you want the text "65". Use strconv.Itoa(65).

Try it

The labs give you a function wrapper with its return line. You fill in the lines between, and the tests check what comes back. Some labs read variables declared outside the functions, which the tests set to different values to see whether your code really uses them. Try these in your own main.go first:

  1. Declare var planet string = "Mars" and moons := 2, then print both with one fmt.Println.
  2. Declare var n int, var done bool, and var name string without values, and print them. You should see 0 false and an empty string.
  3. Set var level uint8 = 255, then level = level + 1, and print level. It prints 0. Then try the same with var x int8 = 127 and see where it lands.
  4. Write a const ( Red = iota; Yellow; Green ) block as separate lines and print Green. You should see 2.
  5. Try sum * weight with an int and a float64, and read the error. Then fix it with float64(sum).
  6. Print 20 / 6 and 20 % 6, then 2 + 3*4 and (2 + 3) * 4, then declare balance := 40 and print -balance.
  7. Start with score := 10, apply score += 5, score -= 3, score *= 2 and score++, and print score after each step. You should end on 25.
  8. Print -7 / 2 and -7 % 3, then math.Round(2.5), math.Floor(2.5) and min(3, 7).

Something wrong with this lesson? Report a problem