今天简单的学习下流程控制与函数,这一块大多数语言都差不多。

if/else

Go语言的if不需要小括号,但代码块必须使用{}包裹。

1
2
3
4
5
6
7
8
func TestIf(t *testing.T) {
age := 30
if age >= 18 {
t.Log("adult")
} else {
t.Log("teenager")
}
}

其次,if条件必须是bool,不支持将整数、字符串、指针等作为条件。

Go 不像 C / C++ / JavaScript,不能用 01nil 等隐式转 bool。

1
2
3
4
5
6
7
func TestIf1(t *testing.T) {
n := 1
//非布尔值 'n' (类型 int) 用作条件
if n {
t.Log("error")
}
}

if支持初始化语句

这是 Go 很常见的写法,尤其用于错误处理、map 查询、类型转换等场景。

1
2
3
if err := doSomething(); err != nil {
t.Log(err)
}

例如:

1
2
3
4
5
6
7
8
9
10
func readConfig() error {
if err := loadFile(); err != nil {
return err
}
return nil
}

func loadFile() error {
return error(nil)
}

初始化语句中的变量,只有在if/else if/ else结构中可见。

1
2
3
4
5
6
7
func TestIfScope(t *testing.T) {
if x := 15; x > 5 {
t.Log("x is greater than 5")
} else {
t.Log(x)
}
}

常见用法:错误处理map查询类型断言,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func TestIfMap(t *testing.T) {
m := map[string]int{
"index": 1,
}
if value, ok := m["index"]; ok {
t.Log(value)
} else {
t.Log("not found")
}
}

//或者错误处理
if err:=saveUser(user); err !=nil {
return err
}

除了必须有{},还有一个必须那就是else必须紧跟着if的右花括号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func TestIfElse(t *testing.T) {
n := 20
if n >=18 {
t.Log("adult")
}
// 这里会报错,Go对换行很敏感,因为编译器会自动插入分号。
else {
t.Log("teenager")
}
}

// 需要紧跟着if的右边花括号。
func TestIfElse(t *testing.T) {
n := 20
if n >=18 {
t.Log("adult")
} else {
t.Log("teenager")
}
}

else if

没啥好说的,好java一样,其他的遵循go的规则,主要是没有小括号,必须花括号,并且紧跟着上一个的右边花括号。

1
2
3
4
5
6
7
8
9
10
func TestElseIf(t *testing.T) {
s := 80
if s >= 90 {
t.Log("A")
} else if s >= 80 {
t.Log("B")
} else if s >= 60 {
t.Log("C")
}
}

编码时,可以用if实现Guard Clause(守卫语句),在方法的开头先检查不满足继续执行条件的情况,遇到异常、非法参数、边界条件就提前返回、抛异常、跳过。避免后面的主逻辑被多层if包裹起来。下面的例子做个对比。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// guard clause写法
func Process() error {
data, err := LoadData()
if err != nil {
return err
}
if len(data) == 0 {
return fmt.Errorf("empty data")
}
return SaveData(data)
}

// 嵌套写法
func Process() error {
data, err := LoadData()
if err == nil {
if len(data) != 0 {
return SaveData(data)
} else {
return fmt.Errorf("empty data")
}
} else {
return err
}
}

switch

对于switch,和Java区别也不大。只是不需要写break,因为默认不会向下执行(fallthrough)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func TestSwitch(t *testing.T) {
day := "Monday"
switch day {
case "Monday":
t.Log("Monday")
case "Tuesday":
t.Log("Tuesday")
case "Wednesday":
t.Log("Wednesday")
case "Thursday":
t.Log("Thursday")
case "Friday":
t.Log("Friday")
}
}

当然,case里支持多个条件:

1
2
3
4
5
6
7
8
9
switch day {
case "Monday":
t.Log("work day")
case "Saturday", "Sunday":
t.Log("weekend")
default:
t.Log("normal")

}

如果想继续执行下一个case,可以使用fallthrough

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func TestFallThrough(t *testing.T) {
n := 1
switch n {
case 1:
t.Log("1")
fallthrough
case 2:
t.Log("2")
fallthrough
default:
t.Log("default")

}
}

