Go Operators: Arithmetic, Comparison, and Logical

Operators are the fundamental building blocks of any programming language, and Go keeps them straightforward and predictable. Unlike languages with operator overloading or complex precedence rules,...

Key Insights

  • Go’s arithmetic operators behave differently for integers versus floats—integer division truncates toward zero, which catches many developers off guard when porting code from other languages
  • Comparison operators in Go are type-strict: you cannot compare values of different types without explicit conversion, preventing an entire class of subtle bugs common in dynamically-typed languages
  • Logical operators use short-circuit evaluation, meaning && stops at the first false condition and || stops at the first true condition—leverage this for performance and safe nil checks

Introduction to Operators in Go

Operators are the fundamental building blocks of any programming language, and Go keeps them straightforward and predictable. Unlike languages with operator overloading or complex precedence rules, Go’s operators work exactly as you’d expect with minimal surprises.

Go organizes operators into several categories: arithmetic, comparison, logical, bitwise, and others. This article focuses on the three most commonly used: arithmetic, comparison, and logical operators. Understanding these thoroughly will make you productive in Go immediately.

package main

import "fmt"

func main() {
    // Expression combining multiple operator types
    x := 10
    y := 3
    result := (x + y) * 2 > 20 && x%2 == 0
    fmt.Printf("Result: %v\n", result) // Output: Result: true
}

Arithmetic Operators

Go provides five basic arithmetic operators: + (addition), - (subtraction), * (multiplication), / (division), and % (modulus). These work on numeric types including integers, floats, and complex numbers.

The most important gotcha is integer division behavior. When you divide two integers, Go truncates toward zero, discarding any remainder:

package main

import "fmt"

func main() {
    // Integer division truncates
    a := 7 / 2
    fmt.Println(a) // Output: 3, not 3.5

    // At least one operand must be float for float division
    b := 7.0 / 2
    fmt.Println(b) // Output: 3.5

    // Type conversion for float result
    c := float64(7) / float64(2)
    fmt.Println(c) // Output: 3.5
}

The modulus operator % returns the remainder of integer division. It’s incredibly useful for cyclic operations, checking even/odd numbers, and wrapping values:

func isEven(n int) bool {
    return n%2 == 0
}

func wrapIndex(index, length int) int {
    return index % length
}

func main() {
    fmt.Println(isEven(42))        // true
    fmt.Println(isEven(17))        // false
    fmt.Println(wrapIndex(13, 10)) // 3
}

Go also supports compound assignment operators that combine arithmetic with assignment: +=, -=, *=, /=, and %=. These modify a variable in place:

counter := 10
counter += 5  // Same as: counter = counter + 5
counter *= 2  // Same as: counter = counter * 2
fmt.Println(counter) // Output: 30

One critical point: Go does not have increment/decrement expressions (++i or i++ as expressions). The ++ and -- operators exist only as statements:

i := 0
i++           // Valid statement
// x := i++   // Invalid! Cannot use as expression

Comparison (Relational) Operators

Comparison operators evaluate relationships between values and return boolean results. Go provides six comparison operators: == (equal), != (not equal), < (less than), > (greater than), <= (less than or equal), and >= (greater than or equal).

Go’s type system is strict about comparisons. You cannot compare values of different types without explicit conversion:

package main

import "fmt"

func main() {
    var a int = 10
    var b int64 = 10
    
    // fmt.Println(a == b) // Compile error! Cannot compare int and int64
    
    // Explicit conversion required
    fmt.Println(a == int(b))      // true
    fmt.Println(int64(a) == b)    // true
}

This strictness prevents subtle bugs that plague languages with automatic type coercion. It’s slightly more verbose but significantly safer.

String comparisons work lexicographically (dictionary order):

func main() {
    fmt.Println("apple" < "banana")  // true
    fmt.Println("Apple" < "apple")   // true (uppercase comes first in ASCII)
    fmt.Println("abc" == "abc")      // true
}

Structs can be compared if all their fields are comparable. Two struct values are equal if all corresponding fields are equal:

type Point struct {
    X, Y int
}

func main() {
    p1 := Point{X: 1, Y: 2}
    p2 := Point{X: 1, Y: 2}
    p3 := Point{X: 2, Y: 1}
    
    fmt.Println(p1 == p2) // true
    fmt.Println(p1 == p3) // false
}

However, structs containing slices, maps, or functions cannot be compared with == because these types are not comparable. You’ll need to write custom comparison logic.

Logical Operators

