主题
01 语法基础与类型系统
一句话:Go 的语法刻意做小——声明、类型和零值都围绕「显式」设计,编译器不替你做隐式转换,运行时也不用构造函数补齐默认值,代价是样板代码和对类型宽度的持续注意。
适用 Go 1.22+:range 直接遍历整数从 1.22 开始;泛型从 1.18 开始;any 是 1.18 引入的 interface{} 别名。
为什么需要
读 Go 代码时的困惑通常集中在三处:变量到底在哪一层作用域、两个类型为什么不能直接相加、声明后没赋值的变量能不能用。Go 的答案分别是块级作用域、显式转换和零值可用。语法少让格式和工具容易统一,但也意味着这些规则必须一次讲清,否则会在后面每一章反复踩坑。
核心机制
声明:var、:= 与作用域
go
var a int // 零值 0,包级和函数内都能用
var b, c = 1, "x" // 类型由右值推断
d := 3 // 只能函数内;左侧至少有一个新变量:= 在同一个作用域重复声明时,左侧至少要有一个新变量,其余是赋值;进入内层作用域再写 := 就是新变量,会遮蔽外层。包级变量不能用 :=,需要显式类型或右值推断。var 适合包级声明、需要零值、需要写接口类型或稍后赋值的场景。
常量、iota 与无类型常量
go
const (
_ = iota
KB = 1 << (10 * iota)
MB
)iota 在每个 const 块内从 0 递增,每行加一;表达式省略时复用上一行。无类型常量先按高精度计算,赋给变量时才落到具体类型,所以 const x = 1 << 62 本身能编译,但 var y int = x 在 32 位平台会因常量溢出而编译失败。枚举没有类型安全,Status(99) 合法;需要合法性校验就自己写 Valid()。
类型:具名类型、别名与显式转换
go
type Celsius float64 // 新类型,和 float64 不能直接混算
type Alias = float64 // 别名,完全是同一个类型
var t Celsius = 36.5
var f float64 = float64(t) // 跨类型必须显式转换具名类型有自己的方法集,适合表达单位、ID、状态;别名只用于迁移或简化长类型。整数有 int8/16/32/64、uint*,int 和 uint 的宽度随平台(32 或 64 位),uintptr 给 unsafe 用;byte 是 uint8 的别名,rune 是 int32 的别名。浮点默认 float64,float32 只有约 7 位十进制有效数字,金额不要用浮点。
Go 不做隐式转换,是因为混合类型运算的规则会迅速复杂化,而隐式窄化(int64 到 int32)会静默丢数据。把转换写在调用点,读代码时能看见每一次类型变化,代价是样板代码。
零值:声明即可用
| 类型 | 零值 | 是否可直接用 |
|---|---|---|
| 数值 / bool | 0 / false | 是 |
| string | "" | 是 |
| slice / map | nil | 读、len、range 可以;写 map 不行 |
| struct | 各字段零值 | 通常可以 |
| pointer / interface / func / channel | nil | 需要初始化或判空 |
零值可用是一条设计约束:var buf bytes.Buffer、var mu sync.Mutex、var wg sync.WaitGroup 都能直接工作,调用方不需要构造函数。代价是「零值有意义」要靠类型作者保证:time.Time{} 不是「现在」,sql.DB{} 也不能直接用。
string、byte、rune 与 UTF-8
- string 是只读字节序列,
len返回字节数,s[i]是 byte;UTF-8 中文通常占 3 字节,按字节遍历会切坏字符。 for i, r := range s按 rune 解码,i仍是字节下标;按字符计数用utf8.RuneCountInString,随机访问字符先转[]rune。[]byte(s)和string(b)会复制;热路径反复转换要单独优化。- 一个 rune 不等于一个用户看到的字符,emoji 或组合字符可能是多个 rune,截断和终端宽度要按 grapheme cluster 处理。
控制流:少而明确
- 没有三元运算符,需要值就写完整的
if/else,避免嵌套三元。 switch默认不穿透,需要穿透写fallthrough;没有表达式时等价于switch true,适合替代长 if-else。if/switch/for可以带初始化语句,变量只在该块内可见:if v, ok := m[k]; ok { ... }。for统一了 while:for {}、for cond {}、三段式、range;Go 1.22+ 还能for i := range 10。- 循环变量从 Go 1.22 起每次迭代一份;1.21 及更早整个循环共用一个,闭包捕获会得到最后的值。
最小上手
go
package main
import (
"fmt"
"unicode/utf8"
)
type Celsius float64
const (
_ = iota
KB = 1 << (10 * iota)
MB
)
func main() {
var count int
name := "Go"
fmt.Println(count, name, KB, MB) // 0 Go 1024 1048576
s := "hi 我"
fmt.Println(len(s), utf8.RuneCountInString(s)) // 6 4
for i, r := range s {
fmt.Printf("%d:%c ", i, r) // 0:h 1:i 2: 3:我
}
fmt.Println()
var f float32 = 1.1
_ = float64(f) // 跨类型运算必须显式转换
switch name {
case "Go":
fmt.Println("ready")
default:
fmt.Println("other")
}
for i := range 3 {
fmt.Print(i, " ")
}
fmt.Println()
}完整版本见 examples/01_syntax/main.go。运行:go run examples/01_syntax/main.go。
生产动作
- 跨边界显式转换:JSON、数据库、网络协议来的数字先转
int64,再按业务范围检查;不要用int做跨平台协议字段。 - 金额和精确小数用整数最小单位或 decimal 库,不用
float32/64。 - 字符串入口校验
utf8.ValidString;需要按字符处理时转[]rune,截断不要按字节。 - 枚举用具名类型加
Valid()/String(),不要裸int;iota中间插行会改变后续值。 - 检查:
gofmt -l .、go vet ./...、staticcheck ./...、go test ./...固化到 CI。 - 读代码先看声明:包级
var的初始化顺序按依赖关系而非文件顺序,init()尽量少用。
边界
int宽度随平台;序列化和跨语言协议固定用int32/int64。- 浮点不能精确表示
0.1,比较用容差,NaN不等于自身。 len(string)不是字符数,s[i]不是第 i 个字符。- 常量溢出在编译期报错;运行时转换溢出会静默截断,例如
int64转int32。 iota不是枚举约束,依赖具体数值的地方要显式写值。- 零值可用不等于零值有意义:
time.Time{}、sql.DB{}仍需额外初始化。
讲清楚
Go 的语法小,是为了让类型和生命周期在代码里看得见。变量用 var 或 := 声明,:= 至少引入一个新变量,内层同名会遮蔽外层;常量用 iota 生成递增值,但枚举没有类型安全。Go 不做隐式转换,因为混合类型和窄化会静默丢数据,代价是每次跨类型都要写转换。零值可用是另一条主线:数值是 0、字符串为空、slice/map 是 nil,声明后大多能直接用,所以标准库类型常常不需要构造函数;但零值不一定有业务意义。字符串是 UTF-8 字节序列,len 和下标按字节,range 按 rune,中文和 emoji 要特别小心。
适用 Go 1.22+ | 初稿 2026-10