Skip to content

Go 类型系统 ​

写 Go 时,选对类型只是第一步。接下来还要知道:把一个值交给函数,函数能不能改到调用方的数据?一个容器看起来是空的,能不能直接往里放东西?一段中文的长度,到底应该怎么算?这篇把基本类型、数组、slice、map 和字符串放在一起讲,重点是把这些行为判断清楚。

先认一下各自的用途。表中的“复制”指普通赋值和参数传递,不是序列化,也不是递归复制所有关联对象。Go 传参会复制参数值;值里包含引用时,复制之后仍可能访问同一份数据。[1]

想表达什么常用类型赋值后要注意什么
真或假、计数、近似数值bool、整数、浮点数复制当前数值;转换类型时还要检查范围
长度本身就是约束,例如 32 字节摘要[32]byte整个数组值被复制
元素数量会变的同类序列[]T,即 slice复制的是切片描述,底层数组可能共享
根据键找值map[K]V两份 map 值仍访问同一个映射
不可修改的文本或字节内容string字符串内容不可修改,len 按字节计算
要修改的字节缓冲区[]byte它就是 slice,同样有共享和扩容问题

下面的输出来自 2026-10-13 在 Go 1.27.2、Linux/amd64 上的实际运行。语言规则依据官方规范;扩容容量等实现结果单独标出,不把它们当成跨版本承诺。文末有全部代码、测试和故意编译失败的反例,复制到一个目录即可复现。

基本类型:先让编译器知道这个值代表什么 ​

常用类型怎样选 ​

Go 的预声明基本类型如下。byte 和 rune 是别名,分别与 uint8、int32 是同一个类型;名字不同,是为了提示读代码的人,这里处理的是字节还是 Unicode 码点。[2]

类别类型要记住的区别
布尔bool只有 true 和 false,不能拿整数代替条件
有符号整数int8、int16、int32、int64固定位数,既能表示负数也能表示非负数
无符号整数uint8、uint16、uint32、uint64固定位数,只表示非负数;例如 uint8 的范围是 0~255
常用整数int、uint宽度由实现决定,为 32 或 64 位;len、cap 返回 int
浮点数float32、float64IEEE 754 二进制浮点数,用于允许舍入的计算
复数complex64、complex128实部、虚部分别采用 float32 或 float64
字符串string不可变的字节序列,零值是 ""
字节、码点byte、rune分别是 uint8、int32 的别名
指针位模式用整数uintptr能容纳指针的位模式;它仍是整数,不是保活对象的指针

通常的计数、下标用 int 比较顺手。文件格式或网络协议明确规定了 32 位、64 位时,用固定位宽类型,不把本机的 int 宽度当成协议。这是依据类型范围给出的选择建议。精确金额也需要先规定单位、范围和舍入方式;把金额写成 float64,不会让十进制小数自动变得精确。[2:1][3]

不同数值类型不能因为“都装着数字”就直接混用:int 变量和 int64 变量相加会编译失败。字面量的情况稍有不同,var n int64 = 1 可以成立,因为未指定类型的常量 1 能由目标类型表示。n := 1 没有这个目标类型,整数常量便采用默认的 int。[4]

显式转换也不等于检查范围。

go
n := 257
fmt.Println(uint8(n))
text
1

n 是运行时的整数值,转成 8 位无符号整数时只留下低 8 位,257 就变成了 1。想拒绝超范围输入,应先判断 n < 0 || n > 255。但若直接写 uint8(257),257 仍是常量,编译器会拒绝不能由 uint8 表示的常量转换。文末反例的实际诊断是 constant 257 overflows uint8。两者差别在“常量能否表示”和“非常量整数怎样转换”,并不矛盾。[5]

定义类型与别名解决的问题不同 ​

go
type UserID int64
type IDAlias = int64

var id UserID = 42
var alias IDAlias = 42
fmt.Printf("%T %T\n", id, alias)
text
main.UserID int64

type UserID int64 创建一个新的定义类型,底层类型是 int64。再定义一个 OrderID int64,便不能把 UserID 变量直接赋给 OrderID 变量;要互转必须明确写出来。用户 ID 和订单 ID 都用裸 int64 时,编译器分不清它们,定义类型能帮忙挡住一部分参数传错。它不会检查 ID 是否真实存在,这仍是业务逻辑的工作。[6]

type IDAlias = int64 只给原类型另起一个名字,输出仍是 int64。它适合保持原有类型身份的场景,例如迁移 API 的类型名。若目的是区分两种业务含义,别名达不到这个效果。[6:1]

零值是起点,不保证所有操作都能做 ​

没有写初始值的变量也有确定的值:数值是 0,布尔是 false,字符串是空串;指针、slice、map、函数、channel、接口是 nil。数组和结构体则逐个元素、字段取零值。[7]

go
var enabled bool
var title string
var ptr *int
fmt.Printf("%t %q %t\n", enabled, title, ptr == nil)
text
false "" true

