Go · lesson 5 of 25
Operators in Go
Go operators run for real: integer division and modulo signs, ++ as a statement, short-circuit logic, bit flags with &^, and why there is no ternary.
By SkillByExample · Updated
Go’s operators look like C’s, with a few deliberate differences: integer division truncates, ++ is a statement, there is a bit-clear operator &^, and there is no ternary. Each program below runs as-is.
Arithmetic
Both operands must have the same type, and integer division drops the fractional part.
package main
import "fmt"
func main() {
a, b := 17, 5
fmt.Println(a+b, a-b, a*b, a/b, a%b)
fmt.Println(17.0 / 5.0)
fmt.Println(-7/3, -7%3, 7%-3)
n := -7
fmt.Println(((n % 3) + 3) % 3) // always 0..2
}
$ go run arith.go
22 12 85 3 2
3.4
-2 -1 1
2
17/5 is 3, not 3.4: dividing two integers produces an integer. The third line is where Go differs from Python. Division truncates toward zero, so -7/3 is -2, and the remainder keeps the sign of the left operand so that (a/b)*b + a%b == a holds. If you need a modulo that is always non-negative — a ring buffer index, a day of the week — use ((n % m) + m) % m.
++ and — are statements
They exist only in postfix form and cannot appear inside an expression.
package main
import "fmt"
func main() {
x := 5
x++
y := x++
fmt.Println(x, y)
}
$ go run incr.go
# command-line-arguments
./incr.go:8:8: syntax error: unexpected ++ at end of statement
x++ on its own line compiles; y := x++ is a syntax error, because the grammar has no expression form of ++ — it produces no value to assign. Write x++ and then y := x. The rule removes the C puzzles about whether i++ + ++i is defined.
Comparison
Comparison operators return a bool and work on numbers, strings, pointers, and structs whose fields are all comparable.
package main
import "fmt"
type Point struct{ X, Y int }
func main() {
fmt.Println(10 < 20, 10 == 20, 10 != 20)
fmt.Println("apple" < "banana", "Zebra" < "apple")
fmt.Println(Point{1, 2} == Point{1, 2})
}
$ go run compare.go
true false true
true true
true
Strings compare byte by byte, so every uppercase ASCII letter sorts before every lowercase one — "Zebra" < "apple" is true. Structs compare field by field with ==; slices, maps and functions cannot be compared with == at all (except against nil).
Logical operators short-circuit
&& skips its right side when the left is false; || skips it when the left is true.
package main
import "fmt"
func check(name string, v bool) bool {
fmt.Println(" evaluated", name)
return v
}
func main() {
fmt.Println("&&:", check("A", false) && check("B", true))
fmt.Println("||:", check("C", true) || check("D", true))
var m map[string]int
if m != nil && m["k"] > 0 { // safe: the index is never reached
fmt.Println("found")
}
fmt.Println("done")
}
$ go run shortcircuit.go
evaluated A
&&: false
evaluated C
||: true
done
B and D never print: the result was already decided. The “evaluated” lines appear before the result line because all of Println’s arguments are evaluated before it prints anything. The guard pattern x != nil && x.field... depends on this ordering.
Bitwise operators
&, |, ^ (XOR), <<, >>, unary ^ (NOT), and Go’s own &^ (AND NOT).
package main
import "fmt"
func main() {
var a, b uint8 = 0b1010_1100, 0b1111_0000
fmt.Printf("a&b %08b\n", a&b)
fmt.Printf("a|b %08b\n", a|b)
fmt.Printf("a^b %08b\n", a^b)
fmt.Printf("^a %08b\n", ^a)
fmt.Printf("a<<2 %08b\n", a<<2)
fmt.Printf("a>>2 %08b\n", a>>2)
fmt.Printf("a&^b %08b\n", a&^b)
}
$ go run bits.go
a&b 10100000
a|b 11111100
a^b 01011100
^a 01010011
a<<2 10110000
a>>2 00101011
a&^b 00001100
The values are uint8 so that ^a and a<<2 stay within 8 bits: bits shifted past the top are simply lost (10110000). With a plain int, ^a would be -173, because NOT also flips the sign bit. a &^ b keeps the bits of a where b is 0 — the low nibble 1100 here.
Bit flags
Each flag is one bit, so a set of flags fits in one integer.
package main
import "fmt"
type Perm uint8
const (
Read Perm = 1 << iota // 001
Write // 010
Execute // 100
)
func main() {
p := Read | Write
fmt.Printf("%03b read=%t exec=%t\n", p, p&Read != 0, p&Execute != 0)
p |= Execute // set a bit
p &^= Write // clear a bit
fmt.Printf("%03b write=%t\n", p, p&Write != 0)
}
$ go run flags.go
011 read=true exec=false
101 write=false
|= turns a bit on, &^= turns it off, and p&Flag != 0 tests it. Note the precedence: & binds tighter than !=, so p&Read != 0 means (p&Read) != 0 — the opposite of C, where that expression would need parentheses.
Address and dereference
&x takes the address of a variable; *p reads or writes the value at that address.
package main
import "fmt"
func double(n *int) { *n *= 2 }
func main() {
x := 21
p := &x
fmt.Printf("%T %t\n", p, p == &x)
double(p)
fmt.Println(x, *p)
}
$ go run pointers.go
*int true
42 42
double receives a copy of the address, so writing through it changes x in main. Printing p itself would show a heap or stack address that differs from run to run; comparing it with &x shows what matters — they are the same pointer.
No ternary operator
There is no cond ? a : b. Use if/else, or the built-in max/min (Go 1.21+) for the most common case.
package main
import "fmt"
func main() {
a, b := 7, 12
label := "small"
if a+b > 15 {
label = "large"
}
fmt.Println(label, max(a, b), min(a, b, 3))
}
$ go run ternary.go
large 12 3
Declaring label with its default and overriding it in an if is the idiomatic shape. max and min accept any number of arguments of an ordered type.
Precedence
| Precedence | Operators |
|---|---|
| 5 (highest) | * / % << >> & &^ |
| 4 | + - | ^ |
| 3 | == != < <= > >= |
| 2 | && |
| 1 (lowest) | || |
Five levels only, and shifts and & sit with multiplication — so 1 + 2<<3 is 1 + (2<<3):
package main
import "fmt"
func main() {
fmt.Println(1+2<<3, (1+2)<<3)
fmt.Println(2+3*4, (2+3)*4)
x, y := 5, -1
fmt.Println(x > 0 && y > 0 || x > 4)
}
$ go run prec.go
17 24
14 20
true
&& binds tighter than ||, so the last line is (x>0 && y>0) || x>4, which is true. Parentheses cost nothing; add them whenever a reader would have to look up the table.
Practice
1. Write isLeap(year int) bool using %, && and ||: divisible by 4, except centuries, except every 400 years. Test 1900, 2000, 2024 and 2023.
Solution
package main
import "fmt"
func isLeap(y int) bool {
return y%4 == 0 && y%100 != 0 || y%400 == 0
}
func main() {
for _, y := range []int{1900, 2000, 2024, 2023} {
fmt.Println(y, isLeap(y))
}
}
$ go run p1.go
1900 false
2000 true
2024 true
2023 false
&& binds tighter than ||, so no parentheses are needed — though adding them around the && part makes the rule easier to read.
2. Using the Perm flags from flags.go, write has(p, want Perm) bool that is true only when all bits of want are set in p.
Solution
package main
import "fmt"
type Perm uint8
const (
Read Perm = 1 << iota
Write
Execute
)
func has(p, want Perm) bool { return p&want == want }
func main() {
p := Read | Execute
fmt.Println(has(p, Read), has(p, Read|Execute), has(p, Read|Write))
}
$ go run p2.go
true true false
p&want != 0 would test for any of the bits; comparing with want requires all of them.
3. Write wrap(i, n int) int that maps any integer, including negatives, into 0..n-1, and print wrap of -7, -1, 0 and 8 with n = 3.
Solution
package main
import "fmt"
func wrap(i, n int) int { return ((i % n) + n) % n }
func main() {
for _, i := range []int{-7, -1, 0, 8} {
fmt.Print(wrap(i, 3), " ")
}
fmt.Println()
}
$ go run p3.go
2 2 0 2
i % n is in -(n-1)..n-1; adding n makes it non-negative and the second % n brings it back into range.
4. Check whether an int is a power of two using one bitwise expression, and test it on 1, 6, 64 and 0.
Solution
package main
import "fmt"
func isPow2(n int) bool { return n > 0 && n&(n-1) == 0 }
func main() {
for _, n := range []int{1, 6, 64, 0} {
fmt.Println(n, isPow2(n))
}
}
$ go run p4.go
1 true
6 false
64 true
0 false
A power of two has exactly one bit set; subtracting 1 flips that bit and sets all lower ones, so the AND is 0. The n > 0 guard is needed because 0 & -1 is also 0.