运算符:Go语言的“计算器”与“裁判员”

运算符这东西,说白了就是让程序帮你做判断和计算的工具。Go语言的运算符分四类:算术、比较、逻辑、位运算。咱们一个一个聊。

算术运算符:加减乘除,还有取模

算术运算符跟数学课上学的差不多。加+、减-、乘*、除/、取模%。嗯,这里要注意,Go是强类型语言,不同类型不能直接运算。

package main

import "fmt"

func main() {
    a := 10
    b := 3

    fmt.Println(a + b) // 13
    fmt.Println(a - b) // 7
    fmt.Println(a * b) // 30
    fmt.Println(a / b) // 3,整数除法,结果还是整数
    fmt.Println(a % b) // 1,取余数
}
⚠️ 避坑指南
我曾经在写一个支付系统时,直接用整数除法算单价,结果金额全错了。记住:10 / 3 在Go里等于 3,不是 3.333。想要小数,得用浮点数。

比较运算符:谁大谁小,一目了然

比较运算符返回布尔值,truefalse。包括:==!=<><=>=

package main

import "fmt"

func main() {
    x := 5
    y := 8

    fmt.Println(x == y) // false
    fmt.Println(x != y) // true
    fmt.Println(x < y)  // true
    fmt.Println(x > y)  // false
}
💡 个人习惯
我写条件判断时,习惯把常量放左边,比如 0 == x 而不是 x == 0。这样万一少写一个等号变成赋值,编译器会报错。这招是从C语言时代带过来的老毛病,但确实管用。

逻辑运算符:与或非,组合判断

逻辑运算符有三个:&&(与)、||(或)、!(非)。它们有短路特性——能提前确定结果就不往后算了。

package main

import "fmt"

func main() {
    age := 25
    hasID := true

    // 与:两个都满足才行
    if age >= 18 && hasID {
        fmt.Println("可以进入")
    }

    // 或:满足一个就行
    if age < 12 || hasID == false {
        fmt.Println("需要家长陪同")
    }

    // 非:取反
    if !hasID {
        fmt.Println("没带证件")
    }
}
🔑 关键点
短路求值:a && b 如果 afalseb 根本不会执行。我在项目中遇到过因为依赖短路特性来避免空指针,结果重构时顺序搞反了,直接panic。所以依赖短路要谨慎。

位运算符:底层操作的利器

位运算直接操作二进制位。包括:&(按位与)、|(按位或)、^(按位异或)、<<(左移)、>>(右移)。

package main

import "fmt"

func main() {
    a := 5  // 二进制 0101
    b := 3  // 二进制 0011

    fmt.Println(a & b)  // 1  (0001)
    fmt.Println(a | b)  // 7  (0111)
    fmt.Println(a ^ b)  // 6  (0110)
    fmt.Println(a << 1) // 10 (1010),左移一位相当于乘以2
    fmt.Println(a >> 1) // 2  (0010),右移一位相当于除以2
}
💡 实战场景
位运算在权限系统里特别好用。比如用 1 << 0 表示读权限,1 << 1 表示写权限,1 << 2 表示执行权限。组合起来就是一个整数,检查权限时用 & 运算就行。我在做容器权限管理时就这么干的,性能好,代码也干净。

流程控制:让代码学会“思考”

if 语句:最简单的分支

Go的 if 不需要括号,但必须有大括号。而且可以在条件前加一个简单语句。

package main

import "fmt"

func main() {
    score := 85

    // 基本用法
    if score >= 90 {
        fmt.Println("优秀")
    } else if score >= 60 {
        fmt.Println("及格")
    } else {
        fmt.Println("不及格")
    }

    // 带初始化语句的if
    if age := 20; age >= 18 {
        fmt.Println("成年人")
    }
    // 这里 age 已经不可见了,作用域只在if块内
}
⚠️ 避坑指南
我曾经见过有人把 if err != nil 写成 if err = nil,少了一个感叹号,结果赋值成功了,条件永远为假。Go编译器不会报错,但逻辑全错了。所以写条件判断时,多看一眼。

switch 语句:多路分支的优雅写法

Go的 switch 不需要 break,默认只执行一个分支。而且可以匹配表达式、类型,甚至空着当 if-else 用。

package main

import "fmt"