因此不必为了“清空”整数再赋一次 0。但 nil 指针不能随意解引用,nil map 也不能直接插入。后面用 slice 和 map 对照看,能看出“零值可读”和“零值可写”并不是一回事。

数组与 slice:固定的一组元素,和查看数组的一段范围 ​

先把两个容易混用的概念放到同一张表里。[8][9]

比较项数组 [N]Tslice []T
长度是不是类型的一部分是,[3]int 和 [4]int 类型不同不是,不同长度都可以是 []int
值包含什么N 个元素指向底层数组某处的引用、长度、容量
普通赋值复制什么每个元素的值切片描述,通常继续共享元素
长度能否改变不能可以重新切片,或接收 append 的返回值
零值N 个元素各取零值nil,长度和容量都是 0
能否用 == 比较两份元素类型可比较时可以不可以,只能与 nil 比较

同样传入函数,为什么修改结果不同 ​

go
func changeArray(a [3]int) { a[0] = 9 }

func changeSlice(s []int) {
    s[0] = 9
    s = s[:1]
    fmt.Println("inside:", s)
}

a := [3]int{1, 2, 3}
changeArray(a)
fmt.Println("array:", a)

s := []int{1, 2, 3}
changeSlice(s)
fmt.Println("outside:", s)
text
array: [1 2 3]
inside: [9]
outside: [9 2 3]

数组参数 a 是三个整数的副本,改副本的第一个整数不会改到原数组。slice 参数也是按值传递,但这次复制的是描述同一段底层数组的信息。s[0] = 9 沿着引用找到共享元素,因此调用方能看到 9;s = s[:1] 只把函数里的描述改短,所以调用方的长度仍为 3。[1:1][9:1]

下面画的是调用期间的关系。两个 slice 描述是分开的,指向的数组却是同一个;它不是“整个 slice 按引用传递”。图中的三部分对应本次 Go 运行时的 slice 表示,程序行为仍应以语言规范为准。[10]

正在绘制图示…

可切换适合窗口或原始尺寸
查看图源代码
flowchart LR
    C["调用方 s<br/>起点 0 · len 3 · cap 3"] --> A["同一个底层数组<br/>0:9 · 1:2 · 2:3"]
    F["函数参数 s<br/>起点 0 · len 1 · cap 3"] --> A
放大图示

原始尺寸下可横向或纵向滚动;按 Escape 关闭。

正在绘制图示…

查看图源代码
flowchart LR
    C["调用方 s<br/>起点 0 · len 3 · cap 3"] --> A["同一个底层数组<br/>0:9 · 1:2 · 2:3"]
    F["函数参数 s<br/>起点 0 · len 1 · cap 3"] --> A

数组也不保证“复制之后整个对象树独立”。若元素是指针或 slice,数组复制的是这些元素的值,其中的引用仍可能共享。判断共享时,要继续看元素是什么类型。[1:2]

len 决定能读哪些元素,cap 决定能扩到哪里 ​

创建 slice 的常用写法:

写法初始状态适用场景
var s []intnil,长度和容量都为 0暂时没有元素,之后直接 append
s := []int{1, 2}长度 2,有两个初值已经知道内容
s := make([]int, 2)长度和容量都是 2,元素为 0接下来按下标填两个位置
s := make([]int, 0, 100)长度 0,容量 100预计追加约 100 项,仍从第 0 项开始追加

make 的第二个参数是长度,第三个参数是容量。预留了容量,不等于那些元素已经在当前长度里。规范要求索引小于 len(s);重新切片则可以在容量允许的范围内扩大。[11]

go
s := make([]int, 2, 4)
fmt.Println(s, len(s), cap(s))

s = s[:4]
s[2] = 7
t := s[1:3]
fmt.Println(t, len(t), cap(t))
text
[0 0] 2 4
[0 7] 2 3

第一步不能直接访问 s[2],因为当前只有下标 0、1。s = s[:4] 把长度扩到 4 后才可以。t 从原数组下标 1 开始,到下标 3 之前结束,所以能看到两个元素;从它的起点到数组末尾还有三个位置,容量就是 3。常见误写 make([]T, n) 后再追加 n 个元素,会得到前面 n 个零值加后面 n 个新元素;要先预留容量而不增加长度,应写 make([]T, 0, n)。[11:1][12]

nil slice 和非 nil 的空 slice 没有元素,却不是同一种状态:

go
var a []int
b := []int{}
fmt.Println(a == nil, b == nil, len(a), len(b))
a = append(a, 5)
fmt.Println(a)
text
true false 0 0
[5]

只问“有没有元素”,用 len(s) == 0;问“是否是 nil”,才用 s == nil。nil slice 可以遍历、可以追加,不能按下标访问不存在的元素。接口是否区分 nil 与空集合,应看对应 API 的约定,不从两者打印起来很像来推断。[9:2][12:1]

append 与复制:先问底层数组还共不共用 ​

容量够时,append 会写到原数组里 ​

