Variables, types, and the compiler that argues with you
This is the chapter where Go feels most foreign, and it is worth knowing why in advance.
This is the chapter where Go feels most foreign, and it is worth knowing why in advance. In JavaScript, a variable is a box that holds anything and can hold something else tomorrow. In Go, a variable has one type forever, decided when it is created, and the compiler refuses to build your program if you break that promise.
That refusal is the feature. Work with it for a week and you will notice a whole category of bug has stopped happening to you.
Declaring things
Go has two ways to make a variable, and you will use one of them ninety percent of the time.
var name string = "Ada" // the long form, type statedvar age = 36 // type inferred, still a var// short form: declare and infer, inside functionsname := "Ada"
The := operator is the one you want. It declares a new variable and infers its type from the value on the right. It only works inside a function, which is why package level variables use var.
If you know JavaScript
:= is closest to let in that it creates a new binding. There is no equivalent of const for variables created with :=. Go's const exists, but it is only for values known at compile time: numbers, strings, booleans. You cannot declare a constant struct or slice. If you want "do not reassign this", Go's answer is to write a small enough function that it is obvious.
An unused variable is a compile error, not a warning.
func main() {count := 10fmt.Println("hello")}// ./main.go:4:5: declared and not used: count
This feels hostile on day one and reasonable by day ten. Dead variables are usually the residue of a half-finished edit, and Go would rather stop you than let the residue accumulate. The same rule applies to unused imports.
Watch out
The escape hatch is _, the blank identifier, which means "I know there is a value here and I am deliberately ignoring it". You will use it constantly in loops and multiple returns. Do not reach for it to silence a real warning:_ = count compiles and tells the next reader nothing.
The types you will actually use
Go has a lot of numeric types. You will use about four of them.
- Type
- JavaScript
- Notes
stringstring- Immutable, UTF-8 bytes. Indexing gives a byte, not a character
intnumber- Whole numbers. 64 bit on any machine you own
float64number- Decimals. The default for a fractional literal
boolbooleantrueorfalse. No truthiness, everbyte- -
- Alias for
uint8. A byte of data rune- -
- Alias for
int32. One Unicode code point
The one to notice is that JavaScript's single number splits into int andfloat64, and Go will not convert between them for you.
count := 10 // intprice := 2.5 // float64total := count * price// invalid operation: count * price (mismatched types int and float64)
You must say what you mean:
total := float64(count) * price // 25
If you know JavaScript
JavaScript coerces almost anything: "3" * 2 is 6, [] + {} is a string, and 0.1 + 0.2 is famously not 0.3. Go does none of this. There is no implicit conversion between any two types, not even int and int64. Every conversion is written out, which is more typing and removes an entire family of surprise.
Zero values: there is no undefined
Every type in Go has a zero value, and a declared variable always has it. There is no undefined, and no uninitialised memory.
string""int,float640boolfalse- pointers, slices, maps, interfaces, functions
nil
var name stringvar count intvar ready boolfmt.Println(name == "", count == 0, ready == false) // true true true
This is genuinely useful. A struct you create is immediately valid, with every field at a sensible default, and you never check whether a field "exists".
Watch out
It is also the one place zero values hurt: you cannot tell "the caller set this to zero" apart from "the caller did not set it". If that distinction matters, for example a form field where empty and absent mean different things, you need a pointer (*int, where nil means absent) or a dedicated type. Chapter eight covers pointers properly. This problem is real, and it is the usual reason you see *string in a struct.
No truthiness
An if in Go takes a bool. Not a value that is bool-ish. A bool.
More characters, and no memorising which values are falsy. There is no == and=== distinction either, because there is only one == and it does not coerce.
Strings are bytes, and that matters
A Go string is a read-only slice of bytes holding UTF-8. Indexing it gives you a byte.
s := "héllo"fmt.Println(len(s)) // 6, not 5: é takes two bytesfmt.Println(s[1]) // 195, a byte value, not "é"
To work with characters, range over the string, which decodes runes:
for i, r := range "héllo" {fmt.Printf("%d: %c\n", i, r)}// 0: h// 1: é <- index jumps to 3 next, because é was two bytes// 3: l// 4: l// 5: o
If you know JavaScript
JavaScript strings are UTF-16 and have the same class of problem: "👍".lengthis 2. Both languages make you think about encoding the moment you leave ASCII. Go is at least explicit that you are holding bytes.
Strings are immutable, so building one in a loop with += allocates a new string every time. For anything beyond a few concatenations use a builder:
var b strings.Builderfor _, word := range words {b.WriteString(word)b.WriteString(" ")}result := b.String()
Control flow, briefly
if and for are almost familiar, with two differences worth stating.
There are no parentheses around the condition, and braces are mandatory even for one line.
if count > 10 {fmt.Println("plenty")} else if count > 0 {fmt.Println("some")} else {fmt.Println("none")}
for is the only loop keyword. It covers every case JavaScript spreads acrossfor, while and for...of.
for i := 0; i < 10; i++ { } // classicfor count < 10 { } // whilefor { } // infinitefor i, v := range items { } // for...of, with the index
An if can carry a statement before the condition, scoped to the if. You will see this constantly in real code:
if err := doSomething(); err != nil {return err}// err does not exist here, which is the point
If you know JavaScript
switch in Go needs no break: cases do not fall through. You can switch on no value at all, which makes it a tidy replacement for a long else ifchain:
switch {case score > 90:grade = "A"case score > 80:grade = "B"default:grade = "C"}
Project 2: a temperature table
Write a program that prints a table converting Celsius to Fahrenheit from minus 40 to 100 in steps of 20. The formula is f = c*9/5 + 32.
package mainimport "fmt"func main() {fmt.Printf("%8s %8s\n", "C", "F")for c := -40; c <= 100; c += 20 {f := float64(c)*9/5 + 32fmt.Printf("%8d %8.1f\n", c, f)}}
Two things to notice. float64(c) is required: without it you get integer division and wrong answers for anything not divisible by five, which is the bug this chapter exists to prevent. And %8.1f means "right align in eight columns, one decimal place", which is how Go does string formatting.
Now break it on purpose: remove the float64(...) conversion and read the error. Then change it to c*9/5 with c still an int and see that it compiles but produces wrong numbers. The compiler catches the type error. It cannot catch the maths error, and knowing which is which is the skill.
Your turn
Add a third column showing Kelvin (k = c + 273.15), and right align the header over it. Then make the step size a variable at the top of main and confirm the table still lines up when you change it to 10.
Assignment
Commit your work with the message Variables - Assignment One, exactly as written, so the leaderboard can score it.
Try it
Make the zero values concrete, because undefined is the habit that takes longest to drop.
In part1-language/03-types, write a program that declares a variable of every type in this chapter's table, with no initial value, and prints each one with its type:
fmt.Printf("%-10T %q\n", s, s)
One. Print the zero value of a string, an int, a float64, a bool, a slice, a map, a pointer and a struct. Note in a comment which of them you can use immediately and which panic if you write to them.
Two. Write three lines that each fail to compile, and keep them commented out with the exact error: an int assigned to a float64, a string compared to a number, and an if on a non-boolean.
Three. In NOTES.md, answer in two sentences: which of these would JavaScript have accepted, and what would it have done instead?
Assignment
Commit your work with the message Variables - Assignment Two, exactly as written, so the leaderboard can score it.
Try it
Strings are bytes, and the day that matters is the day somebody types their own name into your form.
In part1-language/03-types/strings:
One. Take a string with characters outside ASCII in it, your own name or a word with an accent on it, and print len(s) next toutf8.RuneCountInString(s). Explain the difference in a comment.
Two. Index it with s[0] and print what comes back. It is a number, and the type is the explanation.
Three. Loop over it twice, once with an index loop over bytes and once withrange, printing each item. Only one of them produces characters.
Four. Reverse the string, correctly. The byte-wise reversal that works for"hello" will produce nonsense for your test string, and both versions belong in the commit.
Five. In NOTES.md, write the one sentence you would put in a code review when somebody slices a string to truncate it for display.
Spot a typo? Have an idea?
Help us improve this lesson. One click opens a GitHub issue with the lesson URL pre-filled, suggest clearer wording, report a bug, or request more depth. The course keeps improving thanks to learners like you.
Suggest an improvement on GitHub