17. Boolean logic in depth

Chapter 16 used &&, ||, and ! to join conditions. This chapter looks closer at what they do, and at Go's strictest rule about conditions.

Only a bool goes in an if

A condition must be a bool: true or false, or something that gives one. Nothing else is allowed. A string, a number, or an empty value does not count as true or false in Go.

Open exercises/17/01-not-a-bool.go. It has two if statements, one with a string and one with the number 1. It does not compile:

# command-line-arguments
exercises\17\01-not-a-bool.go:13:5: non-boolean condition in if statement
exercises\17\01-not-a-bool.go:17:5: non-boolean condition in if statement

Non-boolean means "not a bool". Column 5 is where the condition starts. Replace the conditions with real comparisons, using the next example as a guide.

So write the question out. Asking whether a string has anything in it, or whether a number is not zero, takes a comparison:

name := "Keiko"
count := 3

if name != "" {
    fmt.Println("Hello, " + name)
}
if count != 0 {
    fmt.Println("The count is not zero.")
}

Output:

Hello, Keiko
The count is not zero.

This takes a few more characters, and in return the condition says exactly what it tests. name != "" cannot be misread. The compiler also catches the mistake for you, before the program runs:

level := 1
if level {
    fmt.Println("Has a level.")
}
non-boolean condition in if statement

Comparisons, again

Each comparison from Chapter 16 gives a bool. Strings can be compared too. < and > compare them in dictionary order, with one catch: capital letters come before lower case ones.

fmt.Println("apple" == "apple")  // true
fmt.Println("apple" != "banana") // true
fmt.Println("apple" < "banana")  // true
fmt.Println("Zebra" < "apple")   // true

Both sides of a comparison must have the same type. Go does not turn a number into text for you, or text into a number:

score := 73
if score == "73" {
    fmt.Println("Seventy-three.")
}
invalid operation: score == "73" (mismatched types int and untyped string)

Convert first with strconv from Chapter 14, or compare with the number 73.

&& and || give back a bool

&& and || take two bools and give one bool. ! takes one bool and gives one bool. In full:

a b a && b a || b !a
true true true true false
true false false true false
false true false true true
false false false false true

The output of exercises/17/02-bools.go tries a few of these:

false
false
true
true
false
true
Can enter: true

Because the answer is a bool, you can store it in a variable with a good name, then use the variable as the condition. A bool variable can stand in an if alone, with no comparison:

level := 12
hasKey := true
canEnter := level >= 10 && hasKey

if canEnter {
    fmt.Println("You can enter the dungeon.")
}
fmt.Println("Can enter:", canEnter)

Output:

You can enter the dungeon.
Can enter: true

When && and || are mixed, && goes first, the way * goes before +. Use parentheses when you want a different order, or when they make the line clearer:

fmt.Println(true || false && false)   // true
fmt.Println((true || false) && false) // false

A default value

A common job: use a value if there is one, and otherwise use a fallback. || cannot do this, because it only works on bools:

chosen := ""
move := chosen || "rock"
fmt.Println(move)
invalid operation: operator || not defined on chosen (variable of type string)

Chapter 11 showed that an empty variable is not really empty. It holds the zero value of its type, and for a string that is "". So "nothing was entered" means the string equals "", and an if supplies the fallback:

typedName := "" // nothing was entered

name := typedName
if name == "" {
    name = "stranger"
}
fmt.Println("Hello, " + name) // Hello, stranger

If typedName had held a real value, name would keep it instead. Start with the value, then replace it only when it is missing. This is three lines long, and you will see it often.

This works when "" can never be a real answer. A number is harder, since a 0 may be a real value or may be what strconv.Atoi gave for bad text. Chapter 21 shows how to tell those apart.

Open exercises/17/03-default.go. chosen is set to "", so move falls back to "rock". Set chosen to a real word and run it again.

Short-circuit: the second side is skipped

&& and || stop as soon as the answer is certain:

  • a && b: if a is false, the answer is false, and b is never worked out.
  • a || b: if a is true, the answer is true, and b is never worked out.

This is called short-circuit evaluation. It lets the first test guard the second, which runs only after the first has passed:

total := 90
count := 0

// The average is only worth working out when count is not zero,
// so the count check guards the division.
if count > 0 && total/count > 20 {
    fmt.Println("high average")
} else {
    fmt.Println("no average to show")
}

Output:

no average to show

Because count > 0 is false, Go never evaluates total/count. It goes straight to the else. The guard is needed, because dividing a whole number by zero stops the program. Swap the two sides:

total := 90
count := 0

if total/count > 20 && count > 0 {
    fmt.Println("high average")
}

Now the division runs first, and the program crashes with:

panic: runtime error: integer divide by zero

This is the same kind of crash as the slice error in Chapter 12: the program compiled, then stopped while running.

|| guards in the same way. Here the division is reached only when count is not zero:

total := 90
count := 0

if count == 0 || total/count < 20 {
    fmt.Println("nothing special")
}

Output:

nothing special

Open exercises/17/04-short-circuit.go. Run it, then swap the two sides of && and run it again to see the crash. Finally, put them back and set count to 3.

! flips a bool

! turns true into false and false into true. It is handy for asking "is this missing?" when you have a bool:

fmt.Println(!true)  // false
fmt.Println(!false) // true

hasKey := false
if !hasKey {
    fmt.Println("You need a key.")
}

Output:

false
true
You need a key.

As with ||, it only works on bools. To ask the same question about a string, compare it with "":

key := ""
if !key {
    fmt.Println("You need a key.")
}
invalid operation: operator ! not defined on key (variable of type string)
key := ""
if key == "" {
    fmt.Println("You need a key.")
}

Output:

You need a key.

Homework

Homework starters are in exercises/17/homework/. Each one runs as it is and prints a placeholder line.

Problem 1 — Default colour

Open exercises/17/homework/01-default-colour.go. fav holds a colour or "". Set colour to fav, or to "blue" when fav is empty, then print Your colour is <colour>. Run it once with fav := "" and once with a real colour.

Problem 2 — Say what you mean

Open exercises/17/homework/02-explicit-tests.go. Five variables hold 0, "", false, "hi", and 1. For each one, print the value and the result of the test that asks whether it differs from its zero value, such as count != 0. Use %q for the strings so the empty one is visible. Which of the five needs no comparison?

Problem 3 — Guarded division

Open exercises/17/homework/03-guarded.go. Two numbers: total and count (which might be 0). Using &&, when count is greater than 0 and total/count is over 20, print the average with two decimals, as a float64; otherwise print no high average. Test it with count set to 0, to 3, and to 10.

Challenge — First value that exists

Open exercises/17/homework/04-first-value.go. Three variables a, b, c each hold a string or "". Using a switch with no value, print the first one that is not empty, or none if all three are empty. Try different combinations.

Stuck or finished? Open the homework solutions page.