Operators #
Operators in Go look familiar to anyone who has written code in C, Java, or Python — but there are several design decisions that make Go different. ++ and -- are statements, not expressions, so you can’t write x = y++. There’s no ternary operator ? :. There’s no ** for exponentiation. There’s the &^ operator (AND NOT) that doesn’t exist in most other languages. And there’s the <- operator, specific to channels, which is at the heart of Go’s concurrency. Understanding Go operators isn’t just memorizing symbols — it’s about understanding why the design is the way it is.
Arithmetic Operators #
Arithmetic operators work on numeric types and follow standard mathematical rules, with a few important caveats.
a := 17
b := 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 (not 3.4!)
fmt.Println(a % b) // 2 — modulus (remainder)
Integer Division — A Gotcha That Often Surprises #
When both operands are integers, / performs integer division — the result is always truncated toward zero, not rounded down:
fmt.Println(7 / 2) // 3 (not 3.5)
fmt.Println(-7 / 2) // -3 (not -4, truncated toward ZERO, not rounded down)
fmt.Println(7 / -2) // -3
// ANTI-PATTERN: a calculation needing decimals but forgetting to convert
func calcAverage(total, count int) float64 {
return float64(total / count) // ✗ integer division first, then conversion
// total=7, count=2 → 7/2=3 → float64(3) = 3.0 (not 3.5!)
}
// CORRECT: convert to float64 BEFORE dividing
func calcAverage(total, count int) float64 {
return float64(total) / float64(count) // ✓ 7.0/2.0 = 3.5
}
For float division, one or both operands must be floats:
fmt.Println(7.0 / 2) // 3.5
fmt.Println(7 / 2.0) // 3.5
fmt.Println(float64(7) / float64(2)) // 3.5
Modulus and Negative Values #
The % operator in Go follows this rule: the sign of the result always matches the left operand (the dividend):
fmt.Println( 7 % 3) // 1
fmt.Println(-7 % 3) // -1 (negative sign follows -7)
fmt.Println( 7 % -3) // 1 (positive sign follows 7)
fmt.Println(-7 % -3) // -1
// A safe even/odd check (works for negative numbers)
func isEven(n int) bool {
return n%2 == 0 // safe: -4%2 = 0 (even), -3%2 = -1 (odd, non-zero)
}
// If you need an always-positive modulus (like Python):
func positiveMod(a, b int) int {
return ((a % b) + b) % b
}
fmt.Println(positiveMod(-7, 3)) // 2 (not -1)
The + Operator on Strings
#
The + operator also works on strings for concatenation:
firstName := "Budi"
lastName := "Santoso"
fullName := firstName + " " + lastName
fmt.Println(fullName) // "Budi Santoso"
// But remember: every + creates a new string in memory
// For lots of concatenation, use strings.Builder (see the Data Types article)
Comparison Operators #
Comparison operators always produce a bool. All comparison operators work as expected for basic types, but there’s a comparability rule to understand for composite types.
a, b := 10, 20
fmt.Println(a == b) // false — equal to
fmt.Println(a != b) // true — not equal to
fmt.Println(a < b) // true — less than
fmt.Println(a > b) // false — greater than
fmt.Println(a <= b) // true — less than or equal to
fmt.Println(a >= b) // false — greater than or equal to
Comparability — Not All Types Can Be Compared #
// Types that CAN be compared with == and !=:
// int, float, bool, string, pointer, channel, interface, struct (if all its fields are comparable)
// Structs can be compared if all their fields are comparable
type Point struct {
X, Y int
}
p1 := Point{1, 2}
p2 := Point{1, 2}
fmt.Println(p1 == p2) // true — all fields are equal
// Types that CANNOT be compared with == (compile error):
s1 := []int{1, 2, 3}
s2 := []int{1, 2, 3}
// fmt.Println(s1 == s2) // ← compile error: slice can only be compared to nil
fmt.Println(s1 == nil) // ✓ slices can only be compared with nil
m := map[string]int{"a": 1}
// fmt.Println(m == map[string]int{"a": 1}) // ← compile error
fmt.Println(m == nil) // ✓ maps can only be compared with nil
// To compare slices/maps, use reflect.DeepEqual or a manual loop
import "reflect"
fmt.Println(reflect.DeepEqual(s1, s2)) // true
Pointer Comparison #
x := 42
p1 := &x
p2 := &x
p3 := new(int)
*p3 = 42
fmt.Println(p1 == p2) // true — both point to the same variable x
fmt.Println(p1 == p3) // false — point to different memory, even though the values are the same
fmt.Println(*p1 == *p3) // true — the pointed-to values are equal
String Comparison #
Strings are compared lexicographically — based on byte order:
fmt.Println("apple" == "apple") // true
fmt.Println("apple" < "banana") // true — 'a' < 'b'
fmt.Println("apple" < "Apple") // false — 'a' (97) > 'A' (65) in ASCII
fmt.Println("abc" < "abd") // true — same until 'c' vs 'd'
// Case-insensitive comparison:
import "strings"
fmt.Println(strings.EqualFold("Go", "go")) // true — case-insensitive
fmt.Println(strings.EqualFold("GO", "go")) // true
Logical Operators #
Logical operators work on bool values and always produce a bool.
x, y := true, false
fmt.Println(x && y) // false — AND: both must be true
fmt.Println(x || y) // true — OR: one being true is enough
fmt.Println(!x) // false — NOT: negation
Short-Circuit Evaluation — More Than Just an Optimization #
Short-circuiting is a fundamental property of && and ||: Go does not evaluate the right operand if the result can already be determined from the left operand:
false && anything→ alwaysfalse, the right operand is not evaluatedtrue || anything→ alwaystrue, the right operand is not evaluated
The working principle of short-circuit evaluation on the && and || logical operators can be visualized in the following diagram:
flowchart TD
subgraph AndEval["AND Evaluation (&&)"]
A1["Left Condition"] --> CheckA{"Left Result?"}
CheckA -->|"False"| RetFalse["Immediately false (Right ignored)"]
CheckA -->|"True"| EvalRightA["Evaluate Right Condition"]
end
subgraph OrEval["OR Evaluation (||)"]
O1["Left Condition"] --> CheckO{"Left Result?"}
CheckO -->|"True"| RetTrue["Immediately true (Right ignored)"]
CheckO -->|"False"| EvalRightO["Evaluate Right Condition"]
endThis isn’t just about performance — it’s a critical safety pattern:
// Pattern 1: nil guard — check nil before dereferencing
var user *User = nil
if user != nil && user.IsAdmin() {
// user.IsAdmin() won't be called if user == nil
// without short-circuit, this would panic
fmt.Println("admin")
}
// Pattern 2: chained validation — stop early if anything fails
func isValidRequest(r *Request) bool {
return r != nil &&
len(r.Body) > 0 &&
len(r.Body) <= MaxBodySize &&
isValidToken(r.Token) // only checked if all previous conditions are true
}
// Pattern 3: lazy initialization with ||
func getConfig() *Config {
return cachedConfig || loadFromDisk()
// loadFromDisk() is only called if cachedConfig is falsy
}
// Pattern 4: expensive conditions on the right
if isSimpleCheck(x) && isExpensiveCheck(x) {
// isExpensiveCheck is only called if isSimpleCheck is true
}
Put the condition most likely to be false on the left side of&&, and the condition most likely to be true on the left side of||. This maximizes the short-circuit benefit — expensive operations on the right only run when truly needed.
Bitwise Operators #
Bitwise operators work on the binary representation of integers. They’re used for bit-level manipulation, permission flags, masking, and certain optimizations.
a := 0b1010 // binary: 1010, decimal: 10
b := 0b1100 // binary: 1100, decimal: 12
Bitwise AND (&) — A Bit Is 1 Only If Both Are 1
#
fmt.Println(a & b)
// 1010
// & 1100
// ------
// 1000 = 8
// Use case: extracting or checking specific bits
const FlagActive = 0b0001
const FlagAdmin = 0b0010
const FlagVIP = 0b0100
userFlags := 0b0011 // Active + Admin
fmt.Println(userFlags & FlagActive != 0) // true — user is active
fmt.Println(userFlags & FlagAdmin != 0) // true — user is admin
fmt.Println(userFlags & FlagVIP != 0) // false — user is not VIP
Bitwise OR (|) — A Bit Is 1 If Either or Both Are 1
#
fmt.Println(a | b)
// 1010
// | 1100
// ------
// 1110 = 14
// Use case: adding a flag
userFlags |= FlagVIP // add the VIP flag
fmt.Println(userFlags & FlagVIP != 0) // true — now VIP
Bitwise XOR (^) — A Bit Is 1 If the Two Differ
#
fmt.Println(a ^ b)
// 1010
// ^ 1100
// ------
// 0110 = 6
// Use case: toggling a bit (flipping 0 to 1 and vice versa)
userFlags ^= FlagAdmin // toggle admin: removed if present, added if absent
Bitwise AND NOT (&^) — Unique to Go
#
&^ is an operator rarely found in other languages. It clears bits: a bit in the result is 1 only if the first bit is 1 and the second bit is 0:
fmt.Println(a &^ b)
// 1010
// &^1100
// ------
// 0010 = 2 (bits present in a but NOT in b)
// Use case: removing a specific flag
userFlags &^= FlagAdmin // remove the Admin flag, without touching other flags
fmt.Println(userFlags & FlagAdmin != 0) // false — admin has been removed
// In other languages, this is done with: userFlags &= ~FlagAdmin
// In Go, &^ is cleaner and doesn't need the NOT operator (~)
Left Shift (<<) and Right Shift (>>)
#
x := 1
fmt.Println(x << 1) // 2 — multiply by 2
fmt.Println(x << 2) // 4 — multiply by 4
fmt.Println(x << 3) // 8 — multiply by 8
fmt.Println(x << 10) // 1024 — 2^10
fmt.Println(16 >> 1) // 8 — divide by 2
fmt.Println(16 >> 2) // 4 — divide by 4
// Classic use case: defining size constants
const (
KB = 1 << 10 // 1024
MB = 1 << 20 // 1,048,576
GB = 1 << 30 // 1,073,741,824
)
Right shift on signed integers uses arithmetic shift in Go — the sign bit is propagated. This means
-8 >> 1produces-4, not2147483644. If you need a logical shift (filling with 0s), use an unsigned type.fmt.Println(-8 >> 1) // -4 (signed: arithmetic shift) fmt.Println(uint(-8) >> 1) // 9223372036854775804 (unsigned: logical shift)
Assignment Operators #
Go provides compound assignment that combines an operation with assignment:
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
flags := 0b1010
flags &= 0b1100 // AND
flags |= 0b0001 // OR
flags ^= 0b0011 // XOR
flags &^= 0b0010 // AND NOT
flags <<= 1 // left shift
flags >>= 1 // right shift
++ and -- — Statements, Not Expressions
#
In Go, ++ and -- are statements, not expressions. That means they don’t produce a value and can’t be used inside larger expressions:
i := 5
i++ // ✓ valid — statement
i-- // ✓ valid — statement
// ANTI-PATTERN: all of these are compile errors in Go
// x = i++ // ✗ invalid — ++ is not an expression
// fmt.Println(i++) // ✗ invalid
// j := i++ // ✗ invalid
// ++i // ✗ invalid — Go doesn't have prefix ++
// There's only one form: postfix, as a standalone statement
for i := 0; i < 5; i++ { // ✓ in a for statement
fmt.Println(i)
}
Why? In C and Java, i++ as an expression is a classic source of confusion: x = i++ differs from x = ++i. Go eliminates this ambiguity entirely by making ++ usable only as a standalone statement.
Address and Dereference Operators #
Already covered in the Data Types article, but worth repeating in the operator context:
x := 42
p := &x // & = "address of" — takes x's memory address, produces a *int
*p = 100 // * = "dereference" — accesses the value at the address p holds
fmt.Println(x) // 100 — x changed through the pointer
fmt.Println(*p) // 100 — same as x's value
fmt.Println(p) // 0xc000... — the memory address
// * is also used in TYPE DECLARATIONS to state "pointer to T"
var q *int = &x // q is a pointer to int
The Channel Operator (<-)
#
The <- operator is specifically for communicating via channels — the heart of Go’s concurrency:
ch := make(chan int, 1) // buffered channel, capacity 1
// Send a value to the channel (send)
ch <- 42
// Receive a value from the channel (receive)
value := <-ch
fmt.Println(value) // 42
// Receive with a check whether the channel is still open
value, ok := <-ch
if !ok {
fmt.Println("channel is closed")
}
// Receive without storing the value (just for synchronization)
<-done // wait until the done channel receives a value
Operator Precedence #
In an expression with multiple operators, Go follows precedence rules — higher-precedence operators are evaluated first:
Precedence (from highest to lowest):
5: * / % << >> & &^
4: + - | ^
3: == != < <= > >=
2: &&
1: ||
// Precedence examples
fmt.Println(2 + 3*4) // 14, not 20 (* is higher than +)
fmt.Println(5 > 3 && 2 < 4) // true (&& is lower than >)
fmt.Println(true || false && false) // true (&& is higher than ||)
// true || (false && false)
// true || false
// true
// If unsure, always use parentheses
// Clearer and doesn't depend on memorizing precedence:
fmt.Println((2 + 3) * 4) // 20 — explicit
fmt.Println(true || (false && false)) // true — clear
Use parentheses when in doubt. Clear code beats “clever” code that relies on precedence order not everyone has memorized. The compiler doesn’t care — but human readers benefit enormously.
What Go Deliberately Doesn’t Have #
Go intentionally omits several operators that exist in other languages:
// 1. No ternary operator (?:)
// In Java/C: int max = a > b ? a : b;
// ANTI-PATTERN: you can't write this in Go
// max := a > b ? a : b // ← compile error
// CORRECT: use a plain if-else
max := a
if b > a {
max = b
}
// Or in a single expression with a function:
func ternary(cond bool, a, b int) int {
if cond {
return a
}
return b
}
// 2. No exponentiation operator (**)
// In Python: x = 2 ** 10
// CORRECT: use math.Pow
import "math"
x := math.Pow(2, 10) // 1024.0 (float64)
// For small integer powers, hardcoding or shifting is more efficient:
x2 := 1 << 10 // 1024 (int) — only for powers of 2
// 3. No prefix ++ and --
// In C: ++i and --i
// Go only has postfix, and only as a statement:
i++ // ✓
// ++i // ✗ compile error
// 4. No ~ operator (bitwise NOT)
// In C: ~flags
// CORRECT: use XOR with all bits 1
flags := 0b1010
notFlags := flags ^ -1 // XOR with -1 (all bits 1) = NOT
// Or for a specific size:
notFlags8 := ^uint8(flags) // ^ as a unary operator = bitwise NOT
Idiomatic Patterns with Operators #
Swap Without a Temporary Variable #
a, b := 10, 20
a, b = b, a // elegant swap — no temp variable needed
fmt.Println(a, b) // 20 10
Checking Bits with Masking #
// Bit-based permission system
type Perm uint8
const (
Read Perm = 1 << iota // 001
Write // 010
Execute // 100
)
func checkPerm(userPerm, needed Perm) bool {
return userPerm&needed == needed // all bits in needed must be in userPerm
}
userPerm := Read | Write // 011
fmt.Println(checkPerm(userPerm, Read)) // true
fmt.Println(checkPerm(userPerm, Execute)) // false
fmt.Println(checkPerm(userPerm, Read|Write)) // true — check a combination
fmt.Println(checkPerm(userPerm, Read|Execute)) // false — doesn't have Execute
Alignment and Padding with Bitwise #
// Round n up to the nearest power-of-2 multiple
func alignTo(n, alignment int) int {
return (n + alignment - 1) &^ (alignment - 1)
}
fmt.Println(alignTo(13, 8)) // 16 — round up to a multiple of 8
fmt.Println(alignTo(16, 8)) // 16 — already a multiple of 8
fmt.Println(alignTo(17, 8)) // 24
Complete Example Program #
The following program simulates a bit-flag-based access control system using various operators:
package main
import (
"fmt"
"strings"
)
type Permission uint8
const (
PermRead Permission = 1 << iota // 00000001
PermWrite // 00000010
PermDelete // 00000100
PermAdmin // 00001000
PermAudit // 00010000
)
var permNames = map[Permission]string{
PermRead: "Read",
PermWrite: "Write",
PermDelete: "Delete",
PermAdmin: "Admin",
PermAudit: "Audit",
}
func (p Permission) String() string {
if p == 0 {
return "None"
}
var parts []string
for perm, name := range permNames {
if p&perm != 0 {
parts = append(parts, name)
}
}
return strings.Join(parts, "|")
}
func (p Permission) Has(perm Permission) bool {
return p&perm == perm
}
func (p *Permission) Grant(perm Permission) {
*p |= perm
}
func (p *Permission) Revoke(perm Permission) {
*p &^= perm
}
func (p *Permission) Toggle(perm Permission) {
*p ^= perm
}
type User struct {
Name string
Perm Permission
}
func main() {
users := []User{
{Name: "Alice", Perm: PermRead | PermWrite},
{Name: "Bob", Perm: PermRead},
{Name: "Carlos", Perm: PermRead | PermWrite | PermDelete | PermAdmin},
}
fmt.Println("=== Initial Permission Status ===")
for _, u := range users {
fmt.Printf("%-8s → %s\n", u.Name, u.Perm)
}
fmt.Println("\n=== Permission Operations ===")
// The |= operator for granting
users[1].Perm.Grant(PermWrite)
fmt.Printf("Bob grant Write → %s\n", users[1].Perm)
// The &^= operator for revoking
users[2].Perm.Revoke(PermAdmin)
fmt.Printf("Carlos revoke Admin → %s\n", users[2].Perm)
// The ^= operator for toggling
users[0].Perm.Toggle(PermDelete)
fmt.Printf("Alice toggle Delete → %s\n", users[0].Perm)
users[0].Perm.Toggle(PermDelete) // toggle again = back to the original
fmt.Printf("Alice toggle Delete → %s\n", users[0].Perm)
fmt.Println("\n=== Access Checks ===")
actions := []struct {
name string
perm Permission
}{
{"Read file", PermRead},
{"Edit file", PermWrite},
{"Delete file", PermDelete},
{"Manage users", PermAdmin},
{"View audit", PermAudit},
}
for _, u := range users {
fmt.Printf("\n%s:\n", u.Name)
for _, action := range actions {
// The & and == operators for checking
allowed := u.Perm.Has(action.perm)
status := "✗"
if allowed {
status = "✓"
}
fmt.Printf(" %s %-15s\n", status, action.name)
}
}
// Demonstrate arithmetic and comparison operators
fmt.Println("\n=== Statistics ===")
totalUsers := len(users)
adminCount := 0
for _, u := range users {
if u.Perm.Has(PermAdmin) {
adminCount++
}
}
// Percentage calculation — remember: convert to float64 BEFORE dividing
adminPct := float64(adminCount) / float64(totalUsers) * 100
fmt.Printf("Total users: %d\n", totalUsers)
fmt.Printf("Admins: %d (%.1f%%)\n", adminCount, adminPct)
fmt.Printf("Non-admins: %d\n", totalUsers-adminCount)
}
Summary #
- Integer division (
/) always truncates toward zero — convert tofloat64before dividing if you need decimals.- Negative modulus (
%) follows the left operand’s sign —-7 % 3 = -1, not2.&&and||short-circuit — the right operand isn’t evaluated when the result is already determined; use this for nil guards and chained validation.&^(AND NOT) is Go’s unique operator for clearing specific bits without touching others.<<(left shift) equals multiplying by a power of 2;>>equals dividing by a power of 2.++and--are statements, not expressions — they can’t be used in assignments or as prefixes (++iis invalid).<-is the channel operator —ch <- valfor sending,val := <-chfor receiving.- There’s no ternary
? :in Go — use a more explicitif-else.- There’s no
**operator for powers — usemath.Pow()or bit shifts for powers of 2.- Use parentheses when an expression involves many operators — clearer than relying on memorized precedence.