go
base := []int{10, 20, 30, 40}
left := base[:2]
result := append(left, 99)
fmt.Println("base:", base)
fmt.Println("left:", left, "result:", result)
text
base: [10 20 99 40]
left: [10 20] result: [10 20 99]

left 长度是 2,容量却是 4。追加第三个元素不必换数组,99 就写进原数组下标 2,覆盖了原来的 30。left 本身仍长 2,返回的 result 才长 3,所以 append 的结果必须被使用,常见写法是 s = append(s, x)。只修改元素和改变切片长度,是两个不同动作。[12:2]

正在绘制图示…

可切换适合窗口或原始尺寸
查看图源代码
flowchart LR
    B["base<br/>len 4 · cap 4"] --> A["底层数组<br/>10 · 20 · 99 · 40"]
    L["left<br/>len 2 · cap 4"] --> A
    R["append 返回的 result<br/>len 3 · cap 4"] --> A
放大图示

原始尺寸下可横向或纵向滚动;按 Escape 关闭。

正在绘制图示…

查看图源代码
flowchart LR
    B["base<br/>len 4 · cap 4"] --> A["底层数组<br/>10 · 20 · 99 · 40"]
    L["left<br/>len 2 · cap 4"] --> A
    R["append 返回的 result<br/>len 3 · cap 4"] --> A

这种共享无需并发就会发生。把一小段 slice 交给另一个函数,那个函数只是追加内容,也可能覆盖你仍然使用的后半段。

限制容量只能隔开追加,不能隔开已有元素 ​

接着上面的 base:

go
fenced := base[:2:2]
fenced[0] = 11

detached := append(fenced, 77)
detached[0] = 88
fmt.Println("base:", base)
fmt.Println("fenced:", fenced, "detached:", detached)
text
base: [11 20 99 40]
fenced: [11 20] detached: [88 20 77]

三下标切片的格式是 s[low:high:max],长度为 high-low,容量为 max-low。base[:2:2] 把容量也截到 2,追加第三个元素时容量不足,必须获得新数组并复制已有元素。之后改 detached[0] 不再影响原数组。[11:2][12:3]

但第一句 fenced[0] = 11 仍然修改了 base,因为那时两个描述还指向同一个数组。限制容量适合防止后续追加覆盖邻接数据,不是只读保护,也不是深复制。

要独立的一份元素,用 copy 或 Clone ​

go
cloned := slices.Clone(base)
cloned[0] = -1
fmt.Println("base:", base, "clone:", cloned)

nested := [][]int{{1, 2}}
shallow := slices.Clone(nested)
shallow[0][0] = 9
fmt.Println("nested:", nested)
text
base: [11 20 99 40] clone: [-1 20 99 40]
nested: [[9 2]]

第一份是 []int,整数元素被复制到新的存储里。第二份是 [][]int,外层元素本身是 slice,复制外层只会复制这些 slice 描述,内层数组仍然共享。slices.Clone 的文档明确说它做的是浅复制;需要独立的二维数据,必须继续复制每一行。[13]

也可以用 dst := make([]T, len(src)); copy(dst, src)。copy 返回实际复制的元素数,由源、目标的较短长度决定,不看目标还剩多少容量。因此 make([]int, 0, 3) 接收不到任何元素,先把长度设为 3 才能复制三个。文末测试专门检查了这一点。[12:4]

复制还可以解决内存保留问题:一个长期保存的小 slice 可能让原来的大数组一直可达。只保留几十个字节时,把需要的部分复制出来,便不再通过它引用整块缓冲区。是否值得做,要看原数组多大、保存多久以及复制成本;这里没有测量具体回收时机,不能据此宣称固定能节省多少内存。[14]

扩容为什么不能背成“每次翻倍” ​

语言保证的是:容量足够就复用数组,容量不足就分配足够大的新数组。新的容量具体是多少,语言不规定。[12:5]

下面给同样长度的 []int、[]byte 各追加一个元素:

go
ints := make([]int, 512)
bytes := make([]byte, 512)
ints = append(ints, 1)
bytes = append(bytes, 1)
fmt.Println("int:", len(ints), cap(ints))
fmt.Println("byte:", len(bytes), cap(bytes))

本次 Go 1.27.2、Linux/amd64 的实际输出:

text
int: 513 848
byte: 513 896

同样从 512 增到 513,容量并不相同。在本次工具链的 runtime/slice.go 中,nextslicecap 先计算增长目标:小容量倾向于翻倍,大容量逐渐过渡到较小增长比例;若一次请求很多元素,则先满足请求。growslice 还会根据元素大小和分配器规格取整,所以仅背一个倍数解释不了最后的容量。这里描述的是固定发行版本的实现,不能把 848、896 或阈值写进业务逻辑。[10:1]

知道大概会追加多少项时,可以预留容量来减少中途增长;预留过大也会占更多空间。这是时间与空间的取舍,本篇没有做性能基准,不给出“快几倍”的结论。

map:查找方便,但有存在性、共享和并发边界 ​

