02 词法与语法骨架

Go 的词法要素速查:25 个关键字分组、预声明标识符、字面量写法、运算符优先级、分号插入规则、命名与导出

02 词法与语法骨架

本页是「查写法」的底座:关键字有哪些、字面量怎么写、运算符谁先算、什么名字能被外部包看见。语法结构的语义在 05 语句 与 03 类型 里展开。

基线:Go 1.27.1。所有输出为本机实跑结果。


25 个关键字,一个不多

Go 的关键字总数是 25 个,全部小写,且不能用作标识符。记住这个分组比背列表有用:

mindmap
  root(("25 个关键字"))
    声明
      var
      const
      type
      func
    复合类型
      struct
      interface
      map
      chan
    流程控制
      if
      else
      switch
      case
      default
      for
      range
    跳转
      break
      continue
      goto
      fallthrough
      return
    并发与选择
      go
      select
      defer
    包管理
      package
      import
分组关键字一句话
声明var const type func四种顶层声明形式
复合类型struct interface map chan类型字面量的构造器
条件与分支if else switch case defaultdefault 同时用于 switch 和 select
循环for rangeGo 只有 for 一种循环 🔥
跳转break continue goto fallthrough returnfallthrough 只存在于 switch
并发go select defer三个「动词」,Go 的招牌
包package import每个文件开头两行

容易误记的边界

  • Go 没有 while、do、try、catch、finally、throw、class、extends、implements、public、private、static、new(作为关键字,但有内置函数 new)、delete、typeof、instanceof。
  • true false nil iota 不是关键字,是预声明标识符——意味着你可以给它们重新赋值(虽然绝不该做)🛑:
1
2
3
4
5
6
7
8
package main

import "fmt"

func main() {
	true := "我不是布尔值了"   // 🛑 合法但邪恶:遮蔽了预声明的 true
	fmt.Println(true)
}
1
我不是布尔值了

预声明标识符

这些名字在**宇宙块(universe block)**里预声明,可以随时被局部声明遮蔽。理解这一点,才能解释「为什么 len 能被当成变量名」以及「为什么遮蔽 error 会导致灾难」。

flowchart TB
    U["宇宙块<br/>预声明标识符"]
    U --> T["类型<br/>int string error any bool ..."]
    U --> C["常量<br/>true false iota"]
    U --> Z["零值<br/>nil"]
    U --> F["内置函数<br/>make new len cap append copy delete<br/>panic recover print println<br/>complex real imag<br/>min max clear close"]

    T --> SHADOW["都可以被局部变量遮蔽"]
    F --> SHADOW
    SHADOW --> RISK["⚠️ 遮蔽 error / len / min 会造成<br/>难以定位的 bug"]
类别名字
布尔与数值类型bool byte rune int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 uintptr float32 float64 complex64 complex128 string error any(any = interface{} 的别名 🆕 1.18)
常量true false iota
零值nil
内置函数append cap clear close complex copy delete imag len make max min new panic print println real recover
空白标识符_

⚠️ print/println 是给运行时调试用的,输出格式不保证稳定、且写到 stderr。生产代码一律用 fmt 或 log。

⚠️ min/max/clear 是内置函数 🆕 1.21,所以老代码里叫 min 的变量会在升级后与新内置函数冲突(多数情况仍能编译,但语义可能被遮蔽)。


字面量怎么写

整数:四种进制 + 下划线分隔

1
2
3
4
5
6
n := 1_000_000        // 十进制,下划线只是给人看的 🆕 1.13
b := 0b1011_0010      // 二进制
o := 0o755            // 八进制(新版写法)
old := 0755           // 八进制(旧写法,仍然合法)
h := 0xDEAD_BEEF      // 十六进制
big := 1 << 62        // 位移也是最常见的「写大数」方式
1
1000000 178 493 493 3735928559 4611686018427387904

浮点与虚数

1
2
3
4
5
6
f1 := 3.14
f2 := .5          // 省略整数部分
f3 := 1.          // 省略小数部分
f4 := 1e9         // 科学计数法 → float64
f5 := 1e-3
c := 2 + 3i       // complex128

rune 与字符串:单引号、双引号、反引号

写法类型是否转义用途
'A' '世' '\n'rune(int32)✅单个字符/码点
"a\nb"string✅普通字符串
`a\nb`string❌ 原样多行文本、正则、JSON、SQL 🔥
1
2
3
4
r := 'A'            // rune,值是 65
r2 := '世'          // rune,值是 19990
s := "tab\there"    // 含一个制表符
raw := `tab\there`  // 字面量反斜杠 + t,不转义
1
65 19990 "tab\there" "tab\\there"

转义序列完整表:

转义含义转义含义
\a响铃\'单引号(rune 内)
\b退格\"双引号(string 内)
\f换页\\反斜杠
\n换行\x41十六进制字节(1 字节)
\r回车\u4e16Unicode 码点(4 位十六进制)
\t制表符\U0001F600Unicode 码点(8 位)
\v垂直制表\101八进制字节

⚠️ 字符串可以被索引,但索引单位是字节,不是字符。中文按 UTF-8 占 3 字节,所以 s[0] 拿到的是半个汉字:

1
2
3
4
s := "héllo, 世界"
fmt.Println("len(bytes) =", len(s), " len(runes) =", len([]rune(s)))
fmt.Printf("%q\n", s[0:3])   // 按字节切
fmt.Printf("%q\n", s[1:3])   // 切出合法字符纯属巧合
1
2
3
len(bytes) = 14  len(runes) = 9
"hé"
"é"

想要「第 n 个字符」必须 []rune(s) 或 for range(见 05 range)。


运算符与优先级

Go 的优先级只有 5 档,比 C 简单得多——因为它没有 ?:、没有逗号表达式、没有 ++ 前缀、没有指针算术。

flowchart TB
    P5["⑤ 最高:* / % << >> & &^"] --> P4
    P4["④ + - | ^"] --> P3
    P3["③ == != < <= > >="] --> P2
    P2["② &&"] --> P1
    P1["① 最低:||"]
    NOTE["同级从左到右结合<br/>没有三元运算符 🔥<br/>没有幂运算符 **"]
优先级运算符记忆
5* / % << >> & &^乘除模 + 位运算全家
4+ - | ^加减 + 按位或/异或
3== != < <= > >=比较
2&&短路与
1||短路或

💡 唯一必须记的:Go 里 & ^ | 的优先级高于比较运算——这一点和 C 相反(C 里 == 高于它们)。⚠️ 但不是所有位运算都和 C 相反:C 的 << >> 同样高于比较,与 Go 一致。

1
2
3
4
5
6
const flags uint8 = 4

// Go:先算 &,再算 ==,所以这就是「检查第 2 位」
if flags&0x04 == 0 { /* 不成立 */ } else { /* 走这里 */ }

// C/Java:== 优先级高于 &,同样的表达式含义完全不同 ⚠️
1
bit set
语言flags & 0x04 == 0 的解析结果
Go(flags & 0x04) == 0检查位是否清零 ✅
C / C++flags & (0x04 == 0) → flags & 0恒为 0,判断永远成立 🛑
Javaint & boolean 类型不匹配编译错误 🛑(Java 的 & 两侧必须同为整型或同为 boolean)

C 侧实测(本机 cc):3 << 1 > 5 → 1(移位先算,与 Go 一致);6 & 2 == 2 → 0(== 先算,与 Go 相反)。

即便如此,混合位运算与比较时仍然建议加括号:读代码的人不必回忆优先级表。

1
if (flags&0x04) == 0 { }   // ✅ 推荐:意图无歧义

位运算速查

运算符名称例子(a=0b1100, b=0b1010)结果
&ANDa & b0b1000
|ORa | b0b1110
^XOR(二元)a ^ b0b0110
&^AND NOT(清位)🔥a &^ b0b0100
<<左移a << 20b110000
>>右移a >> 20b0011
^按位取反(一元)^a(对 int 是 -a-1)^0 == -1
1
2
3
var a, b uint8 = 0b1100, 0b1010
fmt.Printf("a&b=%04b a|b=%04b a^b=%04b a&^b=%04b\n", a&b, a|b, a^b, a&^b)
fmt.Printf("a<<2=%04b a>>2=%04b\n", a<<2, a>>2)
1
2
a&b=1000 a|b=1110 a^b=0110 a&^b=0100
a<<2=110000 a>>2=0011

&^(AND NOT)是 Go 独有的清位运算符,等价于 a & (^b),写标志位清除时最省事。

移位的三个注意事项

Go 的移位语义和 C 差得比较远,值得单列:

事项Go 的行为
移位量可以为变量✅ a << n 是合法的(C 在旧标准里也常这样,但 Go 更彻底)
移位量为负💥 运行时 panic:negative shift amount ⚠️
移位量超过位宽合法,结果按类型截断(例如 uint8(1) << 8 == 0)
常量移位溢出🛑 编译错误:constant shift overflow
优先级与 * / % 同级(第 5 档)🔥 比 C 高——C 里 << 低于 +
1
2
3
4
5
var a uint8 = 0b1100
n := -1
fmt.Println(a >> n)     // 💥 panic: runtime error: negative shift amount

fmt.Println(a<<8, a>>8) // 合法:0 0(按 uint8 截断)

⚠️ 移位量来自外部输入(配置、协议字段)时,必须先校验非负:

