(一)基础语法篇——变量、流程控制与函数
在上一篇《系列序言》中,我聊了聊自己自学 Go 的踩坑经历。从本篇开始,正式进入硬核干货!本期我们先解决“为什么学 Go”的疑惑,然后从零开始,吃透变量、流程控制与函数这三大语法基石。《系列序言》
四、变量 —— 程序的地基砖块
不管学啥语言,变量都是第一道坎。说白了,变量就是一块带名字的存储空间,用来装数据的。你可以把它想成快递柜——每个格子都有一个编号(变量名),里面放着包裹(数据),格子有大有小(数据类型)。
为啥要用变量?因为程序要干活,就得记住东西。比如你写一个计算用户年龄的程序,得先有个地方存"出生年份"和"当前年份",算完再把"年龄"存起来。这"先存、再算、后取"的过程,靠的就是变量。
Go 里声明变量有三种姿势,挑你最顺手的用。
第一种,var关键字,最正经的写法:
packagemainimport"fmt"funcmain(){// 声明一个 int 类型的变量 age,默认值是 0varageintfmt.Println(age)// 输出: 0// 声明 + 初始化,类型写在后面varnamestring="利威尔"fmt.Println(name)// 输出: 利威尔// 也可以一次声明多个var(aint=10bstring="hello"cbool=true)fmt.Println(a,b,c)// 输出: 10 hello true}第二种,省略类型,让 Go 自己猜:
packagemainimport"fmt"funcmain(){// 不写类型,Go 会根据右边的值自动推导varage=25varname="阿伟"varisOK=truefmt.Println(age,name,isOK)// 输出: 25 阿伟 true}第三种,海象运算符:=,Go 的甜点语法:
packagemainimport"fmt"funcmain(){// := 是声明 + 初始化的简写,只能在函数内部用age:=25name:="阿伟"pi:=3.14fmt.Println(age,name,pi)// 输出: 25 阿伟 3.14}这三种姿势,新手记住一个原则就行:函数内部优先用:=,函数外部(全局变量)只能用var。
说到这儿,得提一个 Go 很贴心的设计——零值机制。在别的语言里,没初始化的变量是"未定义"的,访问就报错。Go 不一样:每个变量声明之后,都自动有个默认值:
| 类型 | 零值 |
|---|---|
| int | 0 |
| float | 0.0 |
| bool | false |
| string | “”(空字符串) |
| 指针 | nil |
这个设计的好处是:变量永远不会是"野状态",用起来心里踏实。就像你租了间房子,不管你啥时候搬进去,房子起码是干净的,不是毛坯。
最后聊聊常量和iota:
packagemainimport"fmt"funcmain(){// const 声明常量,运行时不能改constPI=3.14159constNAME="利威尔"// 多个常量一起声明const(StatusOK=200StatusNotFound=404StatusError=500)fmt.Println(PI,NAME)fmt.Println(StatusOK,StatusNotFound,StatusError)}iota是 Go 里专门给常量用的"自增魔法",遇到const就从 0 开始数,每行加 1:
packagemainimport"fmt"const(Sunday=iota// 0Monday// 1Tuesday// 2Wednesday// 3Thursday// 4Friday// 5Saturday// 6)funcmain(){fmt.Println("周一对应数字:",Monday)// 输出: 周一对应数字: 1}这个特性在定义枚举的时候特别爽,不用手动写 0、1、2、3。
五、流程控制 —— 程序的指挥棒
变量会用了,接下来就是控制程序的走向。所谓流程控制,就是告诉程序:啥时候该干、啥时候该停、啥时候该拐弯。
Go 的流程控制特别干净,就三件套:if、for、switch,没有while,没有do-while。有人嫌简陋,我倒觉得挺爽——学一遍就够用一辈子。
if 条件判断:让程序学会"看情况"
if的作用是:满足条件才执行,否则跳过。这跟咱们日常生活一样——“如果下雨,就带伞;否则不带”。程序也得有这种判断力。
packagemainimport"fmt"funcmain(){age:=20// 最基础的 ififage>=18{fmt.Println("你已经成年了")}// if-else 二选一ifage>=18{fmt.Println("可以看成人电影")}else{fmt.Println("未成年人禁止")}// if-else if-else 多条件score:=85ifscore>=90{fmt.Println("优秀")}elseifscore>=80{fmt.Println("良好")}elseifscore>=60{fmt.Println("及格")}else{fmt.Println("不及格,回家种地")}}Go 的if有个骚操作:条件前面可以先执行一个语句,常用来做"初始化 + 判断"一气呵成:
packagemainimport"fmt"funcgetScore()int{return88}funcmain(){// v := getScore() 这一句只在 if 内部有效ifv:=getScore();v>=90{fmt.Println("优秀,分数:",v)}elseifv>=60{fmt.Println("及格,分数:",v)}else{fmt.Println("挂科,分数:",v)}// 这里访问 v 会报错:undefined: v// fmt.Println(v)}这种写法的好处是变量作用域最小化——v 只在 if-else 块里活着,出去了就消失。比 Java 那种"先声明再判断"的啰嗦写法清爽多了。
注意一个 Go 的强制规定:if后的左括号必须和if在同一行。不能换行。这是 Go 的格式化工具强制要求的,写错了直接编译不过:
// 错误写法,编译失败ifage>18{fmt.Println("成年")}// 正确写法ifage>18{fmt.Println("成年")}for 循环:让程序学会"重复劳动"
程序最擅长干的事就是重复——重复 1 万次,重复 100 万次,眼睛都不眨一下。这种事交给人来干,会累死;交给程序,分分钟搞定。
Go 里只有for一种循环,没有while。但for能写出所有循环的效果:
packagemainimport"fmt"funcmain(){// 写法 1:经典 for(像 C 语言)fori:=0;i<5;i++{fmt.Println("第",i+1,"次循环")}// 写法 2:省略初始化和递增(像 while)i:=0fori<3{fmt.Println("i =",i)i++}// 写法 3:无限循环(要手动 break 退出)count:=0for{count++fmt.Println("计数:",count)ifcount>=3{break// 跳出循环}}// 写法 4:for range 遍历集合(下节详细讲)nums:=[]int{10,20,30}forindex,value:=rangenums{fmt.Printf("下标 %d,值 %d\n",index,value)}}循环里有几个关键字必须掌握:
break:跳出整个循环(不干了)continue:跳过本轮,进入下一轮(这轮算了,下一轮再来)goto:跳到指定标签(能不用就不用,写出来容易被人骂)
packagemainimport"fmt"funcmain(){// continue 演示:只打印偶数fori:=1;i<=5;i++{ifi%2!=0{continue// 奇数跳过}fmt.Println(i)// 输出: 2 4}// 多层循环里,break 只能跳出当前层// 想跳外层,需要用标签outer:fori:=0;i<3;i++{forj:=0;j<3;j++{ifi==1&&j==1{breakouter// 直接跳出整个外层循环}fmt.Printf("i=%d, j=%d\n",i,j)}}}switch 分支:多选一,比 if-else 链清爽
当你面对一堆"如果……否则……"的时候,写一长串if-else if-else看着就头大。这时候switch就派上用场了——多选一,干干净净。
packagemainimport"fmt"funcmain(){// 基础用法:判断变量等于某个值day:=3switchday{case1:fmt.Println("周一")case2:fmt.Println("周二")case3:fmt.Println("周三")case4:fmt.Println("周四")case5:fmt.Println("周五")default:fmt.Println("周末")}// 多个值合并一个 casescore:="B"switchscore{case"A","a":// A 或 a 都走这里fmt.Println("优秀")case"B","b":fmt.Println("良好")case"C","c":fmt.Println("及格")default:fmt.Println("不及格")}}Go 的switch有个反人类又人性化的设计:默认不穿透。
啥意思呢?别的语言(C、Java)里,case匹配上之后会继续往下跑,除非你写break。这就导致一堆 bug——忘了写 break,整个流程乱套。
Go 干脆反着来:默认不穿透,匹配上就结束。要是想让 Go 像别的语言那样"穿透",得显式写fallthrough:
packagemainimport"fmt"funcmain(){num:=2switchnum{case1:fmt.Println("一")// 这里不会自动落到 case 2,匹配完就结束case2:fmt.Println("二")fallthrough// 强制穿透,执行下一个 casecase3:fmt.Println("三")}// 输出: 二// 三}还有一种不带表达式的 switch,直接当 if-else 链用:
packagemainimport"fmt"funcmain(){score:=85switch{casescore>=90:fmt.Println("优秀")casescore>=80:fmt.Println("良好")casescore>=60:fmt.Println("及格")default:fmt.Println("不及格")}}这种写法的好处是条件可以写任意布尔表达式,不用局限于"等于某个值"。
六、函数 —— 代码的乐高积木
变量、流程控制都学会了,就能写点小段代码了。但代码一长,全堆在一个main里就乱套了——几百行下来,眼睛都看花。
这时候就得把代码拆。怎么拆?用函数。
函数,说白了就是一段打包好的代码块,给它起个名字,下次想用就直接喊名字。可以理解为乐高积木:每个函数是一块积木,复杂的程序就是用积木拼出来的。
定义和调用函数
packagemainimport"fmt"// 定义一个最简单的函数:无参数、无返回值funcsayHello(){fmt.Println("你好,世界")}// 有参数、有返回值的函数funcadd(aint,bint)int{returna+b}// 同类型参数可以合并写funcsub(a,bint)int{returna-b}funcmain(){sayHello()// 调用,输出: 你好,世界result:=add(3,5)fmt.Println("3 + 5 =",result)// 输出: 3 + 5 = 8fmt.Println("10 - 3 =",sub(10,3))// 输出: 10 - 3 = 7}注意几个细节:
- 多个参数类型相同时,可以像
sub(a, b int)这样合并写 - 返回值类型写在参数列表后面
return后面的值就是函数的结果
多返回值 —— Go 的杀手锏
别的语言(C、Java)一个函数只能返回一个值。要返回多个,得搞"out 参数"或者定义结构体,又丑又麻烦。
Go 直接让你一次返回多个值,而且语法干净得不行:
packagemainimport"fmt"// 经典的错误处理模式:返回结果 + 错误funcdivide(a,bint)(int,error){ifb==0{return0,fmt.Errorf("除数不能为零")}returna/b,nil}funcmain(){// 调用方要接收多个返回值result,err:=divide(10,2)iferr!=nil{fmt.Println("出错啦:",err)return}fmt.Println("结果:",result)// 输出: 结果: 5// 错误情况result2,err2:=divide(10,0)iferr2!=nil{fmt.Println("出错啦:",err2)return}fmt.Println("结果:",result2)}这种"返回值 + error"的模式是 Go 的招牌写法,后边讲错误处理的时候会再细说。这里你只要记住:Go 函数可以返回多个值。
如果某个返回值你用不到,可以用_(下划线)丢弃:
packagemainimport"fmt"funcgetUser()(string,int,string){return"利威尔",25,"程序员"}funcmain(){// 只想要名字和职业,年龄不关心name,_,job:=getUser()fmt.Println(name,job)// 输出: 利威尔 程序员}可变参数 —— 数量不固定的参数
有时候你不知道调用方会传几个参数进来,比如计算一堆数字的总和。这时候用可变参数:
packagemainimport"fmt"// nums ...int 表示接收任意数量的 int 参数funcsum(nums...int)int{total:=0for_,n:=rangenums{total+=n}returntotal}funcmain(){fmt.Println(sum(1,2,3))// 输出: 6fmt.Println(sum(10,20,30,40))// 输出: 100fmt.Println(sum())// 输出: 0// 传一个切片进去,要在后面加 ...nums:=[]int{1,2,3,4,5}fmt.Println(sum(nums...))// 输出: 15}可变参数的本质就是切片。函数内部,nums就是一个[]int。
函数是一等公民
在 Go 里,函数是一等公民——可以赋值给变量、传给别的函数、当返回值。听起来玄乎,其实就是函数和数字、字符串一样,可以随便搬来搬去。
packagemainimport"fmt"// 定义一个函数类型typeOperationfunc(int,int)int// 接收函数作为参数funccalculate(a,bint,op Operation)int{returnop(a,b)}// 两个具体的操作函数funcadd(a,bint)int{returna+b}funcmul(a,bint)int{returna*b}funcmain(){// 把函数赋值给变量varmyAdd Operation=add fmt.Println(myAdd(2,3))// 输出: 5// 直接把函数当参数传fmt.Println(calculate(10,5,add))// 输出: 15fmt.Println(calculate(10,5,mul))// 输出: 50// 匿名函数:没名字的函数sub:=func(a,bint)int{returna-b}fmt.Println(sub(10,3))// 输出: 7}这种用法在 Go 里非常常见,后边讲接口和并发的时候会大量用到。
闭包 —— 函数带着"行李"到处跑
闭包听着高大上,其实就一句话:函数 + 它引用的外部变量。
packagemainimport"fmt"// 返回一个闭包funccounter()func()int{count:=0// 这个变量被返回的函数"捕获"了returnfunc()int{count++returncount}}funcmain(){c:=counter()fmt.Println(c())// 1fmt.Println(c())// 2fmt.Println(c())// 3// 新建一个 counter,互不影响c2:=counter()fmt.Println(c2())// 1}counter()返回了一个匿名函数,这个匿名函数引用了count变量。每次调用c(),count都会加 1,并且状态被保留下来。
闭包就像是带着行李的出差人员:函数是出差的人,count是他的行李。人走到哪,行李就跟着到哪,不会丢。
defer —— 延迟执行,资源清理神器
defer是 Go 里一个特别实用的关键字:把语句推迟到函数返回前再执行。
packagemainimport"fmt"funcmain(){deferfmt.Println("最后执行")deferfmt.Println("倒数第二执行")fmt.Println("先执行")// 输出顺序:先执行 → 倒数第二执行 → 最后执行}注意,defer是后进先出(LIFO)的——最后 defer 的最先执行。这跟俄罗斯套娃一样,最里层的最先被打开。
为啥要搞这种顺序?因为实际开发中,defer主要用来释放资源——后打开的资源要先关:
packagemainimport"fmt"funcreadFile(){// 假设这是打开文件fmt.Println("打开文件 A")deferfmt.Println("关闭文件 A")fmt.Println("打开文件 B")deferfmt.Println("关闭文件 B")// 业务逻辑...fmt.Println("处理数据")}// 输出顺序:打开 A → 打开 B → 处理数据 → 关闭 B → 关闭 A这就是defer最经典的用法:成对操作,写在一起,开头打开、结尾关闭(用 defer),不怕中间出岔子忘了关。
packagemainimport"fmt"funcmain(){fori:=0;i<5;i++{deferfmt.Println(i)}// 输出顺序:4 3 2 1 0(后进先出)}defer还有几个进阶用法,比如defer后的函数参数会立即求值:
packagemainimport"fmt"funcmain(){i:=0deferfmt.Println(i)// 此刻 i 的值(0)就被锁定了i++// 输出: 0(而不是 1)}但如果defer后面的函数引用了外部变量,那变量的最终值才是被用的值。这块比较绕,实际写代码的时候小心一点就行。
panic 和 recover —— Go 的异常处理
Go 处理异常的姿势和别的语言不一样:没有try-catch,只有panic和recover。
panic是程序崩溃,一般是发生了不可挽回的错误(数组越界、空指针)。recover是接住 panic,让程序不挂掉。
packagemainimport"fmt"funcriskyBusiness(){deferfunc(){// recover 必须在 defer 函数里调用ifr:=recover();r!=nil{fmt.Println("救命,程序要挂了:",r)}}()panic("故意的!")// 触发 panic}funcmain(){riskyBusiness()fmt.Println("程序正常运行结束")// 输出:救命,程序要挂: 故意的!// 程序正常运行结束}但 Go 的哲学是:错误用 error 返回,panic 只用于真正的"不可恢复"场景。日常开发里,大多数情况应该用error,而不是panic/recover。这块下下节会专门讲。
到这里,函数的基础就讲完了。函数是 Go 编程的核心,得多写多练,才能真正用熟。
到这里,Go 的基础语法地基我们就打完了。你现在已经能写出结构完整的 Go 小功能了。下一篇,我们将进入进阶环节,一起啃下 Go 的“核心难点”:切片(Slice)、Map、结构体与接口。点个关注,我们下篇见!