Dev Skills

Operators

Arithmetic, comparison, logical, and bitwise, plus what Go leaves out on purpose.

Topic 6 of 98 · 10 min to read · 50 min to build
After this topic you can
  • Predict the output of integer division, %, and shifts such as 3 << 10.
  • Write a boolean expression with %, && and || that answers a yes/no question.
  • Explain why d != 0 && 10/d > 2 never divides by zero.

What an operator is

An operator is a symbol that does something to one or two values: + adds them, == compares them, && combines two true-or-false answers. They are the smallest building blocks of any expression you write.

Go's operators are worth a proper look for two reasons. A few behave differently from other languages: integer division throws away the remainder, and ++ is a statement rather than something you can use inside an expression. And a few that other languages have, Go leaves out on purpose.

Arithmetic

arithmetic.go
a, b := 10, 3
fmt.Println(a + b) // 13
fmt.Println(a - b) // 7
fmt.Println(a * b) // 30
fmt.Println(a / b) // 3 — INTEGER division truncates, it does not round
fmt.Println(a % b) // 1 — remainder
fmt.Println(float64(a) / float64(b)) // 3.3333333333333335 — convert first if you want a decimal
a++ // a statement, NOT an expression. "x := a++" does not compile.
a-- // and there is no ++a either
Integer division truncates toward zero, which surprises people coming from JavaScript or Python 3. 7/2 is 3, not 3.5, and -7/2 is -3.

Assignment

assignment.go
x := 10 // declare and assign (only inside a function)
x = 20 // assign to an existing variable
var y int = 5 // declare with an explicit type
fmt.Println(x, y)
x += 5 // x = x + 5
x -= 5
x *= 2
x /= 2
x %= 3
x <<= 1 // the bitwise ones work too
x &= 0xFF
a, b := 1, 2
a, b = b, a // swap in one line, no temp variable
fmt.Println(a, b) // 2 1

Comparison

comparison.go
package main
import "fmt"
func main() {
a, b := 3, 5
fmt.Println(a == b) // false
fmt.Println(a != b) // true
fmt.Println(a < b, a >= b) // true false
fmt.Println(2.5 > 2.0) // true — floats compare the same way
fmt.Println("apple" < "banana") // true — strings compare byte by byte
fmt.Println("Go" == "go") // false — case matters
fmt.Println(true == !false) // true — bools compare with == and != only
// Both sides must be the same type. This does not compile:
// fmt.Println(a == 2.5) // a is an int, 2.5 is not
}
Every comparison gives back a `bool`. Later lessons show which other types can be compared with `==` and which cannot.

Logical, and short-circuiting

logical.go
package main
import "fmt"
func main() {
a, b := true, false
fmt.Println(a && b) // false — and
fmt.Println(a || b) // true — or
fmt.Println(!a) // false — not
// Both short-circuit: if the left side already decides the answer,
// the right side is never run. That is what makes this safe:
d := 0
if d != 0 && 10/d > 2 { // 10/d is never reached when d is 0
fmt.Println("big")
} else {
fmt.Println("d is zero, skipped the division")
}
}
Short-circuiting is not a micro-optimisation. Without it, `10/d` would crash the program with "integer divide by zero".

Bitwise

These work on the individual bits (the 1s and 0s) of an integer. %b prints a number in binary so you can watch them.

Shifts move every bit left or right. x << n moves the bits n places left, which is the same as multiplying by 2, n times. x >> n moves them right, which is dividing by 2, n times (rounding down). So 1 << 10 is 1024 and 10 << 20 is 10 × 2²⁰ = 10 × 1,048,576, which is 10 MB. You will see that last one in real code as an upload size limit.