func main() {
    day := "周一"

    // 基本switch
    switch day {
    case "周一", "周二", "周三", "周四", "周五":
        fmt.Println("工作日")
    case "周六", "周日":
        fmt.Println("周末")
    default:
        fmt.Println("无效日期")
    }

    // 无表达式的switch,相当于if-else链
    score := 75
    switch {
    case score >= 90:
        fmt.Println("A")
    case score >= 80:
        fmt.Println("B")
    case score >= 70:
        fmt.Println("C")
    default:
        fmt.Println("D")
    }
}
🔑 关键点
Go的 switch 默认不会穿透。如果你想让一个分支继续执行下一个,得用 fallthrough。但说实话,我很少用 fallthrough,因为它容易让逻辑变得混乱。你想想看,本来每个分支是独立的,突然来个穿透,读代码的人得停下来想半天。

for 循环:Go里唯一的循环

Go只有 for 循环,没有 whiledo-while。但 for 有三种写法,能覆盖所有场景。

package main

import "fmt"

func main() {
    // 标准for循环
    for i := 0; i < 5; i++ {
        fmt.Println(i)
    }

    // 相当于while的写法
    sum := 0
    for sum < 10 {
        sum += 2
    }
    fmt.Println(sum) // 10

    // 无限循环
    count := 0
    for {
        count++
        if count > 3 {
            break
        }
    }

    // range遍历
    nums := []int{10, 20, 30}
    for index, value := range nums {
        fmt.Printf("索引%d的值是%d\n", index, value)
    }
}
💡 个人经验
range 遍历时,如果不需要索引,用 _ 占位。比如 for _, v := range slice。我见过有人写 for i, v := range slice 然后 i 从来没用到,编译器会报错。Go对未使用的变量零容忍,这是好事。

defer 语句:延迟执行,资源清理的利器

defer 是Go里一个很特别的设计。它会把函数调用压入一个栈,等外层函数返回前再依次执行。说白了就是「走之前把事办了」。

package main

import "fmt"

func main() {
    defer fmt.Println("世界")
    fmt.Println("你好")

    // 输出:
    // 你好
    // 世界
}
🔑 关键点
defer 的执行顺序是后进先出(LIFO)。如果你写了多个 defer,最后一个会最先执行。这跟栈的行为一样。

defer 的典型用法:资源管理

最常见的场景是关闭文件、解锁互斥锁、关闭数据库连接。把 defer 紧跟在打开资源后面,代码清晰,也不容易忘记释放。

package main

import (
    "fmt"
    "os"
)

func readFile(filename string) {
    file, err := os.Open(filename)
    if err != nil {
        fmt.Println("打开文件失败:", err)
        return
    }
    defer file.Close() // 函数返回前自动关闭

    // 读取文件内容...
    fmt.Println("文件已打开")
}

func main() {
    readFile("test.txt")
}
⚠️ 避坑指南
我曾经在循环里用 defer 关闭文件,结果循环了1000次,所有 defer 都等到函数返回才执行。文件描述符全被占着,系统直接报 too many open files。记住:defer 是在函数返回时执行,不是在循环结束时。循环里别用 defer,除非你清楚后果。

defer 与参数求值

defer 的参数在声明时就会求值,而不是在真正执行时。这个细节很多人会搞错。

package main

import "fmt"

func main() {
    x := 10
    defer fmt.Println(x) // 这里x的值是10,已经确定了

    x = 20
    fmt.Println(x) // 输出20
    // defer输出10,不是20
}
💡 实战技巧
如果你想让 defer 使用变量最终的值,可以用闭包。比如 defer func() { fmt.Println(x) }()。这样 x 会在 defer 真正执行时才取值。我在写日志记录时经常用这个技巧,记录函数执行后的状态。

defer 与返回值

defer 可以修改命名返回值。这个特性在错误处理时特别有用。

package main

import "fmt"

func divide(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("发生了panic: %v", r)
        }
    }()

    result = a / b
    return
}

func main() {
    r, err := divide(10, 0)
    if err != nil {
        fmt.Println("错误:", err)
    } else {
        fmt.Println("结果:", r)
    }
}
🔑 关键点
defer 可以读取和修改命名返回值。这在做统一错误处理、记录执行时间、捕获 panic 时非常有用。我个人习惯在函数开头写一个 defer 来做收尾工作,这样函数主体可以专注于业务逻辑,不用操心清理的事。

好了,运算符和流程控制就聊到这儿。这些是Go编程的基石,写多了自然就熟练了。下一章咱们聊聊函数和包管理,那才是真正开始组织代码的地方。