先记常用操作,再区分“没有”和“值为零” ​

操作写法边界
创建可写 mapmake(map[string]int) 或字面量make 的可选数字是容量提示,不是元素个数或大小上限
查值v := m[k]不存在时返回值类型的零值
查值并判断存在v, ok := m[k]ok 才能区分不存在与零值
插入、覆盖m[k] = vnil map 会 panic
删除delete(m, k)不存在或 map 为 nil 也可调用
清空clear(m)删除所有项,但不承诺具体内存归还时机
数量、遍历len(m)、for k, v := range m遍历次序没有保证

这些行为由 map、索引和内置函数的规范定义。[15][16]

go
var missing map[string]int
v, ok := missing["x"]
fmt.Println(len(missing), v, ok)
delete(missing, "x")

m := map[string]int{"b": 2, "a": 0}
v, ok = m["a"]
fmt.Println(v, ok)
text
0 0 false
0 true

两次都得到 0,但一个没记录,一个确实存着 0。库存、次数、开关配置这类数据,很容易把两种含义弄混。nil map 可以读、遍历、删除,写入则会 panic;需要写入时先 make。它与可以直接 append 的 nil slice 不同。[16:1]

map 赋值会共享,稳定输出要自己排序 ​

继续使用上面的 m:

go
alias := m
alias["a"] = 7
for _, k := range slices.Sorted(maps.Keys(m)) {
    fmt.Println(k, m[k])
}

copyMap := maps.Clone(m)
copyMap["a"] = 8
fmt.Println(m["a"], copyMap["a"])
clear(copyMap)
fmt.Println(len(m), len(copyMap))
text
a 7
b 2
7 8
2 0

改 alias 就是改同一个映射,所以 m["a"] 变成了 7。maps.Clone 才建立另一份映射,在本例中整数值互不影响。它也只是浅复制:若值是 slice 或指针,其中引用的对象还可能共享。[1:3][17]

输出中 a 在 b 前面,是代码显式排序的结果。maps.Keys 返回迭代器,slices.Sorted 收集后排序;这两个入口自 Go 1.23 可用。直接 range m 没有稳定顺序保证,既不能假定插入顺序,也不能假定每次一定不同。需要签名、可重复快照或稳定测试输出时,应明确排序规则。[18]

同一个 goroutine 在遍历时删掉尚未访问的项,该项不会再被产出;新插入的项可能出现,也可能不出现。这是迭代语义,不是并发安全保证。[18:1]

map 的键为什么不能是 slice ​

键需要可比较,即能按该类型的规则使用 == 和 !=。[19]

类型能否作为 map 的键
布尔、整数、浮点数、复数、字符串可以;浮点 NaN 等特殊值仍要另看相等语义
指针、channel可以,比较身份而不是指向对象的全部内容
数组元素类型可比较时可以
结构体所有字段类型可比较时可以
slice、map、函数不可以
any 等接口类型类型声明允许,但实际放进去的动态值也必须可比较

map[any]int 并不绕过限制。放入 []int{1} 作为键,会在运行时 panic;文末测试捕获了这次 panic。若要用固定长度摘要作键,[32]byte 就比 []byte 合适。若表示的是变长字节内容,可以根据业务语义转成 string。这属于键的建模选择,不要用指针地址代替本来需要的内容相等。[15:1][19:1]

map 元素不能取址,结构体要取出再放回 ​

下面两句都会编译失败,完整反例在文末:

go
_ = &m["x"]       // m 是 map[string]Item
m["x"].N = 2

map 索引表达式不能作为可取址的变量,虽然语言特许 m[k] = v 和 m[k]++ 这样的 map 元素赋值。因而,不能通过 &m[k] 保存元素地址,也不能把 map 中结构体的某个字段当作可直接赋值的位置。不要把“当前实现可能搬动元素”当作这条规则的必要前提;限制来自语言规范。[20]

要更新结构体值,可以取出副本、修改、再写回:

go
m := map[string]Counter{"a": {N: 1}}
c := m["a"]
c.N++
m["a"] = c
fmt.Println(m["a"].N)

p := map[string]*Counter{"a": {N: 1}}
p["a"].N++
fmt.Println(p["a"].N)
text
2
2

第二种存的是指针,通过指针可以修改它指向的 Counter,并没有对 map 元素自身取址。选择指针会增加共享对象和 nil 的管理责任:键不存在时 p[k] 是 nil,继续访问字段会 panic。值类型适合明确取出、改好、写回;指针适合确实需要共享同一个可变对象的场景。[20:1][16:2]

普通 map 不替你协调多个 goroutine ​

多个 goroutine 可以在正确发布 map 后共同只读,前提是期间没人写它。一旦有写入,读、写、删除、清空和遍历等并发访问必须用同步手段协调,不能认为“写不同的键就没事”。运行时有时会报并发 map 错误,但没报错不代表访问安全。[21]

