Golang基础(三):流程控制与函数
流程控制
条件语句if
基本形式: 1
2
3
4
5
6
7
8if cond1{
...
}
else if cond2{
...
}else{
...
}
条件必须是布尔值或者布尔表达式,不再支持整型0/1或者其他非零值(指针等);
条件表达式不需要括号,代码块必须有花括号;
最大的特点是支持初始化语句,允许在if后定义局部变量供给条件语句块使用:
1
2
3
4
5
6
7
8
9
10
11if x:=f(); x>0{
...
}else{
...
}
if n:="abc"; x>0{
fmt.Println(n[0])
}else{
fmt.Println(n[1])
}
条件语句switch
基本形式: 1
2
3
4
5
6
7var flag int = 0
switch flag {
case 0: flag = 1
case 1: flag = 2
case 2: flag = 3
default: flag = 4
}
case语句自带
break,每个case执行完自动退出;想要拒绝自动break实现多个case的穿透,在case的最后一条语句使用
fallthrough关键字1
2
3
4
5
6
7
8flag := 1
switch flag {
case 1:
fmt.Println("Hello")
fallthrough //也会执行case2
case 2:
fmt.Println("World")
}同样支持在
switch后写初始化公式:1
2
3
4
5
6
7
8
9switch flag := 1; flag {
case 1:
fmt.Println("Hello")
fallthrough //也会执行case2
case 2:
fmt.Println("World")
}
//fmt.Println(flag) //不能传递到语句外使用switch可以省略变量判断,golang默认处理为switch(true),适用于那种范围判断而不是相等判断,等效于if...else语句:1
2
3
4
5
6
7
8switch {
case score >= 90:
fmt.Println("优秀")
case score >= 60:
fmt.Println("及格")
default:
fmt.Println("不及格")
}
switch类型萃取
switch类型萃取,支持判别接口参数的类型,注意,必须是接口参数,而不是什么变量或者函数参数:
1
2
3
4
5
6
7
8
9
10
11
12//ok, interface type
var i interface{} //动态类型为nil,静态类型interface{}
switch t := i.(type) {
case int:
fmt.Println("int", t)
case float32:
fmt.Println("float32", t)
case string:
fmt.Println("string", t)
case nil:
fmt.Println("isNil", t) //isNil <nil>
}
1 | //wrong, non-interface type use .(type) is not allowed |
关于golang的接口和动态类型讨论见下章,我们还可以通过any能够将一般值装载到接口值里,如打印泛型的类型:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15func printType[T any](value T) {
switch t := any(value).(type) {
case int:
fmt.Println("int", t)
case float32:
fmt.Println("float32", t)
default:
fmt.Println("other", t)
}
}
func main() {
i := 10
printType(i) //int 10
}
循环语句for
for语句有若干种形式,也都比较简单:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17for i:=0; i<10; i++{
...
}
nums := []int{...}
for(i, value := range nums){
...
}
//类似while方法:
for a<b{
...
}
for true{
...
}
for range注意事项
对array、slice、map的遍历,第一个参数都是索引下标,第二个参数指向元素的拷贝:
1
2
3for i,v := range slice1{
...
}range在遍历前已经确定了指向底层数组的指针以及要遍历的长度,因此如果遍历中篡改切片的长度也是不会影响遍历:
1
2
3
4
5
6
7func main() {
s := []int{1, 2, 3, 4, 5}
for i, v := range s {
s = s[:3] //缩短切片
fmt.Println(i, v) //仍然能打印1到5
}
}
函数
普通函数基本规则
golang的函数和C++还是有鲜明的区别,具体有以下特点:
不支持函数重载,也不支持函数默认值,故同一个package内不能存在同名函数;
无需声明,声明和定义一般同时给出;
支持不定长参数(可与普通参数混用),也支持多返回值:
值得一提的是,可变参数列表可以被1
2
3
4
5
6
7
8func logf(level string, format string, args ...any) (string, string) {
return "OK", fmt.Sprintf("[%s]"+format, append([]any{level}, args...)...) //切片可同时装入不同类型,他们都被转成any
}
func main() {
notify, info := logf("INFO", "x=%d, y=%d", 2, 3)
fmt.Println(notify, info) //OK [INFO]x=2, y=3
}range遍历:1
2
3
4
5
6
7
8func doAdd(args ...int) []int {
res := make([]int, 0)
for _, pos := range args {
pos += 1
res = append(res, pos)
}
return res //[2 3 4 5 6]
}不支持嵌套,但支持匿名函数和闭包,嵌套是指在函数内部直接定义一个函数声明:
1
2
3func once(){
func twice(){...} //嵌套函数定义错误!可以使用匿名函数代替
}支持命名返回的函数值,这个很特别,即golang允许你直接命名返回值,然后直接使用return即可,称裸返回:
但注意,如果是1
2
3
4func getNum(a, b int)(sum int){
sum = a+b
return //sum被命名了,无需显示写return sum
}slice、map、chan等底层引用类型,要注意命名对象空间是否被分配和初始化;若返回值命名和局部变量重名,必须使用显式返回:
1
2
3
4
5
6
7
8
9
10
11
12
13func getNum(a, b int) (z int) {
z = 10
{ //独立作用域
var z int = a + b
return z
}
return
}
func main() {
s := getNum(1, 2)
fmt.Println(s) //3
}函数的左花括号不能写到下一行,这是习惯使用C/C++分行format插件的人的噩梦:
1
2
3
4func once()
{ //错误!花括号不能写到这行
}slice、map、chan、指针、interface作为参数时默认均以底层指针的方式传递,修改参数会影响对象;slice也能够适配可变参数,但是传参时要使用省略号展开:1
2
3
4
5
6
7
8func push_front_elem[T any](dst []T, src ...T) []T {
n := len(src)
dst = append(dst, src...) //扩容
copy(dst[n:], dst[:len(dst)-n])
copy(dst[:n], src)
return dst
}
s1 = push_front_elem(s1, []int{22, 33, 44}...) //OK
golang延迟defer闭包
golang允许在函数内部使用关键字defer定义延迟函数,该函数会在调用函数返回前或者panic前捕获变量值并且执行函数:
1
2
3
4
5
6
7
8
9
10
11
12func getNum(a, b int) (z int) {
z = a + b
defer func() {
z += 10
}()
fmt.Println("z value before return:", z)
return z
}
func main() {
fmt.Println("z=", getNum(1, 2)) //
}1
2z value before return: 3
z= 13
这种特性可以用于对返回值的处理、资源的收尾、异常的处理或者计时的结束打印,如打印执行时间:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23package main
import (
"fmt"
"time"
)
func getDuratiom() (z int) {
start := time.Now()
for i := range 10000 {
z += i
}
time.Sleep(1000 * time.Millisecond)
defer func() {
fmt.Printf("duration = %s, res = %d", time.Since(start), z)
}()
return
}
func main() {
getDuratiom() //duration = 1.0004287s, res = 49995000
}
注意,如果被defer修饰的闭包或者普通函数存在实参,这个实参是defer定义时就捕获的,而不是被捕获变量那样,直到执行(函数返回)时才捕获;
而且这个实参被捕获到栈中,遵循LIFO(后进先出)原则:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19package main
import "fmt"
func getDuratiom() {
for i := 0; i < 3; i++ {
defer fmt.Printf("%d\t", i) //普通函数捕获实参
}
for i := 3; i < 6; i++ {
defer func(v int) {
fmt.Printf("%d\t", v) //形参v
}(i) //闭包捕获实参i
}
fmt.Println("exit")
}
func main() {
getDuratiom() //duration = 1.0004287s, res = 49995000
}
输出: 1
2exit
5 4 3 2 1 0
匿名函数
golang的匿名函数非常强大,其完全可以被认为是一个普通变量,除了C++中那种作局部变量被调用、立刻执行等用法,还可以充当函数参数、返回值、结构体参数等,见下。
基本形式
定义成匿名函数变量:
1
2
3
4
5
6
7func test() {
doAdd := func(a, b int) int {
return a + b
}
doAdd(1, 2)
}立刻定义和调用, 获取执行结果:
1
2
3
4
5
6func test() {
res := func(a, b int) int {
return a + b
}(1, 2)
fmt.Println(res) //3
}匿名函数作为函数参数传入(类似
std::function):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
26package main
import "fmt"
func test(nums []int, f func(int) int) []int {
res := make([]int, len(nums))
for i, pos := range nums {
res[i] = f(pos)
}
return res
}
func main() {
s := []int{
1, 2, 3, 4, 5,
}
s = test(s, func(i int) int {
if i%2 == 0 { //偶数翻倍,奇数清零
i *= 2
} else {
i = 0
}
return i
})
fmt.Println(s) //[0 4 0 8 0]
}匿名函数作为返回值返回:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17//返回int(int)函数类型
func makeCounter(start int) func(end int) int {
n := start
return func(end int) int {
if n < end {
n++
}
return n
}
}
func main() {
counter := makeCounter(0)
for i := 0; i < 100; i++ {
fmt.Printf("%d ", counter(5)) //1,2,3,4,5,5,5,5,5,...
}
}
