背景与动机
Go 的复合数据类型,本质上是在回答“如何组织一组值”这个问题:
- 数组解决“固定个数、同类型元素”的连续存储问题。
- 切片解决“在数组之上做动态视图”的问题。
- 映射解决“通过键快速查值”的问题。
- 结构体解决“把多个不同字段组合成一个业务实体”的问题。
这 4 类类型经常一起出现,但它们的语义完全不同。学习重点不是会写字面量,而是理解:
- 哪些类型是值语义,拷贝时会复制数据。
- 哪些类型内部带引用语义,赋值后会共享底层状态。
- 哪些零值可直接用,哪些零值只能读不能写。
- 数组、切片、
map、struct分别适合什么建模场景。
核心原理拆解
数组:长度属于类型的一部分
数组类型写作:
[3]int[5]string它的核心特征有两个:
- 元素类型固定。
- 长度也是类型的一部分。
这意味着:
[3]int和[4]int是两个不同类型。- 数组值赋值给另一个变量时,会复制整个数组内容。
- 数组大小固定,创建后长度不能变。
例如:
var a [3]intb := [3]int{1, 2, 3}c := bc[0] = 99修改 c 不会影响 b,因为数组默认是完整值拷贝。
数组的零值也很直接:每个元素都取其元素类型的零值。
var a [3]int // [0 0 0]数组适合:
- 大小固定的容器
- 需要值语义的场景
- 明确依赖固定长度的算法或协议边界
不适合:
- 元素个数经常变化的业务集合
【语言特性 - 数组与切片的底层关系图】

切片:对底层数组的一段可变视图
切片类型写作:
[]int[]string切片不是数组本身,而是一个描述符。可以把它理解为“指向某段底层数组的窗口”,这个窗口至少包含 3 个关键信息:
- 指向底层数组某位置的指针
- 长度
len - 容量
cap
因此切片最重要的性质不是“可变长”,而是:
- 切片赋值时,复制的是切片头部,不是底层元素
- 多个切片可能共享同一底层数组
- 通过一个切片修改元素,可能会影响另一个切片看到的结果
例如:
a := [5]int{1, 2, 3, 4, 5}s1 := a[1:4]s2 := s1s2[0] = 99这时 a[1] 和 s1[0] 也会变,因为它们共享同一底层数组。
len 和 cap 的区别
len(slice):当前视图中可直接访问的元素个数cap(slice):从当前起点到底层数组末尾,还能扩展到多大
例如:
a := [5]int{1, 2, 3, 4, 5}s := a[1:3]此时:
len(s) == 2cap(s) == 4

append 的本质
append 会在现有切片后追加元素。
可能发生两种情况:
- 容量足够:直接写到底层数组后面,仍然共享原数组
- 容量不足:分配新数组,把旧数据复制过去,再返回新切片
所以 append 后是否还共享原底层数组,取决于是否发生扩容。这也是切片最容易让人误判的地方。
零值切片
var s []int此时:
s == nillen(s) == 0cap(s) == 0
但它通常可以直接 append,这是切片零值设计里很实用的一点。
映射:键到值的哈希关联
map 类型写作:
map[string]intmap[int]bool它表达的是“通过键查值”的关系,而不是顺序容器。
核心语义:
- 键类型必须可比较
map是引用式语义的数据结构,赋值后多个变量会共享同一底层哈希表map的读取在 key 不存在时会返回值类型零值map的遍历顺序不保证稳定
例如:
m := map[string]int{ "alice": 90, "bob": 80,}取值时有两种常见形式:
score := m["alice"]score, ok := m["tom"]第二种更重要,因为它能区分:
- 键存在,对应值刚好是零值
- 键根本不存在
nil map 的边界
var m map[string]int这是合法零值,此时:
- 可以读
- 可以
len - 可以和
nil比较 - 不能直接写
例如:
m["x"] = 1会触发 panic。因为 nil map 还没有真正分配底层哈希表。
要写入,通常先:
m = make(map[string]int)【图:工程实践 - map 读写与零值行为图】
图意:这张图具体说明 nil map、make 后的 map 和 key -> value 桶结构如何变化,重点看上方 nil map 只能读不能写、中间 make 后才有真实存储,最后理解 map 的零值不是“完全不可用”,而是“未初始化写入能力”。

结构体:把不同字段组合成一个值
结构体类型写作:
type User struct { Name string Age int}它的作用不是“模拟类”,而是“组合字段形成一个新的值类型”。
结构体的核心特点:
- 字段可以是不同类型
- 结构体整体是值类型
- 赋值、传参时默认复制整个结构体
- 字段零值递归成立
例如:
u1 := User{Name: "Tom", Age: 18}u2 := u1u2.Age = 20修改 u2 不会影响 u1,因为结构体默认也是值拷贝。
匿名结构体与具名结构体
具名结构体:
type Point struct { X int Y int}匿名结构体:
p := struct { X int Y int}{X: 1, Y: 2}匿名结构体适合局部临时建模;具名结构体适合稳定业务实体。
结构体与方法接收者的关系
虽然结构体本身只是字段组合,但 Go 常通过“结构体 + 方法”表达行为。这里先记住一条:
- 结构体是常见的方法接收者类型
- 但结构体本身不等于面向对象类
4 类类型放在一起看,差别比相似点更重要
- 数组:固定长度,值语义,拷贝复制全部元素
- 切片:变长视图,轻量头部,可能共享底层数组
map:键值关联,读零值友好,但零值不可写struct:字段组合,值语义,适合建模实体
【语言特性 - 数组、切片、map、struct 对比图】