最直接的方案是围绕访问加 sync.Mutex,或者让一个 goroutine 负责 map,其他 goroutine 发请求。锁的范围要覆盖整个操作,例如 m["n"]++ 包含读取和写回,不能只保护其中一半。文末 TestMapWithMutex 让 4 个 goroutine 各加 250 次,等待结束后验证结果为 1000;race 检查通过。这只验证这段加锁代码实际执行的路径,不是对任意 map 使用方式的证明。[22][23]

map 内部采用什么表结构、什么时候扩容,属于运行时实现。业务代码应依赖查找、赋值、迭代的语义,不把底层表结构当成接口约定。

string、byte、rune:先选对计量单位 ​

一个字节、一个码点、一个可见字符并不总是一回事 ​

string 是不可变字节序列,内容可以不是合法 UTF-8。byte 表示一个字节;rune 是 int32 的别名,处理文本时通常用来保存 Unicode 码点。一个 UTF-8 码点可能占多个字节。[24][25]

操作处理的单位或含义常见用途
len(s)字节数报文大小、存储长度
s[i]第 i 个字节按协议处理字节
s[low:high]按字节偏移切片已经知道合法边界时截取
for i, r := range si 是字节偏移,r 是解码后的 rune按码点遍历
utf8.RuneCountInString(s)码点数;无效编码按解码规则处理只需要计数,不需要构建 []rune
[]byte(s)得到可修改的字节序列修改缓冲区、接字节 API
[]rune(s)解码为码点序列需要按码点下标处理
utf8.ValidString(s)是否是合法 UTF-8验证外部输入的编码
go
s := "A中🙂"
fmt.Println(len(s), utf8.RuneCountInString(s))
fmt.Printf("% x\n", []byte(s))
for i, r := range s {
    fmt.Printf("%d %U\n", i, r)
}
fmt.Println(utf8.ValidString(s[:2]))
text
8 3
41 e4 b8 ad f0 9f 99 82
0 U+0041
1 U+4E2D
4 U+1F642
false

A 占 1 字节,中 占 3 字节,🙂 占 4 字节,共 8 字节、3 个码点。因此 range 的下标是 0、1、4,而不是 0、1、2。s[:2] 拿到 A 和 中 的第一个字节,截断了 UTF-8 编码,所以验证失败。字符串切片不会自动替你寻找码点边界。[24:1][26][25:1]

正在绘制图示…

可切换适合窗口或原始尺寸
查看图源代码
flowchart LR
    S["字符串 A中🙂"] --> B["字节偏移<br/>0:41 · 1:e4 · 2:b8 · 3:ad<br/>4:f0 · 5:9f · 6:99 · 7:82"]
    B --> R["UTF-8 解码<br/>0 → A<br/>1 → 中<br/>4 → 🙂"]
放大图示

原始尺寸下可横向或纵向滚动;按 Escape 关闭。

正在绘制图示…

查看图源代码
flowchart LR
    S["字符串 A中🙂"] --> B["字节偏移<br/>0:41 · 1:e4 · 2:b8 · 3:ad<br/>4:f0 · 5:9f · 6:99 · 7:82"]
    B --> R["UTF-8 解码<br/>0 → A<br/>1 → 中<br/>4 → 🙂"]

如果业务要“最多前 n 个码点”,可以在确认输入编码策略后按 rune 处理。但用户眼中的一个字符还可能包含多个码点,例如字母加组合重音、组合 emoji。光标移动、按可见字符截断等需求涉及 Unicode 字素簇分段,单纯 []rune 仍不够。[27]

改转换后的字节,不会改掉原字符串 ​

接着上面的 s:

go
b := []byte(s)
b[0] = 'B'
snapshot := string(b)
b[0] = 'C'
fmt.Println(s, snapshot, string(b))
fmt.Println(string(rune(65)), strconv.Itoa(65))
text
A中🙂 B中🙂 C中🙂
A 65

[]byte(s) 让你得到可修改的字节内容,改 b 不影响原字符串;string(b) 得到的字符串也不会因为后续再改 b 而改变。这是安全 Go 中可依赖的语义。至于编译器是否在某个上下文省略复制、是否发生堆分配,不能只看转换语法就下结论,需要对具体代码测量。[28]

最后一行是另一个常见误用:整数转字符串的含义是“把这个整数当成 Unicode 码点编码”,所以 65 得到 A。要数字的十进制文本,用 strconv.Itoa 或对应的 Format 函数。[28:1][29]

编码不合法时,range 不会直接报错 ​

go
s := "\xffA"
fmt.Println(utf8.ValidString(s))
for i, r := range s {
    fmt.Printf("%d %U\n", i, r)
}
fmt.Println(utf8.RuneCountInString("é"), utf8.RuneCountInString("e\u0301"))
fmt.Println("é" == "e\u0301")
text
false
0 U+FFFD
1 U+0041
1 2
false

无效的 0xff 被解码成替换字符 U+FFFD,遍历向前推进一个字节,再读到 A。如果 API 要拒绝无效 UTF-8,应明确调用验证函数,不把“range 跑完了”当成编码正确。[26:1][25:2]

