跳到正文

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)会静默丢数据。把转换写在调用点,读代码时能看见每一次类型变化,代价是样板代码。

零值:声明即可用 ​

类型零值是否可直接用
数值 / bool0 / false是
string""是
slice / mapnil读、len、range 可以;写 map 不行
struct各字段零值通常可以
pointer / interface / func / channelnil需要初始化或判空

零值可用是一条设计约束: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

把一门技术讲透到既懂也会用