
文章目录1. if-else 结构2. switch 结构3. for 结构 3.1 传统 for 循环 3.2 while 风格只有条件 3.3 无限循环 3.4 Range 遍历 3.4.1 遍历数组/切片 3.4.2 遍历 map 3.4.3 遍历字符串 3.4.4 遍历 channel4. break 和 continue 标签5. goto6. 小结本系列文章Java 转 go 学习 - 项目管理Java 转 go 学习 - 基本语法Java 转 go 学习 - 类型转换在前面的文章中我们学习了 go 的基本语法如何创建一个项目类型转换这篇文章就来仔细看看 go 的控制结构如if、switch、for…1. if-else 结构Go 的 if-else 和 Java 不一样不需要()将判断条件包起来但是必须要有{}包裹代码块同时要注意一点{}的{和if需要在同一行如果在不同行会报错这点跟 Java 是不一样的下面是 if 的基本结构// 基本格式if条件表达式{// 条件为 true 时执行}else{// 条件为 false 时执行}下面再来看下核心设计带初始化的 if可以在条件判断之前先执行一个简短的赋值或函数调用跟 Java 的 for 循环初始化类似语法格式如下if初始化语句;条件表达式{// ...}这样的好处就是将变量限制在if-else块之内出了这个块变量就销毁了避免污染外部命名空间。下面来看下一个具体的例子functest1(){// 1. 传统写法val,err:getValue()iferr!nil{fmt.Println(Error:,err)}else{fmt.Println(Value:,val)}// 此时 val 和 err 依然在 main 函数的作用域内可能干扰后续代码// 2. Go 推荐写法ifval,err:getValue();errnil{fmt.Println(Error:,err)}else{fmt.Println(Value:,val)// 这里可以使用 val}// fmt.Println(val) // ❌ 第二种写法编译错误val 在这里已经未定义了} 如果某个变量只在 if 或 else 里用到优先使用 if v, err : …; err ! nil 这种写法下面再来看下多分支判断也就是 else if这个应该都很熟悉了直接看下面的例子就行了functest2(){score:85ifscore90{fmt.Println(优秀 (A))}elseifscore80{fmt.Println(良好 (B))}elseifscore60{fmt.Println(及格 (C))}else{fmt.Println(不及格 (F))}}下面再来看下进阶写法首先就是布尔类型的值不需要 true这样来写和 java 是一样的。functest3(){isValid:true// ❌ifisValidtrue{// ...}// ✅ifisValid{// ...}// ❌ifisValidfalse{// ...}// ✅if!isValid{// ...}}然后是提前返回这一点也是如果遇到异常情况就直接return减少层级嵌套对应的就是减少else if的嵌套尽量使用扁平化结构。// 1. 避免嵌套过深funcProcess(data[]int)error{ifdata!nil{iflen(data)0{ifdata[0]0{// 主逻辑...returnnil}else{returnerrors.New(first element not positive)}}else{returnerrors.New(empty data)}}else{returnerrors.New(nil data)}}// 2. 扁平化funcProcess(data[]int)error{// 先处理所有异常/边界情况ifdatanil{returnerrors.New(nil data)}iflen(data)0{returnerrors.New(empty data)}ifdata[0]0{returnerrors.New(first element not positive)}// ....业务逻辑...returnnil}最后再来看一些常见的错误❌ 误用代替这点很好理解就是 if 里面不允许赋值操作要知道 Java 里面是允许在 if 里面赋值的尤其是 JDK 并发包下面的源码就存在大量这种写法看的时候难受的一批。❌ 浮点数直接用比较这点也不难理解因为精度问题最经典的就是0.1 0.2 ! 0.3所以如果要对浮点数去比较最好是按照下面的写法之前学算法的时候用 java 比较浮点数也是这么干的。constepsilon1e-9ifabs(a-0.3)epsilon{fmt.Println(Approximately Equal)}❌if和{不在同一行else if和{不在同一行这种就是结构出错了。// ❌ 错误写法ifa1{}elseifa2{}最后用 ai 总结一下特性特性说明示例圆括号禁止使用if x 0(✅) /if (x 0)(❌)花括号必须使用即使一行代码也要加{}初始化语句特色功能限制变量作用域if v, err : f(); err ! nil { ... }Else If多分支判断else if condition { ... }布尔简写直接写变量或取反if ok { ... },if !ok { ... }赋值检查编译器禁止在条件中赋值防止if x 1错误2. switch 结构Go 的 switch 类型要比 Java 的更加间接并且默认自动 break条件表达式可以是任何类型基本结构如下同样的不需要圆括号()但必须有大括号{}。switch表达式{case值1:// 代码块case值2,值3:// 可以匹配多个值用逗号分隔// 代码块default:// 如果以上都不匹配执行这里可选}go 默认 case 会自动 break那如果想要穿透到下一个 case 需要用到关键字fallthrough且这个关键字必须是 case 的最后一条语句可以看下下面的例子functest4(){level:1switchlevel{case1:fmt.Println(Level 1: Start game)// 强制进入下一个 casefallthroughcase2:fmt.Println(Level 2: Bonus unlocked)// 强制进入下一个 casefallthroughcase3:fmt.Println(Level 3: Boss fight)default:fmt.Println(Game Over)}// 输出:// Level 1: Start game// Level 2: Bonus unlocked// Level 3: Boss fight}同样的switch 也支持在判断前初始化下面是一个例子// 模拟获取用户权限等级funcgetPermission()int{return2}functest5(){// 单个变量初始化switchlevel2:getPermission();{caselevel21:fmt.Println(Read Only)caselevel22:fmt.Println(Read Write)// 匹配这里caselevel23:fmt.Println(Admin)}// 多个变量平行初始化switcha,b:x[0],y[0];{caseab:t-1caseab:t0caseab:t1}}也可以像下面那样用来判断一个interface{}空接口的类型:functest6(iinterface{}){// v 是接口 i 的具体值, %v 会根据不同类型自动适配输出switchv:i.(type){caseint:fmt.Printf(Twice %v is %v\n,v,v*2)casestring:fmt.Printf(Len of %q is %v\n,v,len(v))casebool:fmt.Printf(Value is %v\n,v)casefloat64:fmt.Printf(Is float: %v\n,v)default:fmt.Printf(Unknown type %T\n,v)// %T 打印类型}}funcmain(){test6(10)// 输出: Twice 10 is 20test6(Hello)// 输出: Len of Hello is 5test6(true)// 输出: Value is truetest6(3.14)// 输出: Is float: 3.14}// Twice 10 is 20// Len of Hello is 5// Value is true// Is float: 3.14最后要说一下如果传入的是 nil那么默认匹配 default 分支functest7(){variinterface{}nilswitchi.(type){casenil:fmt.Println(Its nil!)default:fmt.Println(Not nil)}}// Its nil!3. for 结构同样先来看下基础语法结构Go 的 for 循环不需要圆括号()但必须有大括号{}。for初始化语句;条件表达式;迭代语句{// 循环体}下面是三种经典的遍历方式。 3.1 传统 for 循环最经典的计数循环类似 C/Java 的 for把 Java 的 for 循环括号去掉就是了比如fori:0;i5;i{fmt.Println(i)}// 0// 1// 2// 3// 4 3.2 while 风格只有条件go 是没有 while 的但是可以用 for 代替省略初始化和迭代语句只保留条件就行了Java 也可以这么干但是必须要按照for(; i 10; )这种格式使用;标记每一部分但是 go 的 for 循环不需要;直接for i 10就可以下面是例子functest9(){// 省略初始化和迭代语句只保留条件count:0forcount5{fmt.Println(Count:,count)count// 手动更新否则会死循环}} 3.3 无限循环// 省略所有部分分号也省略for{// 永远执行...// 通常配合条件退出ifsomeCondition{// 跳出循环break}} 3.4 Range 遍历range 遍历是 go 中最常见的遍历方式可以用来遍历数组、切片、map、channel语法格式如下·forindex,value:rangecollection{// 使用 index 和 value}如果不想访问index可以使用_代替表示忽略这个变量不然如果在 for 循环里面没有用到 index 编译会报错如for_,value:rangecollection{// 使用 index 和 value}同理不想使用 value可以不写 value又或者使用_代替不过 index 由于是第一个值所以后面的_可以不用写// 只取索引fori:rangecollection{} 3.4.1 遍历数组/切片functest10(){nums:[]int{10,20,30,40}fori,v:rangenums{fmt.Printf(Index: %d, Value: %d\n,i,v)}// 用 _ 忽略索引for_,v:rangenums{fmt.Println(Value:,v)}// 只取索引fori:rangenums{fmt.Println(Index:,i)}// Index: 0, Value: 10// Index: 1, Value: 20// Index: 2, Value: 30// Index: 3, Value: 40// Value: 10// Value: 20// Value: 30// Value: 40// Index: 0// Index: 1// Index: 2// Index: 3}⚠️ 这里的 v 获取到的是数组下标值的副本也就是直接修改 v 不会影响到原来的值functest10(){nums:[]int{1,2,3}for_,v:rangenums{// 不会修改原数组值vv*2}fmt.Println(nums)// [1 2 3]// ✅ 正确修改方式通过索引fori:rangenums{nums[i]nums[i]*2}fmt.Println(nums)// [4 5 6]} 3.4.2 遍历 mapfunctest11(){m:map[string]int{a:1,b:2,c:3,}// 获取 key - valuefork,v:rangem{fmt.Printf(Key: %s, Value: %d\n,k,v)}// 只获取 valuefor_,v:rangem{fmt.Printf(Value: %d\n,v)}// 只获取 keyfork:rangem{fmt.Printf(Key: %s\n,k)}}Map 的遍历顺序是随机的不是有顺序的比如上面的输出就是Key:a,Value:1Key:b,Value:2Key:c,Value:3Value:1Value:2Value:3Key:c Key:a Key:b 3.4.3 遍历字符串遍历字符串range 会按照Unicode 码点rune遍历而不是字节Go 中字节就是uint8也就是byte比如你好 GO中文3字节加起来就是9字节。Unicode 码点是每个字符在 Unicode 字符集中的唯一数字标识比如你 U4F60“A”U0041Go 中用rune (int32 别名)表示 Unicode 码点这样的好处就是每一次都能获取到一个完整的字符而不是单个字节比如你好 GO就是5个码点。range 遍历的规则就是自动解析UTF-8编码将多个字节合成一个rune然后输出如果是普通的for循环就是按照字节遍历下面对比两种输出functest12(){s:Go 语言// 看类型fori,ch:ranges{fmt.Printf(Index: %d, Char: %c, Type: %T\n,i,ch,ch)// Index: 0, Char: G, Type: int32// Index: 1, Char: o, Type: int32// Index: 2, Char: , Type: int32// Index: 3, Char: 语, Type: int32// Index: 6, Char: 言, Type: int32}// 对比两种遍历方式s你好 Go// 按字节索引遍历, i 是字节索引s[i]是单个字节fori:0;ilen(s);i{fmt.Printf(索引%d字节值%d字符%c\n,i,s[i],s[i])// 索引0字节值228字符ä// 索引1字节值189字符½// 索引2字节值160字符// 索引3字节值229字符å// 索引4字节值165字符¥// 索引5字节值189字符½// 索引6字节值32字符// 索引7字节值71字符G// 索引8字节值111字符o}// 按 rune 遍历i 是字符起始字节索引, r是 rune 值, 需要转成 stringfori,r:ranges{fmt.Printf(起始索引%drune值%U字符%s\n,i,r,string(r))// 起始索引0rune值U4F60字符你// 起始索引3rune值U597D字符好// 起始索引6rune值U0020字符// 起始索引7rune值U0047字符G// 起始索引8rune值U006F字符o}} 3.4.4 遍历 channelfunctest13(){ch:make(chanint,3)ch-1ch-2ch-3close(ch)// 必须关闭否则 range 会阻塞// 自动遍历直到 channel 关闭forv:rangech{fmt.Println(Received:,v)// Received: 1// Received: 2// Received: 3}}注意 channel 如果不 closerange 就会阻塞等待下一个输入报错fatal error: all goroutines are asleep - deadlock!。4. break 和 continue 标签break 和 continue 的基础用法和 Java 差不多关键是下面两点break 退出当前层级continue 继续指向当前层级的下一次遍历不过加上标签就可以做到直接跳出外层循环或者跳过外层循环的本次遍历基础用法就不多说了下面主要是来看下标签的用法标签的定义如下OUTER:fori:1;i10;i....上面OUTER就是一个标签定义在循环上方使用冒号结尾下面是 break 跳出外层循环的例子functest14(){OUTER:fori:1;i3;i{forj:1;j3;j{ifi*j6{// 直接跳出OUTER标签对应的外层循环breakOUTER}fmt.Printf(i%d, j%d, 乘积%d\n,i,j,i*j)}}fmt.Println(多层循环结束)// i1, j1, 乘积1// i1, j2, 乘积2// i1, j3, 乘积3// i2, j1, 乘积2// i2, j2, 乘积4// 多层循环结束}可以看到遍历到i 2, j 3的时候就退出了外层 for 循环下面再来看下 continuefunctest15(){OUTER:fori:1;i3;i{forj:1;j3;j{ifj2{// icontinueOUTER}fmt.Printf(i%d, j%d\n,i,j)}}// i1, j1// i2, j1// i3, j1}5. gotoGo 支持 goto但官方强烈不推荐使用除非在极少数特殊场景跟 Java 是一样的大量 goto 跳来跳去会让代码可读性比较差但是既然支持这种写法就得看下goto 的语法如下goto标签名// ... 其他代码 ...标签名:// 跳转目标例子如下functest16(){fori:0;i10;i{ifi5{gotoend// 跳转到 end 标签}fmt.Println(i)}end:fmt.Println(Done)// 0// 1// 2// 3// 4// Done}下面是 goto 的一些限制不能跨函数跳转标签必须在同一函数内。不能跳过变量声明也就是不能跳过变量初始化的代码。不能跳入子块内部意思是不能从外部跳转到 if/for 块内部functest17(){// 不能跳过下面 x 的初始化gotoskip x:10skip:// 不能跳进子块里面gotoinsideiftrue{inside:// 标签在块内不能从外部跳入// ...}}6. 小结好了本文着重学习对比了 Java 和 Go 的流程控制结构下一篇文章继续学习数组切片。如有错误欢迎指出