fallthrough 只会进入下一个 case,不会重新判断下一个 case 的条件。

switchif一样,都支持初始化语句

1
2
3
4
5
6
7
8
9
10
11
12
func TestSwitchInit(t *testing.T) {
switch os := runtime.GOOS; os {
case "darwin":
t.Log("macOS")
case "linux":
t.Log("Linux")
case "windows":
t.Log("Windows")
default:
t.Log("Other OS")
}
}

if一样的,这里的os也只有在switch内部可见。对于上面if的判断TestElseIf(),也可以修改为switch

1
2
3
4
5
6
7
8
9
10
11
func TestSwitch1(t *testing.T) {
s := 80
switch {
case s >= 90:
t.Log("A")
case s >= 80:
t.Log("B")
case s >= 60:
t.Log("C")
}
}

这里可以看到,switch后面没有更任何表达式,这就相当于switch true,通常用来替代复杂的if/else if语句。

switch的case表达式,还可以是任意可比较的表达式

1
2
3
4
5
6
7
8
9
func TestSwitch2(t *testing.T) {
x := 10
switch x {
case 1 + 1:
t.Log("2")
case 5 * 2:
t.Log(10)
}
}

switch的case是从上到下匹配,只要有一个条件匹配成功,后面的就不会再执行。

1
2
3
4
5
6
7
8
9
10
11
func TestSwitch3(t *testing.T) {
s := 80
switch {
case s >= 60:
t.Log("C")
case s >= 80:
t.Log("B")
case s >= 90:
t.Log("A")
}
}

此时,只会输出c,,因为第一个已经匹配成功,后面的就不会执行了。编写代码时,一定要注意case的条件,要把更具体的条件放在最前面。

type switch 类型选择

这是比较重要的一点,主要用于判断接口变量的动态类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func PrintType(v any) {
switch value := v.(type) {
case int:
fmt.Println("int: ", value)
case float64:
fmt.Println("float64: ", value)
case string:
fmt.Println("string: ", value)
case bool:
fmt.Println("bool: ", value)
default:
fmt.Println("unknown")
}
}

func TestSwitchType(t *testing.T) {
PrintType(123)
PrintType("Hello")
PrintType(3.14)
PrintType(true)
}

v.(type) 只能用于 switch 中,不能单独使用。

在每个 case 中,变量 x 的静态类型会变成对应 case 的类型。

for 循环

对于Go语言来说,循环只有一个for。不过for有多种写法:

  1. 类C语言的三段式for
  2. 类似while的for
  3. 无限循环
  4. for range

下面通过几个小例子展示一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 三段式	
for i := 0; i <= 10; i++ {
t.Log(i)
}

// 类似while的for,相当于 while n > 0
n := 5
for n > 0 {
t.Log(n)
n--
}
// 无限循环 while true
// 通常用于服务监听、消费消息队列、重试逻辑、goroutine后台任务
for {
t.Log("running")
}
/**
for {
msg := receive()
if msg == "quit" {
break
}
handle(msg)
}
*/

// for range,前面学习slice、map等经常使用
nums := []int{2, 3, 4}
for index, num := range nums {
t.Log(index, num)
}

只要有循环,那就会有breakcontinue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func TestForBreak(t *testing.T) {
for i := 0; i < 10; i++ {
if i > 5 {
break
}
t.Log(i)
}
}


func TestForContinue(t *testing.T) {
for i := 0; i < 10; i++ {
if i%2 == 0 {
continue
}
t.Log(i)
}
}

遍历string

1
2
3
4
5
func TestForString(t *testing.T) {
for i, c := range "Go语言" {
t.Log(i, c, string(c))
}
}

输出如下:

1
2
3
4
for_test.go:57: 0 71 G
for_test.go:57: 1 111 o
for_test.go:57: 2 35821
for_test.go:57: 5 35328

range string 遍历的是 rune,不是 byte。

如果想按字节遍历

1
2
3
4
5
s := "Go语言"

for i := 0; i < len(s); i++ {
fmt.Println(i, s[i])
}

