在 Go 里,int、make、len 这些名字几乎天天都在用,所以很多人会默认它们就是关键字。但 Go 刻意没有这样设计。理解这件事,不只是记住一个冷知识,更能帮助你判断哪些名字属于语法本身、哪些只是预先放进作用域的标识符,以及为什么实际项目里不该随手把它们拿来当变量名。
为什么 int 和 make 不是关键字
先看一段容易让人误会、但完全合法的 Go 代码:
package main
import "fmt"
func main() {
int := "hello"
make := func() string { return "world" }
fmt.Printf("int = %s, make() = %sn", int, make())
// 输出:int = hello, make() = world
}
这段代码之所以能通过编译,正是因为 int 和 make 不是关键字。它们虽然有特殊含义,但仍然属于可以被重新声明、也可能被局部遮蔽的名字。
如果放到 C 这类语言里,int 是语法层面的保留字,这样写会直接报错。Go 在这里走的是另一条路:把真正不可替代的语法词保留得很少,把常用类型名和内建函数名放到另一套机制里处理。
关键字和预定义标识符,到底差在哪
要看懂 Go 这套设计,先得把两个概念分开:

- 关键字:语言语法的一部分,用来组成控制流、声明、类型定义等结构。
- 预定义标识符:语言预先提供的名字,包括内建类型、常量和函数,但它们本质上仍是标识符。
两者在使用体验上很像,但地位不同。关键字直接参与语法解析,预定义标识符则更接近“默认已经声明好的名字”。
Go 只有 25 个关键字
Go 一直强调语言核心要小,因此关键字数量压得很低,只有 25 个:
break default func interface select
case defer go map struct
chan else goto package switch
const fallthrough if range type
continue for import return var
这些词构成了 Go 的语法骨架。它们承担的是“句法角色”,例如声明函数、控制流程、定义类型、导入包等,因此不能拿来当普通变量名。
预定义标识符有哪些
相比之下,预定义标识符更多,大约有 30 多个,主要分成三类。
内建类型:
- 基本类型:
int,int8,int16,int32,int64 - 无符号整数:
uint,uint8,uint16,uint32,uint64,uintptr - 浮点数:
float32,float64 - 复数:
complex64,complex128 - 其他:
bool,byte,rune,string,error
内建常量:true, false, iota, nil
内建函数:make, len, cap, new, append, copy, close, delete, panic, recover 等
也就是说,int 和 make 虽然“看起来像关键字”,但在语言分类上并不属于关键字集合。
Go 为什么要这样设计
给语言演进留空间
Go 把很多常用名字放进预定义标识符,而不是关键字,一个重要原因就是保留扩展余地。
关键字一旦加入语言语法,兼容性成本就会明显变高;而预定义标识符可以通过向 universe 块增加声明的方式扩展,语言在演进时更灵活,也更不容易破坏既有代码结构。
这也是 Go 一贯的思路:尽量缩小真正需要写进语法的部分,把更多能力留给更易维护、也更易兼容的层次。
从编译器实现看,这不是“弱化”而是“解耦”
站在编译器的角度,关键字和标识符在词法阶段都会先变成 token。不同之处在于,Go 会在初始化时把这些预定义标识符直接注入符号表,因此开发者使用它们时会感觉“几乎和关键字一样顺手”。
这种做法带来的好处很直接:
- 语言核心语法更稳定,不必因为内建能力调整而频繁修改关键字集合。
- 编译器实现更清晰,很多能力可以通过符号表和后续语义处理完成。
- 后续扩展时,语言设计者拥有更大的兼容空间。
所以这不是单纯为了“关键字更少”,而是把语法和内建能力刻意分层。
实际开发里最该注意什么
能重名,不代表应该重名
既然是预定义标识符,它们就可能被局部变量重新定义,也就会引出典型的变量遮蔽问题:

package main
import "fmt"
func main() {
// 在函数内部重新定义int
int := "hello"
// 后续如果想使用整数类型int,会导致错误
// var num int // 这行会编译错误
fmt.Printf("类型:%T,值:%vn", int, int)
}
这类写法的危险不在于“语法奇怪”,而在于它会直接影响后续语义。你在当前作用域里把 int 当作字符串变量名之后,原本的整数类型名就被遮住了。
因此,实际项目里的建议很明确:不要重新定义这些预定义标识符。即便编译器允许,这样写也会明显降低可读性,排查问题时成本很高。
为什么 make 看起来像函数,却又不完全像函数
make 在 Go 里尤其典型。它表面上是函数调用形式,但本质上是编译器内置函数,也就是常说的 compiler intrinsic。
当编译器遇到 make(Type, args...) 时,通常会经过这样几步:
- 源代码解析:识别
make是特殊的内建操作 - 类型检查:根据参数类型转换为特定内部符号
- 代码生成:将内部符号替换为运行时函数调用
- 运行时执行:调用真正的底层实现函数
也正因为如此,make 能针对 slice、map、chan 执行不同的底层分配与初始化逻辑。它在语法上像普通函数,在实现上却带有明显的编译器特殊处理。
和其他语言相比,Go 的关键字为什么这么少
如果把 Go 放到主流语言里比较,它的关键字数量确实相当克制:
- C (C17): 44个关键字
- Java (SE 17): 67个关键字
- Python 3 (3.10): 38个关键字
- Go (1.18): 25个关键字
关键字少,不只是为了“好记”。更关键的是,Go 希望把语言核心控制在一个较小、较稳定的范围内,让开发者更容易掌握语法边界,同时把复杂度转移到更容易维护的实现层和标准能力层。
回到开头那个问题,int 和 make 不是关键字,看起来像小细节,其实正好体现了 Go 的设计取向:语法尽量简,能力要够用,还要为未来扩展保留余地。这也是为什么 Go 的很多规则初看反直觉,理解之后却会觉得它们相当统一。









