04、流程控制

1.条件语句

if

if age >= 18 {
    fmt.Println("成年")
}

if-else

if age >= 18 {
    fmt.Println("成年")
} else { 
    fmt.Println("未成年")
}

else if多分支

score := 85
if score >= 90 {
    fmt.Println("A")
} else if score >= 80 {
    fmt.Println("B")
} else if score >= 60 {
    fmt.Println("C")
} else {
    fmt.Println("D")
}

Go 标准错误处理(高频)

file, err := os.Open("test.txt")
if err != nil { // 常规写法
    log.Fatal(err)
}

// 简化:一行声明+判断
if file, err := os.Open("test.txt"); err != nil {
    log.Fatal("打开失败:", err)
}
defer file.Close()

特殊限制

不支持数字 / 字符串直接当布尔

// 全部报错!Go 严格要求 bool
if 1 {}
if "abc" {}
if 0 {}

不能省略大括号

// 错误,哪怕一行代码也要 {}
if a > 10
    fmt.Println("xxx")

条件不能写赋值 =,只能判等 ==

if a = 10 {} // 编译报错,禁止在if里赋值

实战完整示例

package main

import "fmt"

func main() {
    score := 78
    // 1. 基础多分支
    if score >= 90 {
        fmt.Println("优秀")
    } else if score >= 80 {
        fmt.Println("良好")
    } else if score >= 60 {
        fmt.Println("及格")
    } else {
        fmt.Println("不及格")
    }  // 及格

    // 2. 短变量声明if
    if num := 15; num%2 == 0 {
        fmt.Println(num, "偶数")
    } else {
        fmt.Println(num, "奇数")
    }  // 15 奇数

    // 3. 逻辑运算
    age := 22
    vip := true
    if age >= 18 && vip {
        fmt.Println("VIP成人通道")
    }  // VIP成人通道
}

2.选择语句

switch

package main
import "fmt"

func main() {
    num := 2
    switch num {
    case 1:
        fmt.Println("数字1")
    case 2:
        fmt.Println("数字2")
    default:
        fmt.Println("其他数字")
    }
}
// 输出:数字2

case 多值

month := 3
switch month {
case 1, 3, 5, 7, 8, 10, 12:
    fmt.Println("大月31天")
case 4, 6, 9, 11:
    fmt.Println("小月30天")
case 2:
    fmt.Println("28/29天")
default:
    fmt.Println("非法月份")
}

fallthrough 强制穿透

\\若需要执行完当前 case 后继续执行下一个 case,使用 fallthrough(只能写在 case 末尾)
a := 1
switch a {
case 1:
    fmt.Println("匹配1")
    fallthrough // 强制进入下一个case,不判断条件
case 2:
    fmt.Println("匹配2")
case 3:
    fmt.Println("匹配3")
}
// 输出:
// 匹配1
// 匹配2

注意:

  1. Go 的 switch 默认每一个 case 自带 break,不会自动穿透;想要继续执行下一个 case 代码,手动写 fallthrough
  2. fallthrough 不会校验下一个 case 条件,直接执行;
  3. 必须放在 case 块末尾,但不能放在最后一个 case、default 后面;
  4. Go 不支持跨多层穿透,只能穿透紧邻下一条。

例:权限分级(游客 < 用户 < 管理员)

auth := "user"
switch auth {
case "guest":
    fmt.Println("基础浏览权限")
    fallthrough // 游客也要拥有用户通用权限逻辑
case "user":
    fmt.Println("发帖、评论权限")
    fallthrough // 用户顺带拥有管理员基础页面
case "admin":
    fmt.Println("后台管理、数据修改权限")
}

无表达式 switch

switch 后不写表达式,等价于 switch true,每个 case 写布尔判断,替代替代 if-else if :

score := 85
switch {
case score >= 90:
    fmt.Println("优秀")
case score >= 80:
    fmt.Println("良好")
case score >= 60:
    fmt.Println("及格")
default:
    fmt.Println("不及格")
}
// 输出:良好