最小可运行代码示例
package main
import "fmt"
type User struct { Name string Age int}
func main() { arr := [3]int{1, 2, 3} arrCopy := arr arrCopy[0] = 99
s := []int{10, 20, 30} s2 := s s2[0] = 100
m := map[string]int{"go": 1} m["go"] = 2
u := User{Name: "Tom", Age: 18} u2 := u u2.Age = 20
fmt.Println("array:", arr, arrCopy) fmt.Println("slice:", s, s2) fmt.Println("map:", m) fmt.Println("struct:", u, u2)}运行后可以看到:
- 数组拷贝后互不影响
- 切片赋值后共享底层元素修改结果
map可按键更新- 结构体拷贝后互不影响
常见陷阱与错误示例
1. 把切片当成“独立数组”
错误理解:
s1 := []int{1, 2, 3}s2 := s1s2[0] = 99如果你以为只改了 s2,那就是误判。这里通常也改到了 s1,因为底层数组共享。
2. 认为 append 一定不会影响原切片
这是不对的。若容量够,append 可能直接改原底层数组;若容量不够,才可能分配新数组。是否共享,取决于扩容是否发生。
3. 给 nil map 直接赋值
错误示例:
var m map[string]intm["x"] = 1这会 panic。要先 make(map[string]int)。
4. 用 map 读取结果直接判断键是否存在
错误示例:
m := map[string]int{"a": 0}if m["b"] == 0 { // 误以为 key 存在且值为 0}这里无法区分“值就是 0”还是“键不存在”。应写成:
v, ok := m["b"]_ = v_ = ok5. 把结构体大对象随手按值传来传去
结构体是值语义,字段很多时,频繁拷贝会带来不必要成本,也容易让人误判“这里是不是在修改原对象”。
6. 误以为数组和切片可以直接互换
数组和切片是不同类型:
[3]int不是[]int- 切片可由数组切出来
- 但它们不是同一种东西
性能影响或设计取舍
数组的取舍
优点:
- 内存连续
- 类型信息稳定
- 值语义明确
代价:
- 长度固定
- 拷贝成本随大小增长
- 在业务代码里不如切片灵活
切片的取舍
优点:
- 使用最灵活
append和切片表达式很自然- 适合绝大多数顺序集合场景
代价:
- 共享底层数组容易产生副作用
- 扩容时会分配和复制
- 容易因为保留大底层数组而额外占内存
map 的取舍
优点:
- 按键访问效率高
- 建模字典、索引、集合非常直接
- 读取缺失 key 行为友好
代价:
- 遍历无序
- 零值不可直接写
- 相比切片有更高的管理开销
struct 的取舍
优点:
- 最贴近业务建模
- 字段明确,编译期类型安全强
- 值语义清楚,适合表达“一个完整对象值”
代价:
- 大结构体频繁拷贝有成本
- 如果字段设计混乱,会很快演变成“数据袋”
面试高频问题
1. 数组和切片的本质区别是什么
数组是固定长度的值类型,长度属于类型一部分;切片是对底层数组的一段视图,内部包含指针、长度和容量,赋值时通常共享底层数据。
2. 为什么 nil slice 通常能 append,但 nil map 不能写
因为切片的 append 可以在需要时分配新的底层数组;而 map 写入必须依赖已经存在的底层哈希表,nil map 没有这个结构,所以写入会 panic。
3. map 读取一个不存在的 key 会发生什么
不会 panic,会返回值类型的零值。如果要区分“零值”与“缺失”,应使用 value, ok := m[key]。
4. 结构体和数组为什么都常说是值类型
因为它们赋值、传参时默认复制整个值本身,而不是默认共享同一份底层状态。只是如果结构体字段里又包含切片、map、指针,这些字段内部仍可能共享引用对象。
5. append 后原切片一定不变吗
不一定。若原切片容量足够,append 可能直接写入同一底层数组;若容量不足,才可能重新分配新数组。
6. 什么场景优先用数组,什么场景优先用切片
固定长度、协议边界明确、强调值语义时优先数组;绝大多数日常集合处理、动态增长场景优先切片。
一句话总结
数组是固定长度的值容器,切片是数组上的动态视图,map 是按键访问的关联容器,结构体是字段组合形成的业务值类型;真正要掌握的不是字面量写法,而是它们各自的值语义、共享语义、零值行为和适用边界。