目录
【if语句】
if语句的内部变量
if语句的优雅写法
【switch语句】
switch语句的特点
switch语句的表达式类型
switch获取变量类型 x.(type)
【for语句】
for语句的变体
for...range
break 和 continue
goto
for 语句的常见“坑”与避坑方法
Go语言的条件判断有 if 和 switch-case 两种形式;循环结构只有 for 这一种形式。
- Go 中的if、switch、fot语句不需要使用小括号包裹
- Go 坚持 “一件事情仅有一种做法的理念”,只保留了 for 这一种循环结构,去掉了传统语言中的 while 和 do-while 循环结构;
- Go 中 switch 分支结构中每个 case 语句不需要以 break 收尾,并且支持 fallthrough 穿透到后面的分支;
- Go 支持了 type switch 特性,让“类型”信息也可以作为分支选择的条件;
- Go 的 switch 控制结构的 case 语句还支持表达式列表,让相同处理逻辑的多个分支可以合并为一个分支。
【if语句】
操作符优先级决定了操作数优先参与哪个操作符的求值运算,优先级如下所示
a, b := false, true
//下面的if语句实际上不是 (a && b) != true,而是 a && (b != true)
if a && b != true {
println("return true")
return
}
println("return false") //输出 return false,因为 != 的优先级高于 &&
因此为了防止混淆优先级,记得使用小括号把想要优先运算的部分括起来。
if语句的内部变量
可以在 if 的布尔表达式前声明属于if语句自己的变量,这些变量只可以在 if 语句的代码块范围内使用。
//下面的 diff 只能在 if ... else 块里面使用
yesterday := 27
today := 30
if diff := today - yesterday; diff > 0 {
fmt.Printf("今天气温比昨天升高了: %d\n", diff)
} else {
fmt.Printf("今天气温比昨天降低了: %d\n", diff)
}
//fmt.Printf(diff) //此处报错:undefined: diff
上面这种写法一般用在判断一个函数的返回值是否正确,看下面的例子:
package main
import "fmt"
func main() {
if username, err := getUserName(); err == nil {
fmt.Println("username: ", username)
} else {
fmt.Println("get username error")
}
}
func getUserName() (string, error) {
return "zhangsan", nil
}
if语句的优雅写法
一般不推荐层层嵌套的复杂冗长的if语句块,而是建议遇到false的情况及时return,能提高代码的阅读效果,如下所示:
package main
import "fmt"
func main() {
//if语句的优雅写法
score := 80
ifDemo1(score)
ifDemo2(score)
}
func ifDemo1(score int) {
if score >= 0 && score < 60 {
fmt.Println("未及格")
} else if score >= 60 && score < 80 {
fmt.Println("及格了但不够优秀")
} else if score >= 80 && score < 95 {
fmt.Println("比较优秀但还有上升空间")
} else if score >= 95 && score <= 100 {
fmt.Println("三好学生")
} else {
fmt.Println("分数有问题")
}
}
func ifDemo2(score int) {
if score >= 0 && score < 60 {
fmt.Println("未及格")
return
}
if score >= 60 && score < 80 {
fmt.Println("及格了但不够优秀")
return
}
if score >= 80 && score < 95 {
fmt.Println("比较优秀但还有上升空间")
return
}
if score >= 95 && score <= 100 {
fmt.Println("三好学生")
return
}
fmt.Println("分数有问题")
}
很明显,上面的 ifDemo2() 的可读性更高,尤其是在if语句里面又嵌套了一层if语句的时候,这种改造效果会更好。很多优秀的设计模式都是对复杂的条件判断进行的优化。
【switch语句】
先用switch语句改造一下上面的代码:
score := 70
switch score {
case 10:
fmt.Println("未及格10")
case 20:
fmt.Println("未及格20")
break
fmt.Println("Unreachable code") //当前分支的 break后面的语句 不会再执行
case 60, 70:
fmt.Println("及格了60/70")
fallthrough
case 80, 90:
fmt.Println("比较优秀80/90")
default:
fmt.Println("分数有问题")
}
/*
输出:
及格了60/70
比较优秀80/90
*/
switch语句的特点
从上面的例子中可以得到Go语言中的switch有如下特点:
- 匹配到复合条件的分之后直接返回,不需要添加 break;(如果加了break,当前分支的 break后面的语句 不会再执行,没必要这么写)
- 使用 fallthrough 可以穿透当前分支,但是如果某个 case 语句已经是最后一个 case 并且它的后面也没有 default 分支了,那么这个 case 中就不能再使用 fallthrough
- 每个 case 后面可以写多个值
- 无论 default 分支出现在什么位置,它都只会在所有 case 都没有匹配上的情况下才会被执行的。
- 建议将匹配成功概率高的 case 表达式排在前面,就会提升 switch 语句执行效率。
针对上面的例子,如果score是75则不能匹配。在Go语言中,switch可以使用类似if语句的范围判断:
score := 75
switch {
case score >= 0 && score < 60:
fmt.Println("未及格")
case score >= 60 && score < 80:
fmt.Println("及格了")
case score >= 80 && score < 95:
fmt.Println("比较优秀")
case score >= 95 && score <= 100:
fmt.Println("三好学生")
default:
fmt.Println("分数有问题")
}
//输出:及格了
switch语句的表达式类型
switch 语句中表达式类型可以是:整型、布尔类型、字符串类型、复数类型、数组类型、结构体类型。
type person struct {
name string
age int
}
p := person{
"tom", 13,
}
switch p {
case person{"tony", 33}:
println("match tony")
case person{"tom", 13}:
println("match tom")
case person{"lucy", 23}:
println("match lucy")
default:
println("no match")
}
//输出: match tom
switch language := "golang"; language {
case "php":
fmt.Println("PHP语言。。")
case "golang":
fmt.Println("Go语言")
case "java":
fmt.Println("Java语言")
}
//输出: Go语言
switch获取变量类型 x.(type)
package main
import "fmt"
func main() {
x := "hello"
fmt.Println(getVariableType(x)) //the type of v is string, v = hello
}
func getVariableType(x interface{}) string {
switch v := x.(type) { //此处的 v 存储的是变量 x 的动态类型对应的值信息
case nil:
return "v is nil"
case int:
return fmt.Sprintf("the type of v is int, v = %v", v)
case string:
return fmt.Sprintf("the type of v is string, v = %v", v)
case bool:
return fmt.Sprintf("the type of v is bool, v = %v", v)
default:
return fmt.Sprintf("don't support the type")
}
}
Go 中所有类型都实现了 interface{}类型,所以 case 后面可以是任意类型信息。
【for语句】
Go 语言的 for 循环支持声明多个变量,并且可以应用在循环体以及判断条件中
//for循环中声明多个变量
var sum int
for i, j, k := 0, 1, 2; (i < 20) && (j < 10) && (k < 30); i, j, k = i+1, j+1, k+5 {
sum += i + j + k
fmt.Print(sum, " ") //3 13 30 54 85 123
}
for语句的变体
//省略前置语句
i := 0
for ; i < 10; i++ {
i++
}
//省略后置语句
for i := 0; i < 10; {
i++
}
//省略前置和后置语句
for i < 10 {
println(i)
i++
}
//全都省略,类似于PHP的 while 循环
m := 0
for {
println("死循环")
m++
if m >= 3 {
break
}
}
for...range
在 for range 语句中,range 后面接受的表达式的类型可以是数组、数组的指针、切片、字符串、map、channel(注意:要对 map 进行循环操作,for range 是唯一的方法。关于map的更多用法请参考:Go语言中array、slice、map的用法和细节分析_浮尘笔记的博客-CSDN博客)
var map1 = map[string]int{
"zhangsan": 18,
"lisi": 22,
"wangwu": 15,
}
for k, v := range map1 {
fmt.Print(k, "=>", v, "; ") //lisi=>22; wangwu=>15; zhangsan=>18;
}
for...range也有几种变体,如下所示:
//如果不关心元素的值,可以省略元素值的变量,只声明下标
for i := range map1 {
fmt.Print(i, " ") //zhangsan lisi wangwu
}
//如果不关心元素下标,只关心元素值,可以用空标识符替代下标
for _, v := range map1 {
fmt.Print(v, " ") //18 22 15
}
//如果既不关心下标值也不关心元素值,只是循环它的次数
for _, _ = range map1 {
fmt.Print("A", " ") //A A A
}
//或者可以直接这样写
for range map1 {
fmt.Print("B", " ") //B B B
}
使用 for...range 循环字符串的注意事项:for range 对于 string 类型来说,每次循环得到的 v 值是一个 Unicode 字符码点(也就是 rune 类型值),而不是一个字节,返回的第一个值 i 为该 Unicode 字符码点的内存编码(UTF-8)的第一个字节在字符串内存序列中的位置。
// 使用 for...range 遍历英文和中文字符串
var str1 = "hello"
for k, v := range str1 {
fmt.Printf("%d => %c ,", k, v) //0 => h ,1 => e ,2 => l ,3 => l ,4 => o ,
}
println()
var str2 = "你好呀"
for k, v := range str2 {
fmt.Printf("%d => %c ,", k, v) //0 => 你 ,3 => 好 ,6 => 呀 ,
fmt.Printf("%d -> %s -> 0x%x, ", k, string(v), v) //0 -> 你 -> 0x4f60, 3 -> 好 -> 0x597d, 6 -> 呀 -> 0x5440,
}
break 和 continue
break用于跳出所有循环,continue用于跳出当前循环,并且都可以指定标签,看下面的例子:
//break用法
for i := 1; i <= 3; i++ {
for j := 1; j <= 3; j++ {
if j == 2 {
break
}
fmt.Printf("i:%d,j:%d\n", i, j)
}
}
/*输出
i:1,j:1
i:2,j:1
i:3,j:1
*/
//break 加上标签
out:
for i := 1; i <= 3; i++ {
for j := 1; j <= 3; j++ {
if j == 2 {
break out
}
fmt.Printf("i:%d,j:%d\n", i, j)
}
}
/*输出
i:1,j:1
*/
//continue用法
for i := 1; i <= 5; i++ {
for j := 1; j <= 5; j++ {
if j == 2 {
continue
}
fmt.Printf("i:%d,j:%d\n", i, j)
}
}
/*输出
i:1,j:1
i:1,j:3
i:2,j:1
i:2,j:3
i:3,j:1
i:3,j:3
*/
//continue 加上标签
out:
for i := 1; i <= 3; i++ {
for j := 1; j <= 3; j++ {
if j == 2 {
continue out
}
fmt.Printf("i:%d,j:%d\n", i, j)
}
}
/*输出
i:1,j:1
i:2,j:1
i:3,j:1
*/
goto
for i := 0; i < 10; i++ {
for j := 0; j < 10; j++ {
fmt.Printf("i:%d,j:%d\n", i, j)
if j == 2 {
goto breakHere
}
fmt.Println("这里的内容不会跳过")
}
fmt.Println("这里的内容会跳过1")
}
fmt.Println("这里的内容会跳过2")
fmt.Println("一直到breakHere标记之间的内容都会跳过")
breakHere:
fmt.Println("直接跳到这里来了")
/*输出
i:0,j:0
这里的内容不会跳过
i:0,j:1
这里的内容不会跳过
i:0,j:2
直接跳到这里来了
*/
for 语句的常见“坑”与避坑方法
(1)在goroutine中使用for循环的问题
//问题一:对一个整型切片进行遍历,并且在每次循环体的迭代中都会创建一个新的Goroutine,输出这次迭代的元素的下标值与元素值。
var m = []int{1, 2, 3, 4, 5}
for i, v := range m {
go func() {
fmt.Print(i, "=>", v, ",")
}()
}
time.Sleep(time.Second * 1)
fmt.Println()
//预期结果: 0=>1,1=>2,2=>3,3=>4,4=>5, (以为每次迭代都会重新声明两个新的变量 i 和 v)
//实际结果: 4=>5,4=>5,4=>5,4=>5,4=>5, (这些循环变量在 for range 语句中仅会被声明一次,且在每次迭代中都会被重用)
问题分析:Goroutine 执行的闭包函数引用了它的外层包裹函数中的变量 i、v,这样变量 i、v 在主Goroutine 和新启动的 Goroutine 之间实现了共享, 而 i, v 值在整个循环过程中是重用的,仅有一份。在 for range 循环结束后,i = 4, v = 5,因此各个 Goroutine 输出的是 i, v 的最终值。解决办法如下:
//为闭包函数增加参数,并且在创建 Goroutine 时将参数与 i、v 的当时值进行绑定
for i, v := range m {
go func(i, v int) {
fmt.Print(i, "=>", v, ",")
}(i, v)
}
time.Sleep(time.Second * 1)
fmt.Println()
//执行结果:4=>5,1=>2,3=>4,2=>3,0=>1, (备注:每次执行的顺序可能不一致,这是由 Goroutine 的调度所决定的)
(2)问题二:在 for...range 循环中参与循环的是range的副本
var a = [5]int{1, 2, 3, 4, 5}
var r [5]int
fmt.Println("原始的a=", a) // [1 2 3 4 5]
for i, v := range a {
if i == 0 {
a[1] = 12
a[2] = 13
}
r[i] = v
}
fmt.Println("循环后a =", a) // [1 12 13 4 5]
fmt.Println("循环后r =", r) // [1 2 3 4 5]
fmt.Println()
//修改一种方式,用切片 a[:] 替代原先的数组a
fmt.Println("原始的a=", a) // [1 2 3 4 5]
for i, v := range a[:] {
if i == 0 {
a[1] = 12
a[2] = 13
}
r[i] = v
}
fmt.Println("循环后a =", a) // [1 12 13 4 5]
fmt.Println("循环后r =", r) // [1 12 13 4 5]
(3)问题三:由于遍历 map 中元素的随机性:如果在循环的过程中,对map进行了修改,那么这样修改的结果是否会影响后续迭代?
var map1 = map[string]int{
"A": 21,
"B": 22,
"C": 23,
}
point := 0
for k, v := range map1 {
if point == 0 { //当 point 值为 0 时删除map中的一个元素
delete(map1, "A")
}
point++
fmt.Print(k, "=>", v, ",")
}
fmt.Println("point is ", point)
//反复运行这个例子多次,可能会得到两个不同的结果:
//当 k="A" 是第0个元素出现时会得到如下结果:A=>21,B=>22,C=>23,point is 3
//当 k="A" 不是第0个元素出现时会得到如下结果:B=>22,C=>23,point is 2
//如果针对 map 类型的循环体中新创建了元素,那这项元素可能出现在后续循环中也可能不出现
var map1 = map[string]int{
"A": 21,
"B": 22,
"C": 23,
}
point := 0
for k, v := range map1 {
if point == 0 { //当 point 值为 0 时 给 map中新增一个元素
map1["D"] = 24
}
point++
fmt.Print(k, "=>", v, ",")
}
fmt.Println("point is ", point)
//反复运行这个例子,执行结果也会有两种情况
//A=>21,B=>22,C=>23,D=>24,point is 4
//或者 A=>21,B=>22,C=>23,point is 3
考虑到上述这种随机性,在遇到遍历 map 的时候同时需要对 map 修改的场景要格外小心。
源代码:go-demo-2023: Go语言基本用法和实用笔记 - Gitee.com