3.1 if 语句
age := 18
if age >= 18 {
fmt.Println("成年")
} else if age >= 12 {
fmt.Println("青少年")
} else {
fmt.Println("儿童")
}
两个 Go 特色:
- 条件不加括号(加了也能编译,但不规范,会被
gofmt警告) - if 可以带初始化语句(Go 特色):
// if 前可以加一个初始化语句,变量作用域只在 if-else 块内
if num := computeScore(); num >= 90 {
fmt.Println("优秀", num)
} else {
fmt.Println("继续努力", num)
}
// num 在这里不可访问(出了 if-else 块就失效)
💡 「变量作用域」是什么意思?为什么出了
{}就访问不到?这个概念非常重要,3.6 节会系统讲透(包括 if/for 作用域、变量遮蔽等新手必踩的坑)。现在先记住:用:=在 if 初始化语句里声明的变量,只在 if-else 链内有效。
对比 Node.js:
// JS: if (age >= 18) { ... } // 必须括号
// Go: if age >= 18 { ... } // 不加括号
// JS: if (1) { ... } // 合法(1 转 true)
// Go: if 1 { ... } // ❌ 编译错误,条件必须是 bool
⚠️ 再次强调:Go 条件必须是 bool 类型,整数不能当布尔用。
3.2 for 循环 —— Go 只有 for,没有 while
Go 把所有循环都统一成了 for。三种形式:
形式一:经典三段式(和 C/Java 一样)
for i := 0; i < 5; i++ {
fmt.Println(i)
}
// 上面的代码等价于
for i := range 5 {
fmt.Println(i)
}
// 如果
形式二:类 while(省略前后)
n := 10
for n > 0 {
fmt.Println(n)
n--
}
// 等价于 JS: while (n > 0) { ... }
形式三:无限循环
for {
fmt.Println("无限循环")
break // 不 break 就真的无限
}
形式四:range 遍历(最重要,类似 for...of)
nums := []int{10, 20, 30} // 切片,下章详讲
for index, value := range nums {
fmt.Printf("索引 %d: 值 %d\n", index, value)
}
// 只要值,丢弃索引(用 _ 占位)
for _, v := range nums {
fmt.Println(v)
}
// 只要索引
for i := range nums {
fmt.Println(i)
}
对比 Node.js:
// JS
for (const v of nums) { } // for...of
nums.forEach((v, i) => { }) // forEach
for (const i in obj) { } // for...in 遍历 key
// Go
for i, v := range nums { } // range 一个,同时拿到索引和值
关键陷阱:range 遍历切片,value 是拷贝
nums := []int{1, 2, 3}
for _, v := range nums {
v *= 10 // ❌ 不会改变 nums,因为 v 是拷贝
}
fmt.Println(nums) // [1 2 3],没变
// 要改原数组:
for i := range nums {
nums[i] *= 10 // ✅ 通过索引修改
}
遍历 map(顺序不保证!)
m := map[string]int{"a": 1, "b": 2, "c": 3}
for k, v := range m {
fmt.Println(k, v) // 顺序可能每次不同!
}
⚠️ Go 的 map 遍历顺序是随机的,这是故意的(防止依赖顺序)。下章详讲。
3.3 switch —— 默认不穿透
day := "Monday"
switch day {
case "Saturday", "Sunday": // 多个值用逗号
fmt.Println("周末")
case "Monday", "Tuesday", "Wednesday", "Thursday", "Friday":
fmt.Println("工作日")
default:
fmt.Println("未知")
}
Go switch 的三大特色:
- 默认不穿透(不需要写 break!)—— 和 PHP/Java/C 完全相反
- 要穿透就写
fallthrough(极少用) - case 可以是任意类型、任意表达式(不限于常量)
// case 可以是表达式
score := 85
switch {
case score >= 90:
fmt.Println("A")
case score >= 80:
fmt.Println("B")
case score >= 60:
fmt.Println("C")
default:
fmt.Println("D")
}
// 等价于一长串 if-else if,但更清晰
对比 PHP:
// PHP: 每个 case 必须写 break,否则穿透
switch ($day) {
case "Monday": ...; break; // 不写 break 会穿透到下一个
}
// Go: 默认不穿透
switch day {
case "Monday": ... // 自动 break,不会穿透
}
3.4 函数 —— 多返回值是 Go 的标志
函数声明语法
// 基本函数
func add(a int, b int) int {
return a + b
}
// 参数类型相同时可以简写
func add(a, b int) int {
return a + b
}
// 调用
sum := add(3, 5)
对比 Node.js 箭头函数:
// JS
const add = (a, b) => a + b
// Go
func add(a, b int) int { return a + b }
语法顺序不同(Go 是 func 名(参数) 返回类型),但概念一样。
多返回值(Go 标志性特性)
这是 Go 和 PHP/JS 最大的差异之一。Go 函数可以返回多个值:
// 返回商和余数
func divmod(a, b int) (int, int) {
return a / b, a % b
}
q, r := divmod(17, 5)
fmt.Println(q, r) // 3 2
最常见的用法:返回结果 + 错误(这是 Go 的标志性模式,第 07 章会系统讲错误处理):
简单说:
error是 Go 内置的「错误类型」,nil表示没错误。函数把「成功的结果」和「错误」一起返回,调用方通过检查err != nil判断是否出错。
func parseAge(s string) (int, error) {
n, err := strconv.Atoi(s)
if err != nil {
return 0, err // 出错:返回零值 + 错误
}
return n, nil // 成功:返回结果 + nil(表示没错误)
}
age, err := parseAge("28")
if err != nil { // 检查有没有出错
fmt.Println("解析失败:", err)
return
}
fmt.Println(age)
用 _ 丢弃某个返回值:
q, _ := divmod(17, 5) // 只要商,不要余数
_, r := divmod(17, 5) // 只要余数
对比 Node.js:JS 函数只能返回一个值,要返回多个得用数组或对象解构:
// JS 的模拟多返回值(不优雅)
function divmod(a, b) {
return [Math.floor(a/b), a%b]
}
const [q, r] = divmod(17, 5)
// Go 的多返回值是语言原生支持,类型明确、性能更好
命名返回值
返回值可以命名,相当于在函数开头声明了变量:
func divide(a, b int) (quotient int, remainder int) {
quotient = a / b
remainder = a % b
return // 裸 return,自动返回命名返回值
}
return 后不写值,会自动返回命名返回值。但大型函数里用裸 return 容易混乱,建议小函数用,大函数显式 return。
可变参数
func sum(nums ...int) int { // nums 是 []int 切片
total := 0
for _, n := range nums {
total += n
}
return total
}
sum(1, 2, 3) // 6
sum(1, 2, 3, 4, 5) // 15
// 传一个切片
nums := []int{1, 2, 3}
sum(nums...) // 注意三个点,展开切片
对比 Node.js:
// JS
function sum(...nums) { } // rest 参数
sum(...[1, 2, 3]) // spread 展开
// Go
func sum(nums ...int) { } // 可变参数
sum(nums...) // 展开切片
几乎一样的概念。
3.5 闭包与匿名函数
Go 的函数是一等公民,可以赋值给变量、作为参数传递、作为返回值。
3.5.1 ⚠️ 先记住一个限制:函数内部不能声明「命名函数」
从 JS/PHP 转 Go 的人,第一个会撞上的限制是这个:
// JS:函数内部定义命名函数,完全合法
function outer() {
function inner() { // ← 命名函数,内部可见
console.log("inner")
}
inner() // ← 能调用
}
// ❌ Go 不允许!编译错误
func outer() {
func inner() { // 错误:函数内部不能声明命名函数
fmt.Println("inner")
}
inner()
}
Go 的规则:func 名字() {} 这种命名函数声明只能写在包级别(函数外面),不能嵌套在函数里。
那函数内部要定义可复用的函数怎么办? 用匿名函数赋值给变量:
// ✅ Go 的做法:匿名函数赋给变量
func outer() {
inner := func() { // 匿名函数,赋给变量 inner
fmt.Println("inner")
}
inner() // ← 能调用,和命名函数效果一样
inner() // ← 能反复调用
}
为什么 Go 这么设计? 简化作用域规则、防止函数嵌套地狱(像 JS 回调地狱)、倡导「小函数平铺组合」。实际开发里你很少需要嵌套命名函数,用匿名函数赋值完全够用。
💡 这条限制也解释了为什么 Go 代码看起来「扁平」——所有函数都平铺在包级别,不层层嵌套。
3.5.2 匿名函数
// 赋值给变量
greet := func(name string) string {
return "Hello, " + name
}
fmt.Println(greet("Go"))
// 立即调用(IIFE,执行一次就丢弃)
result := func(x int) int { return x * 2 }(5) // 10
闭包(捕获外部变量):
func counter() func() int {
count := 0
return func() int {
count++
return count
}
}
c := counter()
fmt.Println(c()) // 1
fmt.Println(c()) // 2
fmt.Println(c()) // 3
对比 Node.js:
// JS 完全一样的写法
const counter = () => {
let count = 0
return () => ++count
}
const c = counter()
c() // 1
闭包概念可迁移,只是语法不同。
3.6 变量作用域与遮蔽(重要)
前面 3.1~3.5 节多次出现「作用域」这个词(比如 if 的初始化语句、循环变量),本节系统讲透。这是新手最容易踩坑的地方之一。
3.6.1 先分清两个概念:作用域 vs 可见性
很多人(包括很多教程)把这两个混在一起讲,其实它们是两回事:
| 作用域(Scope) | 可见性(Visibility) | |
|---|---|---|
| 回答的问题 | 变量在哪些代码区域能被访问? | 标识符能不能被别的包访问? |
| 维度 | 横向(代码块层级) | 纵向(包层级) |
| 决定因素 | 变量声明在哪对 {} 里 |
标识符首字母大小写 |
| 谁讲的 | 本节 | 第 05、09 章 |
举个对照例子:
package main
import "fmt"
var GlobalCount = 0 // 包级变量:作用域=整个包;可见性=公开(大写)
func main() {
local := 1 // 局部变量:作用域=main 函数内;可见性=私有(小写,但其实无意义,因为别的包看不到 main 内部)
fmt.Println(local, GlobalCount)
}
本节只讲作用域(横向的代码块层级)。
3.6.2 块作用域 {}
Go 用 {} 划分作用域。一对 {} 就是一个块,块内声明的变量只在块内有效:
func main() {
x := 1
fmt.Println(x) // ✅ 1
{ // 内层块开始
y := 2
fmt.Println(x, y) // ✅ 外层变量内层能用
} // 内层块结束,y 的生命期到此为止
fmt.Println(x) // ✅ 1
// fmt.Println(y) // ❌ 编译错误:y 在这里不存在
}
规则一句话:内层块能访问外层块变量,外层块访问不到内层块变量。
3.6.3 if 语句的作用域
3.1 节提到的「if 带初始化语句」,那个变量的作用域只在整个 if-else 块内:
if num := computeScore(); num >= 90 { // num 在这里声明
fmt.Println("优秀", num) // ✅ num 能用
} else {
fmt.Println("继续努力", num) // ✅ num 在 else 里也能用
}
// fmt.Println(num) // ❌ 出了 if-else,num 就不存在了
注意 num 的作用域是整个 if-else 链(包括 else 分支),但出了右大括号就失效。
3.6.4 for 循环变量的作用域
循环变量 i 只在 for 循环内有效:
for i := 0; i < 3; i++ {
fmt.Println(i) // ✅ 0, 1, 2
}
// fmt.Println(i) // ❌ i 出了循环就不存在
// 每次循环,i 是「新的」吗?
// Go 1.22+ :是,每次循环 i 是独立的新变量(重要!影响 goroutine,见 3.6.7)
// Go 1.21 及之前:不是,所有循环共享同一个 i
这涉及一个经典并发 bug(3.6.7 节详讲)。
3.6.5 函数作用域
函数的参数和内部变量只在函数内有效,函数返回后就消失(除非被闭包捕获):
func add(a, b int) int {
result := a + b // result 只在 add 内有效
return result
}
func main() {
sum := add(2, 3)
fmt.Println(sum) // 5
// fmt.Println(result) // ❌ result 是 add 的局部变量,外部访问不到
}
3.6.6 包级作用域
在函数外面声明的变量,作用域是整个包,包内所有函数都能访问:
package main
import "fmt"
var globalCount = 0 // 包级变量,整个 main 包内可见
func increment() {
globalCount++ // ✅ 函数内能访问包级变量
}
func main() {
increment()
increment()
fmt.Println(globalCount) // 2
}
⚠️ 但是! 还记得你之前问的问题吗——「为什么 c = true 写在函数外报错」?
原因正是:函数外只能有「声明」,不能有「语句」。包级作用域只接受声明:
// 函数外(包级):
var c bool = false // ✅ 声明,合法
c = true // ❌ 赋值语句,非法(语句只能写在函数里)
如果你想「先声明一个值,启动时再改它」,把修改放到 init() 或 main() 里(详见 3.7 节 init 函数)。
3.6.7 ⚠️ 变量遮蔽(Shadowing)—— 必须懂的坑
内层作用域声明了和外层同名的变量,外层变量被「遮住」看不见了。
还记得你之前那行被注释的代码吗?
var b string = "8888" // 外层 b(包级)
func main() {
// var b = 9999 // 你注释掉的这行 —— 如果不注释,会怎样?
fmt.Println(b)
}
取消注释后,函数内的 b 会遮蔽(shadow)包级的 b:
- 函数内看到的是新的
b(int 型 9999) - 包级的
b(string 型 "8888")在函数内完全访问不到了
⚠️ 更隐蔽的坑:用 := 时,只要左边有一个新变量,整个赋值会用 :=;但如果左边都是已存在的变量,就是普通赋值 =:
func main() {
count := 0
if true {
count := 10 // ⚠️ 这是「声明新的 count」,不是「修改外层 count」!
fmt.Println(count) // 10(内层 count)
}
fmt.Println(count) // 0(外层 count 没被改!)
}
新手以为 count := 10 修改了外层 count,其实是声明了一个同名新变量。外层 count 还是 0。
怎么避免?
- VSCode 装
gopls后,开启 shadow 检查("gopls": {"analyses": {"shadow": true}}) - 用
go vet -shadow检查 - 命名时避免内外层同名
3.6.8 ⚠️ 闭包 + 循环变量的经典 bug
这是 Go 历史最著名的坑,几乎每个 Go 开发者都踩过:
// ❌ Go 1.21 及之前的坑
funcs := []func(){}
for i := 0; i < 3; i++ {
funcs = append(funcs, func() {
fmt.Println(i) // 闭包捕获的是「i 这个变量」
})
}
for _, f := range funcs {
f() // 输出:3 3 3(不是 0 1 2!)
}
为什么? Go 1.21 及之前,整个 for 循环共享同一个 i。三个闭包捕获的都是同一个 i,循环结束时 i = 3,所以全打印 3。
Go 1.22+ 修复了这个问题:每次循环 i 都是独立的新变量,上面的代码会正确输出 0 1 2。
但为了兼容旧代码,以及写更清晰的代码,推荐传参数:
// ✅ 安全写法:把 i 作为参数传入
for i := 0; i < 3; i++ {
funcs = append(funcs, func(n int) func() {
return func() {
fmt.Println(n) // n 是参数,每次独立
}
}(i))
}
这个 bug 在第 08 章 goroutine 里会再遇到(goroutine + 循环变量),现在先有个印象。
3.6.9 作用域速查表
| 变量声明位置 | 作用域范围 | 能在哪访问 |
|---|---|---|
函数外(包级 var) |
整个包 | 包内所有函数 |
| 函数内 | 当前函数 | 函数内 |
{} 块内 |
当前块 | 块内(含子块) |
if x := f(); ... |
整个 if-else 链 | if 和 else 分支 |
for i := ... |
循环体 | 循环内 |
switch x := ... |
switch 块 | case 分支 |
💡 记忆口诀:
- 变量只在它诞生的那对
{}里活着,出了右大括号就消失- 内层能看外层,外层看不到内层
- 内外层同名 = 遮蔽,外层被隐藏(坑!)
3.7 init 函数 —— Go 特有
每个包可以有一个或多个 init() 函数,在 main 之前自动执行:
package main
import "fmt"
func init() {
fmt.Println("init 函数先执行")
}
func main() {
fmt.Println("main 函数后执行")
}
// 输出:
// init 函数先执行
// main 函数后执行
执行顺序:变量声明 → init() → main()。多个文件的 init 按文件名字母序执行(一般不依赖这个顺序)。
init 的实际用途(什么时候该用它)
用途一:初始化包级变量(最常见)
有些全局变量不能直接声明赋值(要计算、要查环境变量),用 init 最合适:
var config Config
func init() {
// 从环境变量读配置,赋给 config
config.Port = os.Getenv("PORT")
config.DBUrl = os.Getenv("DB_URL")
// 如果直接 var config = loadConfig() 也能做,但 init 里能处理错误、打印日志
log.Println("配置加载完成:", config.Port)
}
用途二:自动注册(框架/驱动常用,最经典的场景)
Go 的数据库驱动、模板引擎都用 init「自注册」。原理:驱动的 init 函数把自己注册到一个全局表里,使用方只需下划线导入(见 9.3 节),不用手动调用:
// 假设这是 mysql 驱动包内部
package mysql
import "database/sql"
func init() {
// 把自己注册到 database/sql 的驱动表里
sql.Register("mysql", &MySQLDriver{})
}
使用方只要 _ "github.com/go-sql-driver/mysql"(下划线导入),这个 init 就会执行,驱动就注册好了。这是 Go 生态的核心机制——第 09 章 9.3 节会详细讲下划线导入。
用途三:校验前置条件
程序启动时检查必需的环境、配置,不满足直接 panic(fail fast):
func init() {
if os.Getenv("API_KEY") == "" {
log.Fatal("必须设置 API_KEY 环境变量") // 启动失败比运行时报错好
}
}
init 的规则
- 不能手动调用(
init()写在代码里会报错),只能由运行时自动执行 - 没有参数、没有返回值
- 一个文件可以有多个 init,按顺序执行
- 一个包可以有多个 init(跨文件)
💡 什么时候用 init vs 直接在 main 里做?
- 只属于本包的初始化(注册、配置)→ 用 init
- 整个程序的启动流程(连数据库、起服务)→ 放 main 里
- 经验:业务代码少用 init,库和框架多用 init
3.8 Node.js / PHP ↔ Go 对照表
| 概念 | Node.js / PHP | Go | 备注 |
|---|---|---|---|
| if 条件 | if (x) |
if x |
Go 不加括号,条件必须 bool |
| while 循环 | while (x) |
for x {} |
Go 只有 for |
| for...of | for (const v of arr) |
for _, v := range arr |
|
| forEach | arr.forEach(cb) |
for i, v := range arr |
Go 没有 forEach 方法 |
| switch 穿透 | 默认穿透,要 break | 默认不穿透 | Go 默认行为相反 |
| 函数声明 | function f() {} |
func f() {} |
|
| 箭头函数 | (a,b) => a+b |
func(a,b int) int {return a+b} |
Go 更啰嗦 |
| 多返回值 | 用数组/对象解构 | 原生支持 (int, error) |
Go 标志特性 |
| 解构忽略 | [, b] = arr |
_, b := f() |
Go 用下划线 |
| 可变参数 | ...args |
nums ...int |
几乎一样 |
| 闭包 | 支持 | 支持 | 概念可迁移 |
| 立即执行 | (function(){})() |
func(){}() |
Go 也支持 |
💬 评论 0
还没有评论,快来抢沙发吧~