Conditional Logic
Conditional Logic
Go has if and switch. There is no ternary operator. The boring default is a guard clause at the top of a function (bail out on the bad case) and a switch when you have more than two named states.
Mental model
if can start with a short statement: if v, err := f(); err != nil. That statement’s names live in the if and else only.
switch compares one value to a list of cases (tagged), or lists boolean conditions with no value (tagless). Cases do not fall through unless you write fallthrough. There is no ?:. Write if and assign.
Worked examples
Case 1: if with an init statement
Save as open_if.go. Look up a table; only seat when the lookup worked.
// open_if.go
package main
import "fmt"
func tableFor(name string) (int, bool) {
if name == "window" {
return 4, true
}
if name == "counter" {
return 1, true
}
return 0, false
}
func main() {
if n, ok := tableFor("window"); ok {
fmt.Println("seat at", n)
} else {
fmt.Println("no table")
}
}Run:
go run open_if.goOutput:
seat at 4
The init form keeps n and ok out of the rest of main. That is the same idea as the scope chapter, now used as the normal way to write if.
Case 2: Tagged switch on a state
Save as ticket_switch.go. One value, several names. default is the leftover.
// ticket_switch.go
package main
import "fmt"
func next(state string) string {
switch state {
case "open":
return "seat the guest"
case "seated":
return "take the order"
case "ordered":
return "send the kitchen"
case "paid":
return "clear the table"
default:
return "unknown state: " + state
}
}
func main() {
fmt.Println(next("seated"))
fmt.Println(next("lost"))
}Run:
go run ticket_switch.goOutput:
take the order
unknown state: lost
Several values can share a case: case "open", "seated":. No extra break is required.
Case 3: Tagless switch
Save as shift_switch.go. Each case is a condition. First match wins.
// shift_switch.go
package main
import "fmt"
func pay(hours int, night bool) int {
switch {
case hours <= 0:
return 0
case night:
return hours * 1800
case hours > 8:
return 8*1500 + (hours-8)*2250
default:
return hours * 1500
}
}
func main() {
fmt.Println(pay(6, false))
fmt.Println(pay(10, false))
fmt.Println(pay(6, true))
}Run:
go run shift_switch.goOutput:
9000
16500
10800
This is the replacement for a chain of else if when the conditions are the story.
Case 4: Guard clauses, not a triangle of if
Save as charge.go. Bad input returns first. The happy path stays left-aligned.
// charge.go
package main
import (
"fmt"
"os"
)
func charge(item string, cents int) error {
if item == "" {
return fmt.Errorf("item is empty")
}
if cents <= 0 {
return fmt.Errorf("%s: cents must be positive", item)
}
fmt.Printf("charged %s %d cents\n", item, cents)
return nil
}
func main() {
if err := charge("toast", 350); err != nil {
fmt.Fprintln(os.Stderr, err)
}
if err := charge("tea", 0); err != nil {
fmt.Fprintln(os.Stderr, err)
}
}Run:
go run charge.goOutput:
charged toast 350 cents
tea: cents must be positive
The nested version (if item != "" { if cents > 0 { ... } }) does the same work and hides the success path. Prefer the guards.
Go has no cents > 0 ? cents : 0. Write if cents < 0 { cents = 0 }.
The trap
fallthrough goes to the next case body, even if that case’s match would have failed. Save as fallthrough.go:
// fallthrough.go
package main
import "fmt"
func main() {
state := "open"
switch state {
case "open":
fmt.Println("seat")
fallthrough
case "seated":
fmt.Println("hand menu")
case "paid":
fmt.Println("should not print for open")
}
}Run:
go run fallthrough.goOutput:
seat
hand menu
"open" is not "seated". fallthrough still ran the seated body. It did not run paid because that case has no fallthrough above it. If you want two states to share work, list them on one case or call a function. Leave fallthrough for the rare C-style dispatch you can name in a comment.
The boring rule
- Guard first. Return the error. Keep the happy path flat.
if init; condwhen the value is only for that branch.switchon a name when the list is known. Taglessswitchwhen the list is conditions.- No ternary. An
ifis two lines and reads in one pass. - Do not write
fallthroughunless the next body must always run. defaultfor the unexpected state, not for the common one.
Try this
- In
open_if.go, look up"counter"and"patio". Print both. - In
ticket_switch.go, combine"open"and"seated"on one case and return"on the floor". - In
charge.go, add a guard that rejectscents > 100_000as “too large.” - Delete
fallthroughfromfallthrough.goand confirm onlyseatprints.