Arithmetic That Follows Rules You Did Not Grow Up With
A billing job computes a prorated refund: refund := charge * daysLeft / daysTotal, all integers. The numbers come back suspicious. The cause is a rule most engineers absorb from C and then forget applies here too — integer arithmetic in Go is exact and unforgiving.
7 / 2 // 3 — division truncates toward zero, never rounds
-7 / 2 // -3 — truncation, not floor: -3.5 rounds toward zero
-7 % 2 // -1 — remainder takes the sign of the dividend
7 % -2 // 1
The remainder follows from the spec's identity x = q*y + r with q truncated toward zero, which forces r to share x's sign. If your code assumed Python's floor division and positive remainders (-7 % 2 == 1 there), every negative input shifts by one. Money, timestamps, and pagination are where that shows up.
Division by zero has two different endings. With constants the compiler catches it:
const bad = 10 / 0 // compile error: division by zero
With variables it is a runtime panic that kills the program, so validating a divisor is a real precondition, not paranoia.
Overflow is the quiet one. Constants are checked at compile time because the compiler evaluates them with arbitrary precision:
const over uint8 = 300 // compile error: 300 overflows uint8
Variables are not: they hold sized storage, and arithmetic wraps silently through two's complement.
var x uint8 = 255
x = x + 1 // 0 — no warning, no error
fmt.Println(x + 1) // 1
Counters that must never wrap need explicit bounds checks before the arithmetic, not after. And a related trap: operands of a binary operator must already have the same type — var i int; var f float64; i + f does not compile, even though both are numbers. Untyped constants slide past this (i + 1 works because the constant 1 adopts int, while i + 1.5 fails because 1.5 cannot), which is exactly the boundary Variables and Constants drew. Getting from i + f to working code means writing float64(i) + f — the conversion story is the next topic, Explicit Type Conversions.
Five Precedence Levels, and the C Bug Go Fixed
Go's entire binary operator precedence fits on one card:
| Level | Operators | Associates with |
|---|---|---|
| 5 (tightest) | * / % << >> & &^ |
multiplication-level |
| 4 | + - ` |
^` |
| 3 | == != < <= > >= |
comparison |
| 2 | && |
logical AND |
| 1 (loosest) | ` |
Unary operators bind tighter than all of these, and ++/-- are not operators at all — they are statements, which is why x++ cannot appear inside an expression and if x++ == 1 {} does not compile.
The level-5 versus level-3 split settles a bug that ships in C code to this day. In C, & binds looser than ==, so flags & Read != 0 parses as flags & (Read != 0) — comparing first, then AND-ing a boolean against an integer. Go puts & at level 5 and == at level 3, so the same line parses as (flags & Read) != 0, which is what every reader assumed. The lesson inverts the usual advice: here the unparenthesized form is correct, and your C-trained instincts are what need adjusting.
The same table explains Effective Go's example x<<8 + y<<16: shifts sit with multiplication, addition sits below them, so the expression means (x<<8) + (y<<16) — what the spacing implies. When in doubt, parentheses are always legal and gofmt preserves them; reach for them when the code must be unambiguous to someone who has not memorized the table, not to satisfy the compiler.
The rest of the expression toolkit is small on purpose:
total += price // compound assignment: += -= *= /= %= &= |= ^= <<= >>= &^=
n := -score // unary negate
ok := !failed // logical not
There is no ternary ?: — max := a; if b > a { max = b } is the idiom, and you will meet it properly in Conditionals with if and else. There is no comma operator, no implicit conversions in mixed expressions, and no ** exponent (use math.Pow). Every omission pushes the same design: one obvious way to write each thing, readable by someone who learned the language last week.
==, <, and the Types That Refuse Both
Comparison splits into two capabilities, and the type system decides which one your value gets.
Equality (==, !=) is broadly available: booleans, all numeric types, strings, pointers, channels, and interfaces support it, along with arrays and structs whose elements all do. Ordering (<, <=, >, >=) is restricted to numbers and strings — you cannot ask whether one struct is less than another, and there is no automatic lexicographic fallback for structs.
"apple" < "banana" // true — byte-wise lexicographic
"Z" < "a" // true — uppercase sorts before lowercase!
[2]int{1, 2} == [2]int{1, 2} // true — element-wise
String comparison is raw byte order, which means it is not natural-language collation: uppercase letters all sort before lowercase, and accented characters follow UTF-8 byte values rather than dictionary order. For user-facing sorting you need explicit locale-aware logic; for keys and IDs, byte order is exactly what you want.
The refusals are worth memorizing because they are compile errors, not surprises:
m1 := map[string]int{"a": 1}
m2 := map[string]int{"a": 1}
m1 == m2 // compile error: maps are not comparable
s1 := []int{1}
s2 := []int{1}
s1 == s2 // compile error: slices are not comparable
Maps, slices, and functions can only be compared against nil — a test of presence, not of content. Structs are comparable only if every field is, so a struct containing a slice cannot use == at all; comparing content there means writing an Equal method or using reflect. Elements matter too: struct { ids []string } inherits the slice's incomparability.
Two runtime behaviors complete the picture. Interface values compare by dynamic type and value — fmt.Stringer(x) == fmt.Stringer(x) is true only if both hold the same underlying value — and comparing interfaces that hold values of the same uncomparable type (two slices, for instance) panics rather than compiling to false. And IEEE floats carry a trap: every comparison against NaN is false, including NaN == NaN, so range checks (x > low && x < high) are the reliable idiom instead of equality on floats.
Equality is the silent half of control flow: every if, switch, and loop condition you write next is built from these operators, so knowing which comparisons are legal — and which are lies — decides how readable that code can be.
Short-Circuit Guards and Bit-Level Flags
The logical operators &&, ||, and ! are ordinary, but Go's && and || short-circuit: the right operand is evaluated only when the left does not decide the result. That is not a micro-optimization — it is a safety mechanism.
if s != nil && s.Len > 0 {
// s.Len runs only when s is non-nil
}
if user == nil || !user.Active {
return ErrInactive
}
Without short-circuiting, s != nil && s.Len > 0 would panic on nil dereference for every nil s — the guard would be decorative. The pattern is load-bearing in nearly every condition you will write, including the err != nil checks from error handling: write the potentially unsafe access on the right, the check that makes it safe on the left, and never reorder them casually. || mirrors it: the right side runs only when the left is false, so user == nil || user.Name == "" is a complete emptiness check with one side-effect-free first test.
Bitwise operators live at a different altitude: they operate on the bits of integers, which makes them the encoding layer under everything from file permissions to network flags.
const (
Read = 1 << iota // 001
Write // 010
Exec // 100
)
var perm = Read | Write // combine: 011
hasRead := perm&Read != 0 // test one bit
perm = perm &^ Exec // clear: Go's AND NOT — keep bits where Exec is 0
perm = perm | Exec // set
The operator table for integers: & keeps bits set in both, | keeps bits set in either, ^ toggles, <</>> shift (with a non-negative count — a negative shift count panics at runtime), and &^ — uniquely Go's spelling — clears the right operand's bits from the left. x &^ (1 << n) reads as "turn off bit n," which is the operation everyone else writes as x & ~(1 << n) and half of them get wrong at the boundaries. The flag-building half of this pattern, 1 << iota, is developed in Variables and Constants.
Watch the contrast that confuses newcomers: && requires booleans and short-circuits, while & requires integers and always evaluates both sides. Using & where you meant && — with non-boolean operands it will not even compile — is caught early; using && on bits is a type error in the other direction. Between the two families, every condition and every flag in the codebase has exactly one idiomatic spelling.
Quiz
1. A refund routine computes `days := -7; perDay := 2; result := days % perDay`. What does it produce, and which rule explains it?
2. How does Go parse `perm & mask != 0`, and how does that differ from C?
3. `if s != nil && s.Len > 0` is evaluated for s == nil. What happens, and why?
4. In your program, `var c uint8 = 255; c = c + 1`. What is the value of c afterward, and how does it differ from `const d uint8 = 255; d + 1`?
Flashcards
Question
How do integer division and % treat negatives in Go?
Click to reveal answer
Answer
Division truncates toward zero (-7/2 == -3, not -4), and the remainder satisfies x = (x/y)*y + r with |r| < |y|, giving the remainder the dividend's sign: -7%2 == -1 while 7%-2 == 1. This differs from Python's floor semantics, so ported code is wrong for negative inputs.
Question
How does Go's precedence for & compare to C's, and why does it matter?
Click to reveal answer
Answer
In Go, & is level 5 (with * and /) while == and != are level 3, so `flags & Read != 0` parses as `(flags & Read) != 0`. C puts & below ==, so the identical line parses as `flags & (Read != 0)` and produces silent nonsense — the ordering that makes C's version a bug is the one Go deliberately reversed.
Question
Why must the nil check come before the field access in `if s != nil && s.Len > 0`?
Click to reveal answer
Answer
Because && short-circuits: the right operand is only evaluated when the left does not already decide the result, so a true left side skips s.Len entirely and a false one never reaches it. Reorder the operands and the dereference runs before the guard, turning a safe check into a panic.
Question
Which values can never be compared with ==, and what are the alternatives?
Click to reveal answer
Answer
Slices, maps, and functions — compile errors; they compare only against nil (presence, not content). A struct containing any of those is also incomparable, so content comparison needs an Equal method or reflect.DeepEqual. At runtime, interfaces holding the same uncomparable dynamic type panic when compared, and NaN fails every equality test including NaN == NaN.
Question
What does &^ do, and how do you clear bit n of a flags variable?
Click to reveal answer
Answer
&^ is AND NOT: it keeps bits of the left operand wherever the right operand has 0, clearing exactly the right operand's set bits. So `flags = flags &^ (1 << n)` turns off bit n — Go's spelling of what C writes as flags & ~(1 << n), with the complement written once instead of twice.
Revision Notes
Key Takeaways
- 1. Integer division truncates toward zero and % takes the dividend's sign — negative inputs break code written with Python/C++ modulo assumptions
- 2. Overflow is a compile error for constants and silent wrap for variables; bounds must be checked explicitly when values are data-dependent
- 3. Go's precedence puts & above ==, so the natural reading of `perm & mask != 0` is the correct one — unlike C
- 4. Equality and ordering are separate capabilities: slices/maps compare only to nil, structs with slices not at all, and NaN equals nothing
- 5. && short-circuits, which makes `x != nil && x.field` a real guard; bitwise operators (especially &^) are the encoding layer under flag sets
Interview Tips
- • "What does `-7 % 2` return?" — -1, and be ready with the derivation: division truncates toward zero, so r = x - (x/y)*y inherits the dividend's sign; contrasting Python's 1 shows you understand semantics, not just syntax
- • "Why is `flags & Read != 0` correct in Go but a bug in C?" — walk the precedence table: Go has & at level 5 and == at level 3; C reversed them; give the C parse `flags & (Read != 0)` as the concrete failure
- • "How do you safely check a field through a pointer?" — `if p != nil && p.Field > 0` and explain short-circuit as the reason the dereference cannot run first
- • "How would you add a permission?" — `perm |= Flag`, test `perm&Flag != 0`, clear `perm &^= Flag`; naming &^ specifically signals real Go experience
Cheat Sheet
Operators and Expressions
Precedence (tightest to loosest):
| Level | Operators |
|---|---|
| 5 | * / % << >> & &^ |
| 4 | `+ - |
| 3 | == != < <= > >= |
| 2 | && |
| 1 | ` |
Arithmetic rules:
| Expression | Result | Rule |
|---|---|---|
7 / 2 |
3 | truncates toward zero |
-7 % 2 |
-1 | remainder follows dividend's sign |
10 / 0 (const) |
compile error | constant division checked |
10 / x (var, x==0) |
runtime panic | validate the divisor |
var u uint8 = 255; u+1 |
0 | variables wrap silently |
const u uint8 = 300 |
compile error | constants must fit |
i + f (int, float64) |
compile error | operands must share a type — convert |
Comparability: numbers/bools/strings/pointers/chans/interfaces/arrays/structs(with comparable fields) → ==; ordered (<) → numbers and strings only; slices/maps/funcs → nil-only; structs containing slices → no == at all.
Code template — flags:
const ( Read = 1 << iota; Write; Exec )
perm := Read | Write // set
ok := perm&Read != 0 // test
perm = perm &^ Exec // clear
perm = perm | Exec // set again
Guard idiom: safe access on the right of &&, check on the left: if s != nil && s.Len > 0.