Operators & Expressions
Operators are the verbs of an expression. Go has the usual arithmetic
and logical operators, plus a few quirks worth knowing, like
&^ (AND-NOT), ++ being a statement (not an
expression), and the absence of a ternary ? :.
Learning objectives
- Use arithmetic, comparison, logical, and bitwise operators correctly.
- Understand integer-division behavior and modulo with negatives.
- Know that
++and--are statements, not expressions. - Read the precedence table well enough to add parentheses preemptively.
- Recognize the
&,*, and<-operators that we'll explain later (pointers and channels).
Arithmetic operators
| Op | Meaning | Works on |
|---|---|---|
+ | add | numbers, strings (concatenation) |
- | subtract / negate | numbers |
* | multiply | numbers |
/ | divide | numbers |
% | modulo (remainder) | integers |
package main
import "fmt"
func main() {
a, b := 10, 3
fmt.Println(a+b, a-b, a*b) // 13 7 30
fmt.Println(a/b) // 3 ← integer division
fmt.Println(a%b) // 1
// Same with floats, full precision
fmt.Println(10.0 / 3.0) // 3.3333333333333335
// String concat
fmt.Println("Hello, " + "World")
}
5 / 2 is 2, not 2.5.
-7 / 2 is -3, not -4. The
modulo operator's sign follows the dividend: -7 % 2 == -1.
Convert to a float (or use math.Mod) when you need real
division.
Comparison operators
All return a bool:
| Op | Meaning |
|---|---|
== / != | equal / not equal |
< / > | less / greater |
<= / >= | less or equal / greater or equal |
Strings compare lexicographically by byte value:
fmt.Println("apple" < "banana") // true
fmt.Println("apple" == "Apple") // false (case-sensitive)
Slices and maps cannot be compared with == (only against
nil). For deep equality, use
reflect.DeepEqual or a per-type loop.
Logical & short-circuit
| Op | Meaning |
|---|---|
&& | logical AND |
|| | logical OR |
! | logical NOT |
Both && and || short-circuit:
the right operand is only evaluated if necessary. This is critical for
safe nil checks:
if u != nil && u.IsAdmin {
// u.IsAdmin only evaluated when u != nil, no nil deref panic
}
if cached, ok := lookupCache(key); ok || expensiveCheck(key) {
// expensiveCheck only runs when the cached lookup says false
}
Bitwise operators
Operate on the binary bits of integers:
| Op | Meaning |
|---|---|
& | AND |
| | OR |
^ | XOR (binary) / NOT (unary) |
&^ | AND-NOT (clear bits) |
<< | left shift |
>> | right shift |
package main
import "fmt"
func main() {
x, y := 0b0110, 0b0011 // 6 and 3 in binary
fmt.Printf("%04b\n", x & y) // 0010 AND
fmt.Printf("%04b\n", x | y) // 0111 OR
fmt.Printf("%04b\n", x ^ y) // 0101 XOR
fmt.Printf("%04b\n", x &^ y) // 0100 AND-NOT (x with bits set in y cleared)
fmt.Printf("%04b\n", x << 1) // 1100 shift left
fmt.Printf("%04b\n", x >> 1) // 0011 shift right
}
&^ is unique to Go (most languages need
x & ~y). It clears the bits of x that
are set in y. Handy when you have flag bitmasks.
Assignment & compound
= assigns. The compound forms combine an arithmetic or
bitwise op with assignment:
x := 10
x += 5 // x = x + 5 → 15
x -= 3 // 12
x *= 2 // 24
x /= 4 // 6
x %= 5 // 1
bits := uint8(0b1010)
bits |= 0b0001 // set the lowest bit
bits &= 0b0011 // mask to lowest 2 bits
bits ^= 0xFF // invert all bits
Increment & decrement
x++ and x-- exist, but they are
statements, not expressions. You can't use them inside
bigger expressions:
i := 0
i++ // ✓ statement
// y := i++ // ❌ won't compile
This rules out the C/Java pattern arr[i++], which Go
considers a readability hazard.
Address-of, dereference, and channel operators (preview)
Three operators we'll meet properly later but mention here so you recognize them in code:
&x: take the address ofx. Produces a pointer (Chapter 16).*p: dereference pointerp. Produces the underlying value (Chapter 16).<-ch: receive a value from channelch(Chapter 25).ch <- v: sendvon channelch.
Precedence (only 5 levels, easy to memorize)
Go has a deliberately small precedence table. Fewer levels means fewer surprises. From highest to lowest:
| Level | Operators |
|---|---|
| 5 (highest) | * / % << >> & &^ |
| 4 | + - | ^ |
| 3 | == != < <= > >= |
| 2 | && |
| 1 (lowest) | || |
Unary operators (+, -, !,
^, *, &, <-)
bind tightest of all.
x := 1 + 2*3 // 7 , * before +
y := a == b || c // (a == b) || c
z := !found && ok // (!found) && ok
No ternary, use if
Go has no cond ? a : b operator. Use a regular
if/else (Chapter 9):
var label string
if score >= 60 {
label = "pass"
} else {
label = "fail"
}
For one-liners, you can write a tiny helper:
func ifElse[T any](cond bool, a, b T) T {
if cond { return a }
return b
}
label := ifElse(score >= 60, "pass", "fail")
(That's a generic function, Chapter 22.)
Check your understanding
Practice exercises
FizzBuzz
The classic. Print the numbers 1 to 30, but for multiples of 3 print "Fizz", multiples of 5 print "Buzz", and multiples of both print "FizzBuzz".
Use the % operator and an if-else if chain.
Show one possible solution
package main
import "fmt"
func main() {
for i := 1; i <= 30; i++ {
switch {
case i%15 == 0:
fmt.Println("FizzBuzz")
case i%3 == 0:
fmt.Println("Fizz")
case i%5 == 0:
fmt.Println("Buzz")
default:
fmt.Println(i)
}
}
}
The "tagless switch" idiom (next chapter!) is cleaner than
if-else if for this kind of cascade.
Bit-flag toggling
Imagine a permissions byte where bit 0 is "read", bit 1 is "write",
bit 2 is "execute". Write functions set,
clear, and has that take a permission
byte and a flag byte and return the new state (or a bool).
Show one possible solution
package main
import "fmt"
const (
Read uint8 = 1 << iota // 0b001
Write // 0b010
Execute // 0b100
)
func set(p, flag uint8) uint8 { return p | flag }
func clear(p, flag uint8) uint8 { return p &^ flag }
func has(p, flag uint8) bool { return p&flag != 0 }
func main() {
var perm uint8
perm = set(perm, Read|Write)
fmt.Printf("%03b\n", perm) // 011
fmt.Println(has(perm, Read)) // true
fmt.Println(has(perm, Execute)) // false
perm = clear(perm, Write)
fmt.Printf("%03b\n", perm) // 001
}
Further reading
Operators in the bag.