1
2
3
4
5
6
7
8
9
// 🛑 危险
shift := cfg.ShiftBits        // 可能是 -1
result := value << shift

// ✅ 安全
if cfg.ShiftBits < 0 || cfg.ShiftBits > 31 {
	return fmt.Errorf("invalid shift: %d", cfg.ShiftBits)
}
result := value << cfg.ShiftBits

算术语义的两个反直觉点

1
2
3
fmt.Println(10/3, 10%3)      // 整数除法截断
fmt.Println(-7/2, -7%2)      // 负数除法向零截断
fmt.Println(1.0/3.0, 3.0/2)  // 浮点除法
1
2
3
3 1
-3 -1
0.3333333333333333 1.5
  • 整数除法向零截断:-7/2 == -3(不是 -4),余数符号跟随被除数:-7%2 == -1。
  • 溢出是静默的:整数运算溢出不回绕报错,直接截断。要检测得自己判边界。
  • 除零:整数除零是运行时 panic;浮点除零得到 +Inf。

无类型常量的精度

这是 Go 最优雅的设计之一:字面量常量在赋值或使用之前不占用具体类型,可以保留任意精度。

1
2
3
4
5
6
7
8
const Big = 1 << 62          // 无类型整数常量
fmt.Println(Big)             // 4611686018427387904

f := 1e9                     // 默认类型 float64
i := 1.0 / 3.0
fmt.Printf("%T %v\n", f, f)  // float64 1e+09
fmt.Printf("%T %v\n", i, i)  // float64 0.3333333333333333
fmt.Printf("%T %v\n", 'A', 'A') // int32 65
写法默认类型
整数常量int
浮点常量float64
虚数常量complex128
rune 字面量rune(int32)
字符串字面量string

⚠️ 1e9 的语义需要说准,它比看上去更微妙:

写法结果
x := 1e9x 的类型是 float64 ⚠️
var n int = 1e9✅ 合法:无类型常量,值能被 int 表示就能赋
n + 1e9(n 是 int)✅ 合法:仍然是常量运算,得到 int
f + 1e9(f 是 float64 变量)✅ 合法,得到 float64
f + n(float64 变量 + int 变量)🛑 编译错误:两个变量类型不同

本机实测:n := 5; fmt.Println(n + 1e9) → 1000000005,编译通过。

💡 结论:1e9 是无类型浮点常量,只有在落到变量上时才取默认类型 float64。想明确表达「这是个整数」用 1_000_000_000 或 1 << 30。

类型断言与类型转换的符号区别

1
2
3
4
5
6
7
8
var x any = 5

n, ok := x.(int)      // 类型断言:any 拆回具体类型
m, ok2 := x.(string)  // 断言失败不 panic(带 ok 形式)
fmt.Println(n, ok, m, ok2)

f := float64(n)       // 类型转换:数值类型之间
s := string(rune(65)) // int → string 是「转成对应字符」
1
5 true  false
写法名称用途失败行为
x.(T)类型断言接口 → 具体类型panic ⚠️
x.(T) 带 ok类型断言接口 → 具体类型返回零值 + false
T(x)类型转换数值/字符串/自定义类型编译期检查
string(int)⚠️ 特例转成码点对应的字符不是数字转字符串!

🛑 最常见的错误:string(65) 得到 "A" 而不是 "65"。数字转字符串要用 strconv.Itoa(65)。


分号插入规则

Go 源码里几乎看不到分号,因为词法分析器会自动插入——规则是**「行的最后一个 token 若是某些符号,就在行尾插入分号」**。

flowchart TB
    L["一行结束"] --> Q{"最后一个 token 是?"}
    Q -- "标识符 / 字面量<br/>break continue fallthrough return<br/>++ -- ) ] }" --> INS["插入分号 🔥"]
    Q -- "其他任何 token" --> NO["不插入,继续下一行"]
    INS --> TRAP["⚠️ 于是左大括号不能另起一行"]

这条规则解释了 Go 代码风格里强制的两件事:

1
2
3
4
// ✅ 正确:左大括号与语句同行
func f() int {
	return 1
}
1
2
3
4
5
// 🛑 编译错误:return 后自动加分号,大括号成了下一个语句
func f() int
{
	return 1
}
1
2
3
4
// 🛑 编译错误:if 条件后自动加分号
if x > 0
{
}
会因为换行踩坑的写法正确写法
return 换行后写返回值return expr 写在同一行
x := []int{ 换行……列表末尾 } 换行列表末尾的 } 与最后一项同行,或写 , 🔥
if cond 换行 {if cond {
链式调用把 .Method() 换行开头把点号留在上一行末尾

多行字面量的经典坑:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 🛑 编译错误:最后一行 } 前缺少逗号或同行右括号
xs := []int{
	1,
	2
}

// ✅ 两种正确写法
xs1 := []int{
	1,
	2,        // 尾逗号是关键 🔥
}
xs2 := []int{1, 2}

命名与导出

导出规则:只看首字母

flowchart LR
    N["标识符 Name"] --> Q1{"首字符是<br/>Unicode 大写字母?"}
    Q1 -- 是 --> EXP["导出 ✅<br/>其他包可访问"]
    Q1 -- 否 --> UNEXP["未导出 ❌<br/>仅包内可见"]
    EXP --> E1["变量、常量、函数、类型<br/>结构体字段、方法<br/>都能导出"]
    UNEXP --> E2["_ 开头的更是明确表示内部"]
名字可被 otherpkg 访问?说明
User✅大写开头
user❌小写开头
UserID✅ID 全大写是惯例
UserId✅能编译,但 golint 会唠叨 💭
HTTPServer✅缩写词全大写
utf8Valid❌小写开头,即使含大写字母也没用

⚠️ 「首字母」指 Unicode 大写字母类(Lu),不只是 A–Z。所以中文标识符首字符永远不是大写,永远导不出去。

命名惯例(Go 社区共识,非编译器强制)

对象惯例例子
包名全小写、单个单词、无下划线httputil 而非 http_util
局部变量越短越好,作用域越短名字越短i n err ctx
导出名MixedCaps(驼峰)ReadAll 而非 read_all
缩写词整体大小写一致URL ID HTTP 而非 Url Id
接收者名1–2 个字母,同一类型保持一致func (u *User) Name()
接口(单方法)方法名 + erReader Writer Stringer
错误变量Err 前缀ErrNotFound
测试函数Test + 被测名TestUserCreate

⚠️ 只有测试文件、平台特定文件、构建标记文件才用下划线:user_test.go、syscall_linux.go、foo_amd64.go。业务代码里出现下划线命名会被 go vet 之外的审查者念叨。

一个反直觉的坑:遮蔽预声明名字

1
2
3
4
5
// 🛑 灾难写法:遮蔽了 error 类型
func bad() (result string, error error) { ... }

// ✅ 正确
func good() (result string, err error) { ... }
1
2
3
4
5
6
// ⚠️ 单元测试里极常见的遮蔽
func TestX(t *testing.T) {
	len := computeLen()   // 遮蔽了内置 len
	_ = len
	// 下面想用内置 len 就没法用了
}

完整语法骨架

把 25 个关键字填进骨架,就是一份 Go 源文件的全部形态:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 1. 包声明(每文件必有一行,包名与目录名无强制关系)
package user

// 2. 导入(分组:标准库 / 第三方 空行分隔是惯例)
import (
	"context"
	"fmt"
	"uuid"            // 🆕 1.27:UUID 已进标准库
)

// 3. 常量
const MaxRetry = 3

// 4. 变量
var ErrNotFound = fmt.Errorf("user not found")

// 5. 类型
type User struct {
	ID   string
	Name string
}

type Store interface {
	Get(ctx context.Context, id string) (*User, error)
}

// 6. 方法
func (u *User) String() string { return u.Name }

// 7. 函数
func New(name string) *User {
	return &User{ID: uuid.New().String(), Name: name}
}

// 8. 并发与错误处理在函数体内展开
func (s *memStore) Get(ctx context.Context, id string) (*User, error) {
	select {
	case <-ctx.Done():
		return nil, ctx.Err()
	default:
	}
	u, ok := s.data[id]
	if !ok {
		return nil, ErrNotFound
	}
	return u, nil
}

本页陷阱速查

症状实际原因正确做法
syntax error: unexpected {左大括号另起一行,分号被自动插入大括号与语句同行
missing ',' before newline多行字面量最后一项缺尾逗号加尾逗号
string(65) 得到 "A"string(int) 是码点转换用 strconv.Itoa(65)
a & b == 0 在 Go 里行为对,从 C 迁过来的代码却错Go 位运算优先级高于比较,C 相反混合时加括号:(a & b) == 0
中文变量在其他包访问不到首字符非 Unicode 大写,不可导出用英文大写开头命名
-7/2 结果是 -3 不是 -4整数除法向零截断需要向下取整用 math.Floor
len("中文") 返回 6 不是 2len 数字节用 utf8.RuneCountInString
1e9 与 int 变量混算报错x := 1e9 让 x 变成 float64 变量用 1_000_000_000 或 1 << 30;常量形式 n + 1e9 是合法的
变量 len/min/error 后行为诡异遮蔽了预声明标识符改名字,别用内置名做变量

📘 官方参考:The Go Programming Language Specification — Lexical elements、Effective Go、Go Code Review Comments

➡️ 上一节:01 起步与工具链 | 下一节:03 类型与零值

最后修改 September 21, 2026: 更新 (ac821931b)