Logical operators combine or modify boolean values. Go provides three: && (AND), || (OR), and ! (NOT). These operators use short-circuit evaluation, which is both a performance optimization and a safety feature.

With &&, if the left operand is false, Go doesn’t evaluate the right operand because the result must be false regardless:

func expensiveCheck() bool {
    fmt.Println("Expensive check executed")
    return true
}

func main() {
    // expensiveCheck() never runs because false && anything is false
    if false && expensiveCheck() {
        fmt.Println("This won't print")
    }
    // Output: (nothing)
}

Similarly, with ||, if the left operand is true, Go skips the right operand:

func main() {
    if true || expensiveCheck() {
        fmt.Println("This prints")
    }
    // Output: This prints
    // expensiveCheck() was never called
}

This behavior is essential for safe nil checks:

type User struct {
    Name string
}

func processUser(u *User) {
    // Safe: if u is nil, the second condition isn't evaluated
    if u != nil && u.Name != "" {
        fmt.Printf("Processing user: %s\n", u.Name)
    }
}

func main() {
    processUser(nil)                    // No panic
    processUser(&User{Name: "Alice"})   // Processing user: Alice
}

The NOT operator ! negates a boolean value:

func main() {
    active := true
    if !active {
        fmt.Println("Inactive")
    }
    
    // Double negation for clarity (though not idiomatic)
    fmt.Println(!!active) // true
}

Common pattern for input validation:

func validateUser(username, email string, age int) bool {
    return username != "" && 
           email != "" && 
           len(email) > 5 && 
           age >= 18 && 
           age < 120
}

Operator Precedence and Best Practices

Go’s operator precedence follows conventional mathematical rules, but memorizing the entire precedence table is unnecessary. Use parentheses liberally for clarity:

// Precedence: *, /, % > +, - > ==, != > && > ||
result := 5 + 3 * 2 == 11 && 10 > 5 // true, but hard to read

// Better: use parentheses for clarity
result = ((5 + (3 * 2)) == 11) && (10 > 5) // Same result, clearer intent

Here’s the precedence for operators covered in this article (highest to lowest):

  1. *, /, %
  2. +, -
  3. ==, !=, <, >, <=, >=
  4. &&
  5. ||

Common pitfalls to avoid:

// Pitfall 1: Integer division when you want float division
average := (a + b) / 2 // If a and b are ints, result is truncated
average := float64(a+b) / 2.0 // Correct

// Pitfall 2: Comparing different numeric types
var x int = 5
var y int64 = 5
// if x == y {} // Won't compile
if int64(x) == y {} // Correct

// Pitfall 3: Forgetting short-circuit evaluation doesn't apply to function calls
result := isValid() && process() // process() only runs if isValid() is true

Practical Applications

Let’s combine these operators in real-world scenarios. Here’s a simple calculator function:

func calculate(a, b float64, operator string) (float64, error) {
    switch operator {
    case "+":
        return a + b, nil
    case "-":
        return a - b, nil
    case "*":
        return a * b, nil
    case "/":
        if b == 0 {
            return 0, fmt.Errorf("division by zero")
        }
        return a / b, nil
    default:
        return 0, fmt.Errorf("unknown operator: %s", operator)
    }
}

Input validation combining multiple operators:

func validatePassword(password string, username string) bool {
    length := len(password)
    hasUsername := strings.Contains(password, username)
    
    return length >= 8 && 
           length <= 128 && 
           !hasUsername &&
           containsDigit(password) &&
           containsSpecialChar(password)
}

Classic FizzBuzz implementation showcasing modulus and logical operators:

func fizzBuzz(n int) {
    for i := 1; i <= n; i++ {
        divBy3 := i%3 == 0
        divBy5 := i%5 == 0
        
        if divBy3 && divBy5 {
            fmt.Println("FizzBuzz")
        } else if divBy3 {
            fmt.Println("Fizz")
        } else if divBy5 {
            fmt.Println("Buzz")
        } else {
            fmt.Println(i)
        }
    }
}

Conclusion

Go’s arithmetic, comparison, and logical operators are straightforward but come with important nuances. Remember that integer division truncates, type conversions are explicit, and logical operators short-circuit. These characteristics make Go code predictable and safe.

The key to mastery is practice. Write small programs that exercise these operators. Implement calculators, validation functions, and simple algorithms. Pay attention to type conversions and use parentheses when precedence isn’t immediately clear.

Next, explore Go’s bitwise operators for low-level manipulation and pointer operators for working with memory addresses. These build on the foundation you’ve established here, enabling more advanced Go programming patterns.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.