后面两种 é 一种是单一码点,一种是 e 加组合重音,码点数分别为 1、2。它们的字节不同,Go 的字符串相等比较就返回 false。是否需要 Unicode 规范化,要由用户名、搜索、展示等具体规则决定;语言不会自动把视觉上相近的文本改成同一串字节。[19:2][27:1]

到这里,类型选择背后的判断就具体了:长度约束写进数组类型,变长序列用 slice,按键访问用 map;共享数据先说清谁能修改,文本操作先说清按字节、码点还是可见字符。遇到行为拿不准的代码,画出值与引用的关系,再写一个只包含这一步操作的例子,通常比背口诀更容易找到原因。

本次验证与复现 ​

示例没有第三方依赖。为固定本次环境,go.mod 声明 Go 1.27.0,本次实际工具链为 Go 1.27.2;这不表示每一个语法特性都从 Go 1.27 才有。maps.Keys 和 slices.Sorted 要求至少 Go 1.23。把下面四份文件按标注路径保存,在目录里运行:

bash
go version
go run .
go test -count=1 -v .
go test -count=1 -race .
go vet .
go build -o invalid.a testdata/compile_fail.go

最后一条是故意失败的编译检查,不要把它的退出码 1 当成示例损坏。其余命令本次全部通过;race 构建需要所在平台支持竞态检测及相应 C 编译器。8 个 Example 测试逐行核对正文的确定性输出,另外 3 个 Test 检查越界等 panic、copy 的长度规则和加锁计数。具体扩容容量只记录观察,不写成测试的跨平台断言。[23:1]

编译反例本次给出的核心诊断如下,文件路径和行号略去:

text
invalid operation: i + j (mismatched types int and int64)
cannot use uid (variable of int64 type UserID) as OrderID value in variable declaration
constant 257 overflows uint8
invalid operation: s == s (slice can only be compared to nil)
invalid operation: cannot take address of m["x"] (map index expression of struct type Item)
cannot assign to struct field m["x"].N in map
cannot assign to text[0] (neither addressable nor a map index expression)

测试中的 panic 都在单独子测试里恢复并断言,不靠肉眼看程序是否崩溃。没有进行性能基准、GC 回收时机测量、未加锁 map 的故障注入,也没有据此声称其他平台或版本的容量相同。

完整可复现代码 ​

go.mod ​

go
module example.com/go-types

go 1.27.0

main.go ​

代码块 · 160 行 · 点击收起 / 展开
go
package main

import (
	"fmt"
	"maps"
	"slices"
	"strconv"
	"unicode/utf8"
)

type UserID int64
type IDAlias = int64

func basics() {
	var id UserID = 42
	var alias IDAlias = 42
	n := 257
	fmt.Printf("%T %T\n", id, alias)
	fmt.Println(uint8(n))
	var enabled bool
	var title string
	var ptr *int
	fmt.Printf("%t %q %t\n", enabled, title, ptr == nil)
}

func changeArray(a [3]int) { a[0] = 9 }
func changeSlice(s []int) {
	s[0] = 9
	s = s[:1]
	fmt.Println("inside:", s)
}
func copying() {
	a := [3]int{1, 2, 3}
	changeArray(a)
	fmt.Println("array:", a)
	s := []int{1, 2, 3}
	changeSlice(s)
	fmt.Println("outside:", s)
}

func lengthAndCapacity() {
	s := make([]int, 2, 4)
	fmt.Println(s, len(s), cap(s))
	s = s[:4]
	s[2] = 7
	t := s[1:3]
	fmt.Println(t, len(t), cap(t))
	var a []int
	b := []int{}
	fmt.Println(a == nil, b == nil, len(a), len(b))
	a = append(a, 5)
	fmt.Println(a)
}

func sharing() {
	base := []int{10, 20, 30, 40}
	left := base[:2]
	result := append(left, 99)
	fmt.Println("base:", base)
	fmt.Println("left:", left, "result:", result)

	fenced := base[:2:2]
	fenced[0] = 11
	detached := append(fenced, 77)
	detached[0] = 88
	fmt.Println("base:", base)
	fmt.Println("fenced:", fenced, "detached:", detached)

	cloned := slices.Clone(base)
	cloned[0] = -1
	fmt.Println("base:", base, "clone:", cloned)
	nested := [][]int{{1, 2}}
	shallow := slices.Clone(nested)
	shallow[0][0] = 9
	fmt.Println("nested:", nested)
}

func growth() {
	ints := make([]int, 512)
	bytes := make([]byte, 512)
	ints = append(ints, 1)
	bytes = append(bytes, 1)
	fmt.Println("int:", len(ints), cap(ints))
	fmt.Println("byte:", len(bytes), cap(bytes))
}

