Skip to main content
Go · lesson 5 of 25
Go beginner 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

PrecedenceOperators
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.

Frequently Asked Questions

Why doesn't Go have a ternary operator?
The Go authors left out condition ? a : b because it is easy to nest into unreadable one-liners. Use an if/else statement instead, or for a numeric choice like the larger of two values, the built-in max and min functions added in Go 1.21.
Does Go support operator overloading?
No. Operators work only on the built-in types they are defined for, so a + b on two structs does not compile. Define a method such as a.Add(b) instead; you always know what + means when you read Go code.
What does the &^ operator do in Go?
&^ is AND NOT, also called bit clear. a &^ b keeps the bits of a that are not set in b, so flags &^= Write turns off the Write bit and leaves every other bit alone. It is equivalent to a & (^b).
Why does -7 % 3 give -1 in Go?
Go's integer division truncates toward zero, and the remainder takes the sign of the dividend so that (a/b)*b + a%b == a always holds. -7/3 is -2, so -7%3 is -1. For a result that is always non-negative, use ((a % n) + n) % n.
Can I use x++ inside an expression in Go?
No. In Go, x++ and x-- are statements, not expressions, so y := x++ or if x++ > 5 do not compile. There is also no prefix form ++x. Increment on its own line, then use the variable.