优势:代码更简洁,多层 if-else 首选。

switch 带初始化语句

switch 开头可定义临时变量,作用域仅 switch 内部:

switch age := 20; {
case age >= 18:
    fmt.Println("成年")
default:
    fmt.Println("未成年")
}

类型 switch

Go 独有,专门判断 interface{} 变量的底层类型,语法:变量.(type)仅能在 switch 中使用

package main
import "fmt"

func checkType(v interface{}) {
    switch val := v.(type) { // val接收对应类型的值
    case int:
        fmt.Printf("int类型:%d\n", val)
    case string:
        fmt.Printf("string类型:%s\n", val)
    case bool:
        fmt.Printf("bool类型:%t\n", val)
    default:
        fmt.Printf("未知类型:%T\n", val)
    }
}

func main() {
    checkType(100)
    checkType("hello")
    checkType(true)
}

select 语句

用于多个channel 的并发读写操作

作用:同时等待多个 channel,哪个 channel 先就绪就执行哪个分支

基础语法:

select {
case ch1 <- val:        // 发送操作:ch1可写入则执行
case res := <-ch2:     // 接收操作:ch2有数据可读则执行
case <-ch3:            // 只接收、丢弃数据
default:               // 无channel就绪时立刻执行,非阻塞
}

规则:

  1. 所有 case 必须是channel 收发表达式
  2. 同一时刻多个 channel 同时就绪:随机选一个执行
  3. 没有 default 时:select 阻塞,直到任意一个 case channel 就绪;
  4. default:永不阻塞,无就绪 channel 直接走 default。
四大基础场景示例

阻塞等待多个 channel(无 default)

goroutine 发数据,select 等待任意一个通道返回:

package main
import "fmt"
import "time"

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
        time.Sleep(1 * time.Second)
        ch1 <- "来自通道1"
    }()
    go func() {
        time.Sleep(2 * time.Second)
        ch2 <- "来自通道2"
    }()

    // 阻塞,等最先就绪的通道
    select {
    case msg := <-ch1:
        fmt.Println(msg)
    case msg := <-ch2:
        fmt.Println(msg)
    }
}
// 1秒后输出:来自通道1

非阻塞 select(带 default)

没有通道可读写,立刻执行 default,不会卡住 goroutine:

ch := make(chan int, 1)
select {
case val := <-ch:
    fmt.Println("读到数据", val)
default:
    fmt.Println("通道无数据,非阻塞退出")
}

select 发送分支 case

监听通道是否可写入(缓冲 chan 满了就阻塞,无缓冲 chan 需对面 goroutine 接收):

ch := make(chan int, 1)
select {
case ch <- 100:
    fmt.Println("写入成功")
default:
    fmt.Println("通道已满,无法写入")
}

超时控制(最常用业务场景)

搭配 time.After() 实现操作限时,避免永久阻塞:

ch := make(chan string)
select {
case res := <-ch:
    fmt.Println("收到:", res)
case <-time.After(3 * time.Second):
    fmt.Println("3秒超时,放弃等待")
}

select 关键特性详解

多通道同时就绪:随机选择

若多个 channel 同时有数据,Go 运行时随机公平挑选一个 case,不会从上到下优先匹配:

ch1 := make(chan int, 1)
ch2 := make(chan int, 1)
ch1 <- 1
ch2 <- 2

// 每次运行随机输出1或2
select {
case <-ch1:
    fmt.Println("ch1")
case <-ch2:
    fmt.Println("ch2")
}

nil channel 永久阻塞

nil chan 的收发操作永远阻塞,可用来临时屏蔽某个分支:

var ch chan int // nil
select {
case <-ch: // 永远不会命中
    fmt.Println("ok")
case <-time.After(time.Second):
    fmt.Println("超时")
}

循环 + select 持续监听(消息循环标准写法)

后台协程持续处理多通道消息,服务、消费者通用模板:

func worker(ch1, ch2 <-chan int, stop <-chan struct{}) {
    for {
        select {
        case v := <-ch1:
            fmt.Println("处理ch1数据", v)
        case v := <-ch2:
            fmt.Println("处理ch2数据", v)
        case <-stop:
            fmt.Println("收到停止信号,退出循环")
            return
        }
    }
}

关闭 channel 不会阻塞,会立刻返回零值

通道 close 后,<-ch 会立即执行分支,不会阻塞:

ch := make(chan int)
close(ch)
select {
case v := <-ch:
    fmt.Println("读到关闭通道零值:", v) // v=0
}

经典业务高频使用场景

接口 / 请求超时

RPC、HTTP 调用、数据库操作,用 time.After 限制最大等待时间,防止 goroutine 泄漏。

多数据源监听

同时监听消息队列、定时任务、停止信号、心跳通道。

优雅退出控制

单独一个 stop chan,收到信号立刻终止循环,安全释放资源。

限流非阻塞收发

带 default 的 select 实现:通道满了不阻塞,直接丢弃 / 返回繁忙。

定时心跳保活

搭配 time.Tick 定时发送心跳包。

select vs switch 核心区别

特性switchselect
分支条件任意表达式、常量、布尔只能是 channel 收发
匹配规则从上到下顺序匹配就绪通道随机选
阻塞行为永不阻塞可以阻塞(无 default)
独有能力类型断言、多值匹配多路 IO 监听、超时控制

示例:

package main

import (
	"fmt"
)

func main(){
    a := make(chan int, 1024)
    b := make(chan int, 1024)
    
    for i := 0; i < 10; i++ {
        fmt.Printf("第%d 次,", i)
        
        a <- 1
        b <- 1
        
        select {
            case <-a:
            		fmt.Println("from a")
            case <-b:
            		fmt.Println("from b")
        }
    }
}
/ *
返回(每次返回结果不同):
第0 次,from a
第1 次,from b
第2 次,from a
第3 次,from a
第4 次,from b
第5 次,from b
第6 次,from b
第7 次,from a
第8 次,from a
第9 次,from a
* /

上述代码的意思是,同时在a 和b 中选择,哪个有内容就从哪个读,由于channel 的读写操作是阻塞操作,使用select 语句可以避免单个channel 的阻塞。此外select 同样可以使用default 代码块,用于避免所有channel 同时阻塞。具体内容可见并发编程。

3.循环语句

for

格式:

for 初始化; 条件; 自增 {
    循环体
}

示例

package main

import "fmt"

func main() {
    // i:=0 初始化; i<10 条件; i++ 自增
    for i := 0; i < 10; i++ {
        fmt.Println(i)
    }
}

组件省略规则

三个部分均可省略分号,但分号不能乱删:

1.省略初始化:i 提前定义

i := 0
for ; i < 5; i++ {
    fmt.Println(i)
}

2.省略自增:循环体内手动更新

for i := 0; i < 5; {
    fmt.Println(i)
    i++
}

3.只保留条件(等价 while)

i := 0
for i < 5 {
    fmt.Println(i)
    i++
}

三部分全部省略,等价 while(true),一般搭配 break 跳出:

i := 0
for {
    if i >= 3 {
        break // 跳出当前for循环
    }
    fmt.Println(i)
    i++
}

for range

专门遍历:字符串、数组、切片、map、通道

语法:

for 索引/,:= range 容器 {
}

1.遍历切片 / 数组

s := []int{10,20,30}
for idx, val := range s {
    fmt.Printf("下标:%d, 值:%d\n", idx, val)
}

2.遍历字符串

str := "golang语言"
for i, r := range str {
    fmt.Printf("索引%d, 字符%c\n", i, r)
}

3.遍历 map

m := map[string]int{"a":1, "b":2}
for k, v := range m {
    fmt.Println(k, v)
}

4.遍历通道 channel

通道关闭前持续读取,关闭自动退出循环