func mapBasics() {
	var missing map[string]int
	v, ok := missing["x"]
	fmt.Println(len(missing), v, ok)
	delete(missing, "x")

	m := map[string]int{"b": 2, "a": 0}
	v, ok = m["a"]
	fmt.Println(v, ok)
	alias := m
	alias["a"] = 7
	for _, k := range slices.Sorted(maps.Keys(m)) {
		fmt.Println(k, m[k])
	}
	copyMap := maps.Clone(m)
	copyMap["a"] = 8
	fmt.Println(m["a"], copyMap["a"])
	clear(copyMap)
	fmt.Println(len(m), len(copyMap))
}

type Counter struct{ N int }

func mapValues() {
	m := map[string]Counter{"a": {N: 1}}
	c := m["a"]
	c.N++
	m["a"] = c
	fmt.Println(m["a"].N)
	p := map[string]*Counter{"a": {N: 1}}
	p["a"].N++
	fmt.Println(p["a"].N)
}

func textUnits() {
	s := "A中🙂"
	fmt.Println(len(s), utf8.RuneCountInString(s))
	fmt.Printf("% x\n", []byte(s))
	for i, r := range s {
		fmt.Printf("%d %U\n", i, r)
	}
	fmt.Println(utf8.ValidString(s[:2]))
	b := []byte(s)
	b[0] = 'B'
	snapshot := string(b)
	b[0] = 'C'
	fmt.Println(s, snapshot, string(b))
	fmt.Println(string(rune(65)), strconv.Itoa(65))
}

func invalidText() {
	s := "\xffA"
	fmt.Println(utf8.ValidString(s))
	for i, r := range s {
		fmt.Printf("%d %U\n", i, r)
	}
	fmt.Println(utf8.RuneCountInString("é"), utf8.RuneCountInString("e\u0301"))
	fmt.Println("é" == "e\u0301")
}

func main() {
	for _, demo := range []struct {
		name string
		run  func()
	}{
		{"basics", basics}, {"copying", copying},
		{"length and capacity", lengthAndCapacity}, {"sharing", sharing},
		{"growth", growth}, {"map basics", mapBasics}, {"map values", mapValues},
		{"text units", textUnits}, {"invalid text", invalidText},
	} {
		fmt.Println("==", demo.name, "==")
		demo.run()
	}
}

main_test.go ​

代码块 · 135 行 · 点击收起 / 展开
go
package main

import (
	"slices"
	"sync"
	"testing"
)

func Example_basics() {
	basics()
	// Output:
	// main.UserID int64
	// 1
	// false "" true
}
func Example_copying() {
	copying()
	// Output:
	// array: [1 2 3]
	// inside: [9]
	// outside: [9 2 3]
}
func Example_lengthAndCapacity() {
	lengthAndCapacity()
	// Output:
	// [0 0] 2 4
	// [0 7] 2 3
	// true false 0 0
	// [5]
}
func Example_sharing() {
	sharing()
	// Output:
	// base: [10 20 99 40]
	// left: [10 20] result: [10 20 99]
	// base: [11 20 99 40]
	// fenced: [11 20] detached: [88 20 77]
	// base: [11 20 99 40] clone: [-1 20 99 40]
	// nested: [[9 2]]
}
func Example_mapBasics() {
	mapBasics()
	// Output:
	// 0 0 false
	// 0 true
	// a 7
	// b 2
	// 7 8
	// 2 0
}
func Example_mapValues() {
	mapValues()
	// Output:
	// 2
	// 2
}
func Example_textUnits() {
	textUnits()
	// Output:
	// 8 3
	// 41 e4 b8 ad f0 9f 99 82
	// 0 U+0041
	// 1 U+4E2D
	// 4 U+1F642
	// false
	// A中🙂 B中🙂 C中🙂
	// A 65
}
func Example_invalidText() {
	invalidText()
	// Output:
	// false
	// 0 U+FFFD
	// 1 U+0041
	// 1 2
	// false
}

func TestRuntimePanics(t *testing.T) {
	tests := []struct {
		name string
		run  func()
	}{
		{"index uses len", func() { s := make([]int, 2, 4); _ = s[2] }},
		{"write nil map", func() { var m map[string]int; m["x"] = 1 }},
		{"slice key in any", func() { m := map[any]int{}; m[[]int{1}] = 1 }},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			defer func() {
				if recover() == nil {
					t.Fatal("expected panic")
				}
			}()
			tc.run()
		})
	}
}

func TestCopyUsesLength(t *testing.T) {
	src := []int{1, 2, 3}
	dst := make([]int, 0, 3)
	if n := copy(dst, src); n != 0 {
		t.Fatalf("copied %d", n)
	}
	dst = dst[:3]
	if n := copy(dst, src); n != 3 || !slices.Equal(dst, src) {
		t.Fatalf("copy: n=%d, dst=%v", n, dst)
	}
	dst[0] = 9
	if src[0] != 1 {
		t.Fatal("copy shares integer storage")
	}
}