可参考day2的字符串

遍历数组时,range 会复制数组。

1
2
3
4
5
6
7
8
9
10
11
func TestForArray(t *testing.T) {
arr := [3]int{1, 2, 3}
// 复制数组,这里的v是元素得副本,修改v不会修原来数组的值
for _, v := range arr {
t.Log(v)
}
//如果数组很大,可以遍历数组指针避免复制
for _, v := range &arr {
t.Log(v)
}
}

数组得使用率远不如切片,不过这一问题对于切片,和数组基本上通用。

range获取地址

对于Go1.26来说,使用range获取地址不会再像1.22之前那样,出现多个地址可能指向同一个变量得问题,例如在Go 1.22之前,如下代码会出问题:

1
2
3
4
5
6
7
8
func TestForPointer1(t *testing.T) {
nums := []int{1, 2, 3}
var ptrs []*int
for _, v := range nums {
ptrs = append(ptrs, &v)
}
t.Log(ptrs)
}

在Go 1.22开始,for range循环变量每次迭代都会创建新的变量,闭包捕获问题被改善,上述代码就不会有问题,但实际开发中,仍然推荐如下写法:

1
2
3
4
5
6
7
8
9
10
11
12
/*
*
更推荐使用索引方式获取元素地址,避免循环变量v被复用导致所有指针指向同一地址的问题。
*/
func TestForPointer2(t *testing.T) {
nums := []int{1, 2, 3}
var ptrs []*int
for i := range nums {
ptrs = append(ptrs, &nums[i])
}
t.Log(ptrs)
}

labeled break/continue

和Java一样,Go语言也支持label跳出多层循环

1
2
3
4
5
6
7
8
9
10
11
func TestForLabeledBreak(t *testing.T) {
outer:
for i := 0; i < 5; i++ {
for j := 0; j < 5; j++ {
if i == 1 && j == 1 {
break outer
}
t.Log(i, j)
}
}
}

break outer 会跳出标记为 outer 的循环。如果是continue

1
2
3
4
5
6
7
8
9
10
11
func TestForLabeledContinue(t *testing.T) {
outer:
for i := 0; i < 5; i++ {
for j := 0; j < 5; j++ {
if i == 1 && j == 1 {
continue outer
}
t.Log(i, j)
}
}
}

continue outer 会直接进入外层循环的下一轮。

函数

函数语法

Go语言中的函数,基本上也就是Java中的方法。其基本定义方式如下:

1
2
3
4
5
func add(a int, b int) int {
return a + b
}

// func + 函数名(参数1 类型,参数2,类型 ...) 返回值类型 {函数体}

对于如果连续多个参数类型相同,可以进行参数类型简写:

1
2
3
func add(a,b int) int {
return a + b
}

多返回值

Java不同的是,Go语言中的函数是支持多个返回值的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 返回结果+异常
func div(a, b int) (int, error) {
if b == 0 {
return 0, fmt.Errorf("分母不能为0")
}
return a / b, nil
}

func TestMultiReturn(t *testing.T) {
result, err := div(10, 0)
if err != nil {
t.Error(err)
}
t.Log(result)
}

注意,这里并不是异常处理机制,而是Go语言 使用多返回值来处理错误。

当然, 返回值也是可以忽略的,前面学习其他知识点的时候都有提及,方法就是使用_忽略不需要的返回值。

1
2
value, _ := strconv.Atoi("123")
fmt.Println(value)

一般不建议在实际开发中随便忽略错误。更推荐如下:

1
2
3
4
5
value,err := strconv.Atoi("123")
if err != nil {
return err
}
fmt.Println(vlaue)

命名返回值

1
2
3
4
5
6
7
8
9
10
func GetSize() (width, height int) {
width = 100
height = 200
return
}

func TestNakedReturn(t *testing.T) {
x, y := GetSize()
t.Log(x, y)
}

这里 return 没有显式返回值,称为 naked return。

命名返回值适合短函数。复杂函数中不建议大量使用 naked return,可读性差。

如果是复杂函数,则更推荐如下写法:

1
2
3
4
5
6
7
8
9
10
func split(sum int) (int, int) {
x := sum / 2
y := sum - x
return x, y
}

func TestNakedReturn1(t *testing.T) {
a, b := split(200)
t.Log(a, b)
}

函数参数是值传递

Go 中函数参数默认都是值传递。

1
2
3
4
5
6
7
8
9
func change(num int) {
num = num * 2
}

func TestMethodArgs(t *testing.T) {
num := 10
change(num)
t.Log(num)
}

回忆在昨天指针章节学习中,如果要修改原始值,需要传递指针

1
2
3
4
5
6
7
8
9
func changePointer(num *int) {
*num = *num * 2
}

func TestMethodArgs2(t *testing.T) {
num := 10
changePointer(&num)
t.Log(num)
}

Go 只有值传递。即使传的是 slice、map、channel,本质上也是拷贝它们的描述符或引用头部。

对于slicemap,可参考前一天的介绍,尤其注意append,它返回的是新的slice,所以需要接受返回值。

可变参数

和Java一样,Go也支持可变参数,其代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func sum(nums ...int) int {
// nums ...int 在函数内部是 []int。
fmt.Printf("nums Type: %T", nums)
total := 0
for _, v := range nums {
total += v
}
return total
}

func TestMethodArray(t *testing.T) {
total := sum(10, 20, 32, 423, 12, 342, 54, 3)
t.Logf("Total : %v", total)

// 对于slice,在调用的时候后面加...
s := []int{1, 2, 3, 4, 5}
t.Logf("Slice: %v", sum(s...))
}

函数是一等公民

在Go语言中,函数是一等公民,可以赋值给变量

1
2
3
4
5
6
func TestMethod1(t *testing.T) {
add := func(a, b int) int {
return a + b
}
t.Log(add(1, 2))
}

也可以作为参数

1
2
3
4
5
6
7
8
9
func calculate(a, b int, op func(int, int) int) int {
return op(a, b)
}

func TestMethod2(t *testing.T) {
t.Log(calculate(10, 20, func(a, b int) int {
return a * b
}))
}

还可以作为返回值

1
2
3
4
5
6
7
8
9
10
func multiplier(factor int) func(int) int {
return func(n int) int {
return n * factor
}
}

func TestMethod3(t *testing.T) {
double := multiplier(2)
t.Log(double(10))
}

闭包

闭包可以捕获外部变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func counter() func() int {
count := 0
return func() int {
count++
return count
}
}

func TestMethod4(t *testing.T) {
next := counter()
t.Log(next())
t.Log(next())
t.Log(next())
}

闭包捕获的是变量本身,不只是值。

defer

defer 用于延迟执行函数,常用于释放资源。

1
2
3
4
5
6
7
8
func readFile() error {
file, err := os.Open("./test.txt")
if err != nil {
return err
}
defer file.Close()
return nil
}

defer执行顺序:多个defer按照后进先出执行,也就是栈结构。

1
2
3
4
5
func TestMethod5(t *testing.T) {
defer fmt.Println("first")
defer fmt.Println("second")
defer fmt.Println("third")
}

defer参数立即求值

defer 后面的函数如果需要传参,defer 声明的那一刻,参数的值就已经定格了

1
2
3
4
5
6
func TestMethod6(t *testing.T) {
x := 1
// 1,此时Go已经知道函数结束时,要打印1
defer fmt.Println(x)
x = 2
}

因为 defer fmt.Println(x) 注册时,就已经把传给函数的参数给固定下来了,而不是等到函数结束,真正执行defer时才去搞清楚x的值。

如果让其打印2呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func TestMethod7(t *testing.T) {
x := 1
// 1,此时Go已经知道函数结束时,要打印1
defer func(p *int) {
fmt.Println(*p)
}(&x)
x = 2
}
// 或者使用闭包也可以实现,因为闭包捕获变量本身。
func TestMethod8(t *testing.T) {
x := 1
// 1,此时Go已经知道函数结束时,要打印1
defer func() {
fmt.Println(x)
}()
x = 2
}

