Skip to main content
Go beginner Lesson 5 of 25

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.

PrecedenceOperators
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

Frequently Asked Questions

Why doesn't Go have a ternary operator?
The Go authors deliberately omitted it. They found that ternary expressions are often abused to write convoluted one-liners. Go forces you to use a full if/else, which is slightly more verbose but always readable.
Does Go support operator overloading?
No. Operator overloading is not supported in Go. This is an intentional design decision to keep code predictable — you always know what + means.
What does the & operator do on a variable?
& is the address-of operator. It returns a pointer to the variable. For example, p := &x gives p the memory address of x. This is different from && (logical AND).