func TestMapWithMutex(t *testing.T) {
	m := make(map[string]int)
	var mu sync.Mutex
	var wg sync.WaitGroup
	for range 4 {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for range 250 {
				mu.Lock()
				m["n"]++
				mu.Unlock()
			}
		}()
	}
	wg.Wait()
	if m["n"] != 1000 {
		t.Fatalf("got %d", m["n"])
	}
}

testdata/compile_fail.go ​

go
package fail

type UserID int64
type OrderID int64
type Item struct{ N int }

func invalid() {
	var i int
	var j int64
	_ = i + j
	var uid UserID
	var oid OrderID = uid
	_ = oid
	_ = uint8(257)
	s := []int{1}
	_ = s == s
	m := map[string]Item{"x": {N: 1}}
	_ = &m["x"]
	m["x"].N = 2
	text := "abc"
	text[0] = 'A'
}

依据 ​

脚注链接定位到正文所用的规则;规范和标准库文档另与本次官方 Go 1.27.2 发行包中的 doc/go_spec.html、包源码和 go doc 交叉核对。容量观察只来自本篇程序,没有沿用其他平台的测量。


  1. Go 规范:值的表示、赋值、调用。赋值与参数传递复制值;包含引用的值可能共享底层数据。 ↩︎ ↩︎ ↩︎ ↩︎

  2. Go 规范:布尔类型、数值类型、字符串类型、长度与容量。 ↩︎ ↩︎

  3. unsafe.Pointer 文档:uintptr 是整数,GC 不把它当作对象引用。 ↩︎

  4. Go 规范:常量、可赋值性。 ↩︎

  5. Go 规范:非常量数值转换、可表示性。整数转换的截断与常量范围检查是不同规则。 ↩︎

  6. Go 规范:类型声明、类型身份。 ↩︎ ↩︎

  7. Go 规范:零值。 ↩︎

  8. Go 规范:数组类型。 ↩︎

  9. Go 规范:slice 类型;官方教程 Go Slices: usage and internals。 ↩︎ ↩︎ ↩︎

  10. 固定证据为 Go 1.27.2 官方发行包 内 src/runtime/slice.go,本次读取了 slice(第 16 行)、growslice(第 178 行)、nextslicecap(第 326 行)。发行包 SHA-256 已核对为 ecbadb99091a3f46e31f5f934b068b1864eafa7995211b39eaddf76996045fe5。官方博客 The mechanics of append 可帮助理解描述与共享;其中的历史实现示例不是本篇的当前增长算法来源。 ↩︎ ↩︎

  11. Go 规范:索引表达式、slice 表达式、创建 slice、map 和 channel。 ↩︎ ↩︎ ↩︎

  12. Go 规范:append 与 copy。 ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  13. slices.Clone:元素按赋值复制,是浅复制;本次也读取了 Go 1.27.2 的包文档。 ↩︎

  14. 官方博客 小 slice 保留底层大数组。这里采用可达性解释,不从本例推断具体 GC 时机。 ↩︎

  15. Go 规范:map 类型。 ↩︎ ↩︎

  16. Go 规范:索引表达式、删除 map 元素、clear;官方教程 Go maps in action。 ↩︎ ↩︎ ↩︎

  17. maps.Clone:新 map 的键、值通过普通赋值设置,因此是浅复制。 ↩︎

  18. Go 规范:range;maps.Keys、slices.Sorted,两个 API 的文档标注自 Go 1.23 可用。 ↩︎ ↩︎

  19. Go 规范:比较运算。数组、结构体是否可比较取决于元素或字段;含接口时还可能遇到动态值不可比较的运行时 panic。 ↩︎ ↩︎ ↩︎

  20. Go 规范:取址操作、赋值左侧、选择器。 ↩︎ ↩︎

  21. Go FAQ:map 为什么不默认支持并发读写;Go 内存模型。 ↩︎

  22. sync.Mutex、sync.WaitGroup。锁保护临界区,等待所有写入完成后再检查结果。 ↩︎

  23. Go 数据竞争检测器:运行方式、支持环境及“只检测实际执行到的竞争”的限制。 ↩︎ ↩︎

  24. Go 规范:字符串类型、索引;官方文章 Strings, bytes, runes and characters in Go。 ↩︎ ↩︎

  25. Go 1.27.2 unicode/utf8:ValidString、RuneCountInString、DecodeRuneInString 及无效编码的处理。 ↩︎ ↩︎ ↩︎

  26. Go 规范:字符串 range:索引是字节偏移,无效 UTF-8 解码为 U+FFFD 并前进一个字节。 ↩︎ ↩︎

  27. Unicode UAX #29 文本分段,定义字素簇;Go 官方字符串文章讨论字符、码点与不自动规范化的区别。 ↩︎ ↩︎

  28. Go 规范:字符串与 slice、整数之间的转换;字符串不可变与转换结果的语义共同约束后续修改行为。 ↩︎ ↩︎

  29. strconv.Itoa:返回整数的十进制文本。 ↩︎

保留各书来源与许可 · 示例运行条件以正文为准