《万字详解 | Go 基础从入门到入土》(基础语法篇)
发布时间:2026/9/28 21:06:13来源:尧图网络
一基础语法篇——变量、流程控制与函数在上一篇《系列序言》中我聊了聊自己自学 Go 的踩坑经历。从本篇开始正式进入硬核干货本期我们先解决“为什么学 Go”的疑惑然后从零开始吃透变量、流程控制与函数这三大语法基石。《系列序言》四、变量 —— 程序的地基砖块不管学啥语言变量都是第一道坎。说白了变量就是一块带名字的存储空间用来装数据的。你可以把它想成快递柜——每个格子都有一个编号变量名里面放着包裹数据格子有大有小数据类型。为啥要用变量因为程序要干活就得记住东西。比如你写一个计算用户年龄的程序得先有个地方存出生年份和当前年份算完再把年龄存起来。这先存、再算、后取的过程靠的就是变量。Go 里声明变量有三种姿势挑你最顺手的用。第一种var关键字最正经的写法packagemainimportfmtfuncmain(){// 声明一个 int 类型的变量 age默认值是 0varageintfmt.Println(age)// 输出: 0// 声明 初始化类型写在后面varnamestring利威尔fmt.Println(name)// 输出: 利威尔// 也可以一次声明多个var(aint10bstringhellocbooltrue)fmt.Println(a,b,c)// 输出: 10 hello true}第二种省略类型让 Go 自己猜packagemainimportfmtfuncmain(){// 不写类型Go 会根据右边的值自动推导varage25varname阿伟varisOKtruefmt.Println(age,name,isOK)// 输出: 25 阿伟 true}第三种海象运算符:Go 的甜点语法packagemainimportfmtfuncmain(){// : 是声明 初始化的简写只能在函数内部用age:25name:阿伟pi:3.14fmt.Println(age,name,pi)// 输出: 25 阿伟 3.14}这三种姿势新手记住一个原则就行函数内部优先用:函数外部全局变量只能用var。说到这儿得提一个 Go 很贴心的设计——零值机制。在别的语言里没初始化的变量是未定义的访问就报错。Go 不一样每个变量声明之后都自动有个默认值类型零值int0float0.0boolfalsestring“”空字符串指针nil这个设计的好处是变量永远不会是野状态用起来心里踏实。就像你租了间房子不管你啥时候搬进去房子起码是干净的不是毛坯。最后聊聊常量和iotapackagemainimportfmtfuncmain(){// const 声明常量运行时不能改constPI3.14159constNAME利威尔// 多个常量一起声明const(StatusOK200StatusNotFound404StatusError500)fmt.Println(PI,NAME)fmt.Println(StatusOK,StatusNotFound,StatusError)}iota是 Go 里专门给常量用的自增魔法遇到const就从 0 开始数每行加 1packagemainimportfmtconst(Sundayiota// 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的作用是满足条件才执行否则跳过。这跟咱们日常生活一样——“如果下雨就带伞否则不带”。程序也得有这种判断力。packagemainimportfmtfuncmain(){age:20// 最基础的 ififage18{fmt.Println(你已经成年了)}// if-else 二选一ifage18{fmt.Println(可以看成人电影)}else{fmt.Println(未成年人禁止)}// if-else if-else 多条件score:85ifscore90{fmt.Println(优秀)}elseifscore80{fmt.Println(良好)}elseifscore60{fmt.Println(及格)}else{fmt.Println(不及格回家种地)}}Go 的if有个骚操作条件前面可以先执行一个语句常用来做初始化 判断一气呵成packagemainimportfmtfuncgetScore()int{return88}funcmain(){// v : getScore() 这一句只在 if 内部有效ifv:getScore();v90{fmt.Println(优秀分数:,v)}elseifv60{fmt.Println(及格分数:,v)}else{fmt.Println(挂科分数:,v)}// 这里访问 v 会报错undefined: v// fmt.Println(v)}这种写法的好处是变量作用域最小化——v 只在 if-else 块里活着出去了就消失。比 Java 那种先声明再判断的啰嗦写法清爽多了。注意一个 Go 的强制规定if后的左括号必须和if在同一行。不能换行。这是 Go 的格式化工具强制要求的写错了直接编译不过// 错误写法编译失败ifage18{fmt.Println(成年)}// 正确写法ifage18{fmt.Println(成年)}for 循环让程序学会重复劳动程序最擅长干的事就是重复——重复 1 万次重复 100 万次眼睛都不眨一下。这种事交给人来干会累死交给程序分分钟搞定。Go 里只有for一种循环没有while。但for能写出所有循环的效果packagemainimportfmtfuncmain(){// 写法 1经典 for像 C 语言fori:0;i5;i{fmt.Println(第,i1,次循环)}// 写法 2省略初始化和递增像 whilei:0fori3{fmt.Println(i ,i)i}// 写法 3无限循环要手动 break 退出count:0for{countfmt.Println(计数:,count)ifcount3{break// 跳出循环}}// 写法 4for range 遍历集合下节详细讲nums:[]int{10,20,30}forindex,value:rangenums{fmt.Printf(下标 %d值 %d\n,index,value)}}循环里有几个关键字必须掌握break跳出整个循环不干了continue跳过本轮进入下一轮这轮算了下一轮再来goto跳到指定标签能不用就不用写出来容易被人骂packagemainimportfmtfuncmain(){// continue 演示只打印偶数fori:1;i5;i{ifi%2!0{continue// 奇数跳过}fmt.Println(i)// 输出: 2 4}// 多层循环里break 只能跳出当前层// 想跳外层需要用标签outer:fori:0;i3;i{forj:0;j3;j{ifi1j1{breakouter// 直接跳出整个外层循环}fmt.Printf(i%d, j%d\n,i,j)}}}switch 分支多选一比 if-else 链清爽当你面对一堆如果……否则……的时候写一长串if-else if-else看着就头大。这时候switch就派上用场了——多选一干干净净。packagemainimportfmtfuncmain(){// 基础用法判断变量等于某个值day:3switchday{case1:fmt.Println(周一)case2:fmt.Println(周二)case3:fmt.Println(周三)case4:fmt.Println(周四)case5:fmt.Println(周五)default:fmt.Println(周末)}// 多个值合并一个 casescore:Bswitchscore{caseA,a:// A 或 a 都走这里fmt.Println(优秀)caseB,b:fmt.Println(良好)caseC,c:fmt.Println(及格)default:fmt.Println(不及格)}}Go 的switch有个反人类又人性化的设计默认不穿透。啥意思呢别的语言C、Java里case匹配上之后会继续往下跑除非你写break。这就导致一堆 bug——忘了写 break整个流程乱套。Go 干脆反着来默认不穿透匹配上就结束。要是想让 Go 像别的语言那样穿透得显式写fallthroughpackagemainimportfmtfuncmain(){num:2switchnum{case1:fmt.Println(一)// 这里不会自动落到 case 2匹配完就结束case2:fmt.Println(二)fallthrough// 强制穿透执行下一个 casecase3:fmt.Println(三)}// 输出: 二// 三}还有一种不带表达式的 switch直接当 if-else 链用packagemainimportfmtfuncmain(){score:85switch{casescore90:fmt.Println(优秀)casescore80:fmt.Println(良好)casescore60:fmt.Println(及格)default:fmt.Println(不及格)}}这种写法的好处是条件可以写任意布尔表达式不用局限于等于某个值。六、函数 —— 代码的乐高积木变量、流程控制都学会了就能写点小段代码了。但代码一长全堆在一个main里就乱套了——几百行下来眼睛都看花。这时候就得把代码拆。怎么拆用函数。函数说白了就是一段打包好的代码块给它起个名字下次想用就直接喊名字。可以理解为乐高积木每个函数是一块积木复杂的程序就是用积木拼出来的。定义和调用函数packagemainimportfmt// 定义一个最简单的函数无参数、无返回值funcsayHello(){fmt.Println(你好世界)}// 有参数、有返回值的函数funcadd(aint,bint)int{returnab}// 同类型参数可以合并写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 直接让你一次返回多个值而且语法干净得不行packagemainimportfmt// 经典的错误处理模式返回结果 错误funcdivide(a,bint)(int,error){ifb0{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 函数可以返回多个值。如果某个返回值你用不到可以用_下划线丢弃packagemainimportfmtfuncgetUser()(string,int,string){return利威尔,25,程序员}funcmain(){// 只想要名字和职业年龄不关心name,_,job:getUser()fmt.Println(name,job)// 输出: 利威尔 程序员}可变参数 —— 数量不固定的参数有时候你不知道调用方会传几个参数进来比如计算一堆数字的总和。这时候用可变参数packagemainimportfmt// nums ...int 表示接收任意数量的 int 参数funcsum(nums...int)int{total:0for_,n:rangenums{totaln}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 里函数是一等公民——可以赋值给变量、传给别的函数、当返回值。听起来玄乎其实就是函数和数字、字符串一样可以随便搬来搬去。packagemainimportfmt// 定义一个函数类型typeOperationfunc(int,int)int// 接收函数作为参数funccalculate(a,bint,op Operation)int{returnop(a,b)}// 两个具体的操作函数funcadd(a,bint)int{returnab}funcmul(a,bint)int{returna*b}funcmain(){// 把函数赋值给变量varmyAdd Operationadd 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 里非常常见后边讲接口和并发的时候会大量用到。闭包 —— 函数带着行李到处跑闭包听着高大上其实就一句话函数 它引用的外部变量。packagemainimportfmt// 返回一个闭包funccounter()func()int{count:0// 这个变量被返回的函数捕获了returnfunc()int{countreturncount}}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 里一个特别实用的关键字把语句推迟到函数返回前再执行。packagemainimportfmtfuncmain(){deferfmt.Println(最后执行)deferfmt.Println(倒数第二执行)fmt.Println(先执行)// 输出顺序先执行 → 倒数第二执行 → 最后执行}注意defer是后进先出LIFO的——最后 defer 的最先执行。这跟俄罗斯套娃一样最里层的最先被打开。为啥要搞这种顺序因为实际开发中defer主要用来释放资源——后打开的资源要先关packagemainimportfmtfuncreadFile(){// 假设这是打开文件fmt.Println(打开文件 A)deferfmt.Println(关闭文件 A)fmt.Println(打开文件 B)deferfmt.Println(关闭文件 B)// 业务逻辑...fmt.Println(处理数据)}// 输出顺序打开 A → 打开 B → 处理数据 → 关闭 B → 关闭 A这就是defer最经典的用法成对操作写在一起开头打开、结尾关闭用 defer不怕中间出岔子忘了关。packagemainimportfmtfuncmain(){fori:0;i5;i{deferfmt.Println(i)}// 输出顺序4 3 2 1 0后进先出}defer还有几个进阶用法比如defer后的函数参数会立即求值packagemainimportfmtfuncmain(){i:0deferfmt.Println(i)// 此刻 i 的值0就被锁定了i// 输出: 0而不是 1}但如果defer后面的函数引用了外部变量那变量的最终值才是被用的值。这块比较绕实际写代码的时候小心一点就行。panic 和 recover —— Go 的异常处理Go 处理异常的姿势和别的语言不一样没有try-catch只有panic和recover。panic是程序崩溃一般是发生了不可挽回的错误数组越界、空指针。recover是接住 panic让程序不挂掉。packagemainimportfmtfuncriskyBusiness(){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、结构体与接口。点个关注我们下篇见
网站建设高端定制企业官网