Go语言的条件判断和循环语句

本文详细介绍了Go语言中的if、switch和for语句的用法,包括if语句的内部变量、优雅写法,switch语句的特点、表达式类型和类型切换,以及for语句的各种变体和注意事项。文章还特别强调了在并发环境中使用for循环时需要注意的变量作用域问题和循环中的陷阱,提醒开发者在遍历map时需谨慎修改map本身。
摘要由CSDN通过智能技术生成

目录

【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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

单线程boy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值