builtin — Go 预声明标识符
builtin 不是可导入的包,而是 Go 语言预声明的标识符集合。这些标识符在任何位置都可以直接使用,无需导入。
1. 预声明类型
// 数值类型
bool
string
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
byte // = uint8
rune // = int32
float32 float64
complex64 complex128
// 特殊类型
error // interface { Error() string }
any // = interface{}(Go 1.18+)int 和 uint 在 64 位系统上是 64 位,在 32 位系统上是 32 位。需要精确位宽时(文件格式、网络协议、二进制序列化),必须用 int32/int64 等确定宽度的类型——依赖 int 的宽度会让同一个数据结构在不同平台上产生不同的字节布局,这是跨平台持久化格式的经典事故源。
2. 预声明函数
2.1 内存分配:new vs make
这是 Go 新手最容易混淆的两个函数。
| 特性 | new(T) |
make(T, ...) |
|---|---|---|
| 适用类型 | 任意类型 | 仅 slice/map/chan |
| 返回值 | *T(指针) |
T(本身,非指针) |
| 是否初始化 | 零值填充 | 初始化内部数据结构 |
| 使用场景 | 获取一个零值的指针 | 创建可用的 slice/map/chan |
// new — 分配零值 + 返回指针
p := new(int) // p 是 *int,指向的值是 0
*p = 42
// make — 初始化内部结构 + 返回值本身
s := make([]int, 5, 10) // slice: len=5, cap=10
m := make(map[string]int) // map: 可用的空 map
ch := make(chan int, 10) // channel: 缓冲区 10Go 1.26 起 new 的入参可以是一个表达式,用来指定初始值。这在给"指针表示可选字段"的序列化结构赋值时特别方便:
type Person struct {
Name string
Age *int // 可选:nil 表示未知
}
age := yearsSince(birth)
p := Person{Name: "Alice", Age: new(age)} // Age 指向 age 的一份副本易错:new(expr) 只接受一个表达式。new(MyStruct, "a", 1) 是错误写法,编译直接报 “too many arguments for new”。要构造带字段的结构体指针,仍然用 &MyStruct{...}。
2.2 集合操作
// append — 向 slice 添加元素
s := []int{1, 2}
s = append(s, 3, 4) // 可变参数
s = append(s, other...) // 展开另一个 slice
// copy — 复制 slice 元素(返回复制的数量)
dst := make([]int, len(src))
n := copy(dst, src) // n = min(len(dst), len(src))
// delete — 从 map 删除键
delete(myMap, key) // 即使 key 不存在也不报错
// clear — 清空 slice/map(Go 1.21+)
clear(m) // 删除 map 所有键;对 slice 则把所有元素置零值
// len — 返回长度
len(slice) // slice 长度
len(map) // map 键值对数量
len(channel) // channel 缓冲区中的元素数
// cap — 返回容量
cap(slice) // slice 底层数组的容量
cap(channel) // channel 缓冲区大小注意:var m map[string]int 声明的是 nil map,可以读但不能写。写入 nil map 会 panic,必须 make(map[string]int) 才能写。而 nil slice 可以 append,两者行为不同——这是新手最常踩的第一坑。
如果知道 slice 的最终大小,用 make([]T, 0, capacity) 预分配容量,避免 append 过程中反复扩容与拷贝。
2.3 数值内建函数 min / max
Go 1.21 把 min/max 提升为内建函数,接受任意多个可比较(有序)参数,省去手写比较:
a, b, c := 3, 7, 1
fmt.Println(min(a, b, c), max(a, b, c)) // 1 7
// 对浮点,NaN 会"传染":只要有一个参数是 NaN,min 和 max 都返回 NaN。
// (这与 IEEE minNum/maxNum "忽略单个 NaN" 的语义不同,别想当然。)2.4 异常处理:panic / recover
// panic — 立即终止当前 goroutine 的正常执行
func must(i int, err error) int {
if err != nil {
panic(err)
}
return i
}
// recover — 只能在 defer 中捕获 panic
func safeCall(f func()) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic recovered: %v", r)
}
}()
f()
return nil
}陷阱:
recover只有在 defer 中直接调用的函数里才有效。defer func() { recover() }()有效;defer recover()无效(recover 需要运行在被 panic 的函数栈帧的 defer 上下文中)。- 一个 goroutine 的 panic 不会传播到其他 goroutine——它会终止整个进程,除非某处 recover 了。
- 不要用 panic 做常规错误处理。panic 留给程序逻辑不可能恢复的场景(如初始化失败),业务错误一律用返回
error。
2.5 Channel 操作:close
ch := make(chan int, 10)
ch <- 1
close(ch)
// 关闭后:
// - 可以继续接收剩余数据(直到缓冲排空)
// - 接收已关闭且排空的 channel 得到零值 + false
val, ok := <-ch // ok == false 表示已关闭且排空最佳实践:发送方负责关闭 channel,接收方永远不关闭。向已关闭的 channel 发送、或重复 close 都会 panic。close 的作用是给接收方一个"数据流结束"的广播信号,因此只有知道"不再有数据"的那个 goroutine 才有权关闭。
2.6 类型转换
// 数值转换
i := int(3.14) // 截断为 3(不是四舍五入)
f := float64(42) // 显式转换
// 字符串与字节/字符的转换
s := string([]byte{'h', 'e', 'l', 'l', 'o'}) // []byte → string
b := []byte(s) // string → []byte(复制!)
r := []rune(s) // string → rune slice(正确处理 Unicode)[]byte(s) 和 string(b) 都会复制数据,不是零成本转换。Go 1.22 起编译器会在把 []byte 转成 string 后仅用于只读比较等场景时做优化,避免复制。追求零拷贝时标准库用 unsafe.String/unsafe.Slice(Go 1.20),但要保证底层内存生命周期。
3. 预声明常量
true false // bool
iota // 自增计数器(仅 const 块内)
nil // 零值(指针、接口、map、slice、chan、func)iota 详解
const (
_ = iota // 0(跳过)
KB = 1 << (10 * iota) // 1 << 10 = 1024
MB // 1 << 20 = 1048576
GB // 1 << 30
)
// iota 每行递增,每个 const 块开始时重置
const (
A = iota // 0
B // 1
C // 2
)iota 只在 const 块内有效,每个源码行(而非每个常量)递增一次。同一行 const 里的多个表达式共享同一个 iota 值。后续未显式赋值的行会隐式重复上一行的表达式,这正是 KB/MB/GB 能自动递增的原因。
nil 的类型
nil 本身没有类型,它在使用时获得目标类型:
- 指针:
(*int)(nil) - 接口:
(error)(nil) - slice:
([]int)(nil) - map:
(map[string]int)(nil) - chan:
(chan int)(nil) - func:
(func())(nil)
陷阱:var w io.Writer = (*os.File)(nil) 之后 w == nil 为 false。这是 Go 中最经典的坑之一——接口是否为 nil,要求动态类型和动态值都是 nil。
深入理解:接口的 nil 判断
接口在 Go 内部由两部分组成:动态类型 + 动态值。
接口值 = (type, value)
真正的 nil 接口: (nil, nil) → w == nil 为 true
"有类型的 nil": (*os.File, nil) → w == nil 为 false!只有当 type 和 value 都是 nil 时,接口才等于 nil。
// 场景演示
var f *os.File = nil // f 是 nil 指针
var w io.Writer = f // w = (*os.File, nil),不是 nil 接口
fmt.Println(w == nil) // false —— 这就是那个惊天大坑
var w2 io.Writer = nil // w2 = (nil, nil),真正的 nil 接口
fmt.Println(w2 == nil) // true如何正确判断:
// 方法 1:直接和 nil 比较 —— 只对真正的 nil 接口有效
if w != nil {
// 即使底层动态值是 nil,这里也会进入
}
// 方法 2:反射判断 —— 可检测"有类型的 nil"
func isNil(v any) bool {
if v == nil {
return true
}
rv := reflect.ValueOf(v)
switch rv.Kind() {
case reflect.Ptr, reflect.Map, reflect.Slice, reflect.Chan, reflect.Func, reflect.Interface:
return rv.IsNil()
}
return false
}根治办法:别让函数把 nil 具体值赋给接口再返回。
// 容易出错的写法
func GetWriter() io.Writer {
return getFile() // getFile() 返回 nil *os.File → 得到 (*os.File)(nil),非 nil 接口
}
// 正确写法
func GetWriter() io.Writer {
f := getFile()
if f == nil {
return nil // 显式返回 nil 接口
}
return f
}记忆口诀:不要把 nil 指针赋给接口再返回。函数返回接口类型时,若具体类型可能是 nil,应显式 return nil。用静态检查器(如 nilness、golangci-lint 的相关规则)能在 CI 阶段拦住这类问题。
4. 实战建议
new/make记住一句话:要指针用new,要能用的容器用make。实际项目里new用得很少,多数场景直接&T{}取地址更清晰。- 处理跨平台二进制格式,绝不用
int,一律int32/uint64显式定宽。 - 接口 nil 判断是 Go 面试与线上事故的双重高频点——写返回
error的函数时,要么返回真正的nil,要么返回包装了具体错误的变量,别中途把类型化 nil 赋给error变量。 - 预分配容量是 Go 里性价比最高的一类优化,尤其在循环 append 前。