bitwise.go
a, b := 0b1100, 0b1010
fmt.Printf("%04b\n", a&b) // 1000 AND
fmt.Printf("%04b\n", a|b) // 1110 OR
fmt.Printf("%04b\n", a^b) // 0110 XOR
fmt.Printf("%04b\n", a&^b) // 0100 AND NOT (bit clear) — this one is unique to Go
fmt.Println(a<<2, a>>2) // 48 3 — shift left (a * 4), shift right (a / 4, rounding down)
// A common use: flags packed into one integer
const (
CanRead = 1 << iota // 1
CanWrite // 2
CanAdmin // 4
)
perms := CanRead | CanWrite // set two flags
has := perms&CanWrite != 0 // test one
perms &^= CanWrite // clear one
fmt.Println(has, perms) // true 1
// Shifts as sizes: 10 << 20 is 10 * 1,048,576 bytes
const maxUpload = 10 << 20 // 10 MB
fmt.Println(maxUpload) // 10485760

Two operators for later

You will also meet & and * in front of a variable, and <- next to a channel. Those are not maths: they belong to pointers and channels, and each gets its own lesson.

(preview — explained in Pointers, and Channels and select)
p := &x // p holds the address of x
v := <-ch // take a value out of channel ch

What Go leaves out on purpose

  • No ternary `?:`. Write the if. The Go FAQ says the readability cost of nested ternaries is not worth the saved line.
  • No operator overloading. You cannot define + for your own struct. a + b is numeric addition, or concatenation for strings, and nothing else.
  • No pointer arithmetic. You cannot do p + 1 to walk memory (outside the unsafe package). That is a large part of what makes Go memory-safe.
  • No `++` as an expression. It is a statement, so arr[i++] does not compile, and neither do the order-of-evaluation bugs it allows in C.
Note: Precedence is simpler than in C: five levels, with * above + above comparison above && above ||. When in doubt, add brackets. gofmt keeps them, and the next reader does not have to remember the table.

Try it yourself

press Run — this executes on the Go Playground
package main
import "fmt"
const (
CanRead = 1 << iota // 1
CanWrite // 2
CanAdmin // 4
)
func loud(name string, v bool) bool {
fmt.Println(" evaluated", name)
return v
}
func main() {
fmt.Println("7 / 2 =", 7/2) // 3, truncated
fmt.Println("7.0 / 2 =", float64(7)/2) // 3.5
fmt.Println("short-circuit && :")
_ = loud("left", false) && loud("right", true) // right never runs
perms := CanRead | CanWrite
fmt.Printf("perms = %04b\n", perms)
fmt.Println("can write? ", perms&CanWrite != 0)
perms &^= CanWrite
fmt.Printf("after clear= %04b\n", perms)
}
Integer division, short-circuiting, and bit flags in one program.
The short version
  • Integer division truncates. Convert to float64 first if you want a decimal.
  • Comparisons give a bool, and both sides must be the same type.
  • && and || short-circuit, which is what makes x != nil && x.Field safe.
  • No ternary, no operator overloading, no pointer arithmetic. All three are left out on purpose.
Remember: None of Go’s operators can be redefined. a + b means addition, or concatenation for strings, in every file you will ever read.
Build this before moving on
Build a permissions integer with iota bit flags: set two permissions, test for one, clear one, and print the result with %b to watch the bits move.
Coding challenge · checked by tests
Write IsLeapYear(year int) bool using only %, ==, !=, && and || (no if). A year is a leap year if it divides by 4, except years that divide by 100, unless they also divide by 400. Examples: 2024 → true, 1900 → false, 2000 → true, 2023 → false.

Tab indents. Press Esc, then Tab, to leave the editor.

Then try these in your own editor
  1. 1Print 17 / 5, 17 % 5, and float64(17) / 5. Predict each before you run it.
  2. 2Write a permissions integer with three iota bit flags. Set two, test one, clear one, and print with %03b after each step.
  3. 3Prove short-circuiting to yourself: write f() and g() that each print their name and return a bool, then run f() && g(). Make f return false and see that g never runs.
  4. 4Without running it, work out 1 << 3, 40 >> 2, and 3 << 10. Then check.
Check yourselfTake the Getting started test