ch := make(chan int, 2)
ch <- 1
ch <- 2
close(ch)

for v := range ch {
    fmt.Println(v)
}

continue

终止本次循环,直接进入下一轮条件判断

// 只打印偶数
for i := 0; i < 10; i++ {
    if i%2 != 0 {
        continue
    }
    fmt.Println(i)  // 0 2 4 6 8
}

break

直接终止当前一层for 循环

for i := 0; i < 10; i++ {
    if i == 5 {
        break
    }
    fmt.Println(i) //0 1 2 3 4
}

Label

多层循环精准跳出,break/continue 默认只作用内层,用标签指定外层循环。

语法

标签名:
for {
    for {
        break 标签名 // 直接跳出外层循环
    }
}

示例:双层循环一键跳出外层

outer: // 定义外层标签
for i := 0; i < 3; i++ {
    for j := 0; j < 3; j++ {
        if i == 1 && j == 1 {
            break outer // 直接终止外层循环
        }
        fmt.Printf("i=%d,j=%d\n",i,j)
    }
}

示例:continue + 标签:跳到外层下一轮

outer:
for i := 0; i < 3; i++ {
    for j := 0; j < 3; j++ {
        if j == 1 {
            continue outer
        }
        fmt.Println(i,j)
    }
}

goto

日常业务 99% 场景不需要 goto

//循环重试
RETRY:
	err := connectDB()
	if err != nil {
		time.Sleep(time.Second)
		goto RETRY
	}

循环常见实操案例:

1.倒序循环

for i := 9; i >= 0; i-- {
    fmt.Println(i)
}

2.求和 1~100

sum := 0
for i := 1; i <= 100; i++ {
    sum += i
}
fmt.Println(sum)

3.过滤 range(忽略索引)

arr := []string{"A","B","C"}
for _, v := range arr {
    fmt.Println(v)
}

4、延迟语句

defer

func main() {
    fmt.Println("1")
    defer fmt.Println("defer执行")
    fmt.Println("2")
}
// 输出:
// 1
// 2
// defer执行

规则 1:defer 注册顺序「后进先出」栈结构

func main() {
    defer fmt.Println("A")
    defer fmt.Println("B")
    defer fmt.Println("C")
}
// 输出:C B A

规则 2:defer 的参数立即求值,函数体延迟执行

func main() {
    x := 10
    defer fmt.Println(x) // x此时=10,值已复制
    x = 20
}
// 输出:10,不是20

想要延迟取变量最新值两种方案

传指针

x := 10
defer func(v *int) { fmt.Println(*v) }(&x)
x = 20
// 输出20

闭包无参(内部直接捕获变量)

x := 10
defer func() { fmt.Println(x) }()
x = 20
// 输出20

三种常见使用场景

1.资源释放(最常用):文件、网络连接

打开资源后立刻 defer 关闭,避免遗忘释放

func readFile() {
    f, err := os.Open("test.txt")
    if err != nil {
        return
    }
    defer f.Close() // 函数退出自动关文件

    // 读写文件逻辑
}

2.锁释放

var mu sync.Mutex

func do() {
    mu.Lock()
    defer mu.Unlock() // 无论中间是否return/panic,都会解锁

    // 临界区操作
}

3.捕获 panic、异常恢复 recover

recover() 只能在 defer 内部生效,捕获 panic 避免程序崩溃

func test() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println("捕获panic:", err)
        }
    }()

    panic("程序崩溃了")
}
/*
panic()主动触发程序异常,终止当前正常流程,逐层向上执行defer,若无 recover 捕获,程序直接退出打印堆栈。
panic() 主动抛出崩溃,触发所有 defer 执行;
recover() 仅在 defer 闭包内生效,捕获当前 goroutine 异常;
*/

注意:

循环内慎用 defer,尽量内层包装函数,每次循环立刻执行释放

defer 仅作用于当前函数,跨函数无效

开销:少量 defer 无影响,极高并发循环不建议滥用