Operators in Go
Arithmetic, comparison, logical, bitwise, and assignment operators in Go — plus why Go has no ternary operator.
Arithmetic Operators
Arithmetic operators work on numeric types. Go requires that both operands have the same type — there is no implicit promotion from int to float64. This strictness prevents subtle precision bugs but means you must convert explicitly when mixing types.
a, b := 17, 5
fmt.Println(a + b) // 22 — addition
fmt.Println(a - b) // 12 — subtraction
fmt.Println(a * b) // 85 — multiplication
fmt.Println(a / b) // 3 — integer division truncates toward zero
fmt.Println(a % b) // 2 — remainder (modulo)
// Float division — both operands must be float
x, y := 17.0, 5.0
fmt.Println(x / y) // 3.4
// Mixed types require explicit conversion — Go will not do this silently
var i int = 10
var f float64 = 3.0
result := float64(i) / f // must convert i to float64 first
fmt.Println(result) // 3.3333...
Increment and Decrement
Go has ++ and -- as statements, not expressions. This means you cannot use them inside a larger expression — a deliberate choice that prevents the class of bugs caused by i++ appearing in unexpected places in C code.
x := 5
x++ // x is now 6
x-- // x is now 5
// These are compile errors in Go:
// y := x++ // ++ is a statement, not an expression
// if x++ > 5 {} // cannot use ++ inside a condition
Comparison Operators
Comparison operators evaluate a relationship between two values and always return a bool. They work on any comparable type — numbers, strings, booleans, pointers, and structs whose fields are all comparable.
a, b := 10, 20
fmt.Println(a == b) // false — equal
fmt.Println(a != b) // true — not equal
fmt.Println(a < b) // true — less than
fmt.Println(a > b) // false — greater than
fmt.Println(a <= b) // true — less than or equal
fmt.Println(a >= b) // false — greater than or equal
// Strings are compared lexicographically (byte by byte)
fmt.Println("apple" < "banana") // true
fmt.Println("abc" == "abc") // true
Logical Operators
Logical operators combine boolean expressions. Go uses short-circuit evaluation: the right side of && is only evaluated if the left side is true, and the right side of || is only evaluated if the left side is false. This matters when the right side has side effects or is expensive to compute.
t, f := true, false
fmt.Println(t && f) // false — AND: both sides must be true
fmt.Println(t || f) // true — OR: at least one side must be true
fmt.Println(!t) // false — NOT: inverts the boolean value
// Short-circuit evaluation — right side is skipped when the result is already determined
func expensive() bool {
fmt.Println("called")
return true
}
if false && expensive() {
// expensive() is never called — left side is false, so AND is already false
}
if true || expensive() {
// expensive() is never called — left side is true, so OR is already true
}
Assignment Operators
Compound assignment operators combine an arithmetic or bitwise operation with assignment. They are shorthand — x += 5 is exactly equivalent to x = x + 5 — but they make the intent clearer and reduce the chance of typos when the variable name is long.
x := 10
x += 5 // x = x + 5 → 15
x -= 3 // x = x - 3 → 12
x *= 2 // x = x * 2 → 24
x /= 4 // x = x / 4 → 6
x %= 4 // x = x % 4 → 2
// Bitwise compound assignment
x &= 0b1111 // bitwise AND assign — clears bits not in mask
x |= 0b1000 // bitwise OR assign — sets specific bits
x ^= 0b0011 // bitwise XOR assign — toggles specific bits
x <<= 2 // left shift assign — multiply by 4
x >>= 1 // right shift assign — divide by 2
Bitwise Operators
Bitwise operators work directly on the binary representation of integers. They are primarily used for low-level tasks: working with hardware registers, network protocols, encoding flags, and writing high-performance code that manipulates individual bits. Go also includes &^ (AND NOT), which is unique to Go and particularly useful for clearing specific bits.
a := 0b1010_1100 // 172
b := 0b1111_0000 // 240
fmt.Printf("%08b\n", a & b) // 10100000 — AND: bit is 1 only if both are 1
fmt.Printf("%08b\n", a | b) // 11111100 — OR: bit is 1 if either is 1
fmt.Printf("%08b\n", a ^ b) // 01011100 — XOR: bit is 1 if exactly one is 1
fmt.Printf("%08b\n", ^a) // NOT: flips all bits (bitwise complement)
fmt.Printf("%08b\n", a << 2) // 10110000 — left shift: multiply by 2^2
fmt.Printf("%08b\n", a >> 2) // 00101011 — right shift: divide by 2^2
// &^ is the AND NOT (bit clear) operator — unique to Go
// Clears the bits in a that are set in b
fmt.Printf("%08b\n", a &^ b) // 00001100
Practical bitwise use — permission flags
Bit flags are a compact way to represent a set of boolean options in a single integer. Each permission is a distinct bit position, so you can combine, check, and remove permissions using bitwise operations. This pattern appears throughout operating systems, networking, and embedded systems.
const (
Read = 1 << iota // 001 — bit 0
Write // 010 — bit 1
Execute // 100 — bit 2
)
perms := Read | Write // 011 — has Read and Write
fmt.Println(perms & Read != 0) // true — has read permission
fmt.Println(perms & Execute != 0) // false — does not have execute
// Add execute permission by setting that bit
perms |= Execute
fmt.Println(perms & Execute != 0) // true
// Remove write permission by clearing that bit
perms &^= Write
fmt.Println(perms & Write != 0) // false
Address and Pointer Operators
The & operator takes the address of a variable, giving you a pointer. The * operator dereferences a pointer, giving you the value it points to. Pointers are important in Go for passing large structs efficiently and for allowing functions to modify their arguments.
x := 42
p := &x // p is a *int — it holds the memory address of x
fmt.Println(p) // 0xc000018060 (some memory address)
fmt.Println(*p) // 42 — dereference: read the value stored at that address
*p = 100 // write through the pointer — modifies x directly
fmt.Println(x) // 100 — x was changed via the pointer
No Ternary Operator
Go does not have a condition ? a : b expression. The Go authors omitted it because ternary expressions tend to be abused, leading to dense, hard-to-read one-liners. The explicit if/else is slightly more verbose but always unambiguous.
// In other languages: max := a > b ? a : b
// In Go, write the full if/else:
var max int
if a > b {
max = a
} else {
max = b
}
// A helper function works for simple cases, but if/else is more idiomatic
func ternary(cond bool, a, b int) int {
if cond {
return a
}
return b
}
max = ternary(a > b, a, b)
For simple cases, a helper function like ternary works, but the explicit if/else is the idiomatic Go approach.
Operator Precedence
Go evaluates higher-precedence operators first. When in doubt, use parentheses — they cost nothing and make your intent unambiguous to both the compiler and future readers.
| Precedence | Operators |
|---|---|
| 5 (highest) | *, /, %, <<, >>, &, &^ |
| 4 | +, -, |, ^ |
| 3 | ==, !=, <, <=, >, >= |
| 2 | && |
| 1 (lowest) | || |
When in doubt, use parentheses to make intent explicit:
result := (a + b) * c // clear — addition happens first
result2 := a + b*c // multiplication happens first (b*c), then addition
result3 := x > 0 && y > 0 // both comparisons evaluated before &&
Practical Example
package main
import "fmt"
// Uses modulo and logical operators to implement the leap year rule
func isLeapYear(year int) bool {
// Divisible by 400 → always a leap year
// Divisible by 100 → not a leap year (unless divisible by 400)
// Divisible by 4 → leap year
return (year%4 == 0 && year%100 != 0) || year%400 == 0
}
func main() {
years := []int{1900, 2000, 2024, 2023}
for _, y := range years {
if isLeapYear(y) {
fmt.Printf("%d is a leap year\n", y)
} else {
fmt.Printf("%d is not a leap year\n", y)
}
}
}
Output:
1900 is not a leap year
2000 is a leap year
2024 is a leap year
2023 is not a leap year