原因: 注册 defer 时,参数立即求值,求出来的是 x 的内存地址。函数结束时,Go 顺着这个地址去找,发现里面的值已经被改成 2 了。

defer还可以修改命名返回值

1
2
3
4
5
6
7
8
9
10
11
func test() (result int) {
defer func() {
result++
}()
return 1
}

func TestMethod9(t *testing.T) {
// 2
t.Logf("test() result %v", test())
}

执行流程:

  1. return 1 先把返回值 result 赋值为 1
  2. 执行 defer,result++
  3. 真正返回,结果是 2

panic和recover

在 Go 语言中,panicrecover 是处理运行时恐慌(致命错误)的特有机制。它们通常与 defer 配合使用,形成了类似于其他语言中 try-catch-finally 的结构,但其哲学和使用场景有着很大的区别。

panic

panic 是一个 Go 内置的函数,用来主动抛出程序无法继续运行的致命错误。

  • 触发方式: 可以是程序自动触发(如空指针引用、数组越界、除以 0),也可以是开发者手动调用 panic()

  • 执行流程: 当一个函数发生 panic 时,当前函数的正常执行流程会立即中断,但已注册的 defer 函数依然会被逐个执行。接着,panic 会沿着调用栈一层层向上抛出,直到程序崩溃并打印出堆栈信息。

recover

recover 也是一个内置函数,用来“拦截”并捕获 panic,让程序不至于崩溃。

  • 使用限制: recover() 必须在 defer 调用的函数中直接执行。在正常执行流程中调用 recover 会直接返回 nil 且毫无作用。

  • 执行效果: 如果在 defer 中捕获到了 panic,程序会停止向上崩溃,并恢复正常的执行逻辑(继续执行该 defer 之后的代码,或者安全退出当前函数)。

核心规则

  1. recover 必须写在 defer:直接写在函数体里的 recover() 是无法捕获任何恐慌的。

  2. recover 只能捕获同一个 Goroutine 的 panic:Go 语言的恐慌是协程(Goroutine)隔离的。如果 A 协程发生了 panic,B 协程里的 defer recover 是管不着的,程序依然会挂掉。

  3. 不要滥用 panic:Go 官方推崇的错误处理方式是返回 errorpanic 应该只用于真正致命的、无法挽回的错误(例如:数据库初始化失败、配置解析失败等)。

1
2
3
4
5
6
7
8
func safeRun() {
defer func() {
if err := recover(); err != nil {
fmt.Println("recover:",err)
}
}()
panic("oom")
}

另一个小例子:

1
2
3
4
5
6
7
8
9
10
func TestMethod11(t *testing.T) {
fmt.Println(" running ...")
defer func() {
if err := recover(); err != nil {
fmt.Printf("成功捕获异常,recover : %v\n", err)
}
}()
panic("OutOfMemory")
fmt.Println(" panic之后的代码不会被执行")
}

init 函数

看样子有点像Java的static{}

特点:

  • 没有参数

  • 没有返回值

  • 自动执行

  • 早于 main

  • 一个包中可以有多个 init

执行顺序大致是:

  1. 初始化导入的包
  2. 初始化包级变量
  3. 执行 init
  4. 执行 main
1
2
3
func init() {
fmt.Println("Init。。。。")
}

执行任意一个测试用例,即可输出如下:

1
2
3
4
5
Init。。。。
=== RUN TestMethod11
running ...
成功捕获异常,recover : OutOfMemory
--- PASS: TestMethod11 (0.00s)

如果导入某一个包

1
import _ "github.com/go-sql-driver/mysql" // 这会触发该包的 init() 来注册驱动

那么init函数和Java的static代码块有啥区别呢?

特性 Go 的 init 函数 Java 的 static 静态块
依附对象 Package(包级别) Class(类级别)
执行时机 main 函数运行前,包被加载时 类被加载(Class Load)时
手动调用 不允许 不允许
编写数量 同一个包/文件内可以写多个 同一个类里可以写多个(但极少这么做)
主要用途 初始化包级变量、环境检查、注册驱动 初始化静态成员变量