今天简单的学习下流程控制与函数,这一块大多数语言都差不多。
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,不能用 0、1、nil 等隐式转 bool。
1 2 3 4 5 6 7 func TestIf1 (t *testing.T) { n := 1 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" ) } else { t.Log("teenager" ) } } 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 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 的条件。
switch和if一样,都支持初始化语句
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有多种写法:
类C语言的三段式for
类似while的for
无限循环
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) } n := 5 for n > 0 { t.Log(n) n-- } for { t.Log("running" ) } nums := []int {2 , 3 , 4 } for index, num := range nums { t.Log(index, num) }
只要有循环,那就会有break和continue
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的字符串
1 2 3 4 5 6 7 8 9 10 11 func TestForArray (t *testing.T) { arr := [3 ]int {1 , 2 , 3 } 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 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 }
对于如果连续多个参数类型相同,可以进行参数类型简写:
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,本质上也是拷贝它们的描述符或引用头部。
对于slice、map,可参考前一天的介绍,尤其注意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 { 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) 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 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 defer func (p *int ) { fmt.Println(*p) }(&x) x = 2 } func TestMethod8 (t *testing.T) { x := 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) { t.Logf("test() result %v" , test()) }
执行流程:
return 1 先把返回值 result 赋值为 1
执行 defer,result++
真正返回,结果是 2
panic和recover 在 Go 语言中,panic 和 recover 是处理运行时恐慌(致命错误)的特有机制。它们通常与 defer 配合使用,形成了类似于其他语言中 try-catch-finally 的结构,但其哲学和使用场景有着很大的区别。
panic panic 是一个 Go 内置的函数,用来主动抛出程序无法继续运行的致命错误。
recover recover 也是一个内置函数,用来“拦截”并捕获 panic,让程序不至于崩溃。
核心规则
recover 必须写在 defer 中 :直接写在函数体里的 recover() 是无法捕获任何恐慌的。
recover 只能捕获同一个 Goroutine 的 panic :Go 语言的恐慌是协程(Goroutine)隔离的。如果 A 协程发生了 panic,B 协程里的 defer recover 是管不着的,程序依然会挂掉。
不要滥用 panic :Go 官方推崇的错误处理方式是返回 error。panic 应该只用于真正致命的、无法挽回的错误 (例如:数据库初始化失败、配置解析失败等)。
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
执行顺序大致是:
初始化导入的包
初始化包级变量
执行 init
执行 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函数和Java的static代码块有啥区别呢?
特性
Go 的 init 函数
Java 的 static 静态块
依附对象
Package(包级别)
Class(类级别)
执行时机
main 函数运行前,包被加载时
类被加载(Class Load)时
手动调用
不允许
不允许
编写数量
同一个包/文件内可以写多个
同一个类里可以写多个(但极少这么做)
主要用途
初始化包级变量、环境检查、注册驱动
初始化静态成员变量