第 4 章:复合数据类型
jerry北京市2026年4月30日Go 10 次阅读 约 13 分钟

深入理解数组、切片(slice)、映射(map)和结构体(struct),这些是 Go 中最常用的数据结构。
4.1 数组
数组是固定长度、同类型元素的集合:
// 声明
var arr [5]int // [0 0 0 0 0]
arr2 := [3]string{"a", "b", "c"} // [a b c]
arr3 := [...]int{1, 2, 3, 4} // 编译器自动推断长度为 4
// 访问和修改
arr[0] = 10
fmt.Println(arr[0]) // 10
fmt.Println(len(arr)) // 5
数组在 Go 中是值类型,赋值和传参会复制整个数组:
a := [3]int{1, 2, 3}
b := a // 复制
b[0] = 100
fmt.Println(a[0]) // 1(a 不受影响)
实际开发中数组用得很少,切片(slice)才是主角。
4.2 切片(Slice)
切片是 Go 中最重要的数据结构之一,是对数组的动态抽象。
底层结构
切片在底层由三部分组成:
┌─────────┬─────────┬──────────┐
│ pointer │ len │ cap │
│ (指向底层数组) │ (当前长度) │ (容量) │
└─────────┴─────────┴──────────┘
创建切片
// 从数组创建
arr := [5]int{1, 2, 3, 4, 5}
s1 := arr[1:4] // [2 3 4],左闭右开
// 直接创建
s2 := []int{1, 2, 3}
// 使用 make
s3 := make([]int, 5) // len=5, cap=5
s4 := make([]int, 3, 10) // len=3, cap=10
// nil 切片
var s5 []int // s5 == nil, len=0, cap=0
append 操作
s := []int{1, 2, 3}
s = append(s, 4) // [1 2 3 4]
s = append(s, 5, 6, 7) // [1 2 3 4 5 6 7]
// 追加另一个切片
other := []int{8, 9}
s = append(s, other...) // [1 2 3 4 5 6 7 8 9]
切片扩容机制(面试重点)
当 append 导致长度超过容量时,Go 会分配新的底层数组:
s := make([]int, 0, 4)
fmt.Println(len(s), cap(s)) // 0, 4
s = append(s, 1, 2, 3, 4)
fmt.Println(len(s), cap(s)) // 4, 4
s = append(s, 5) // 触发扩容
fmt.Println(len(s), cap(s)) // 5, 8
扩容策略(Go 1.18+):
- 新容量 < 256:翻倍
- 新容量 >= 256:增长约 25%(
newcap += (newcap + 3*256) / 4)
切片的坑
// 坑 1:切片共享底层数组
a := []int{1, 2, 3, 4, 5}
b := a[1:3] // [2 3]
b[0] = 20
fmt.Println(a) // [1 20 3 4 5] a 也被修改了
// 解决:使用 copy 创建独立副本
c := make([]int, len(b))
copy(c, b)
// 坑 2:append 可能影响原切片
a = []int{1, 2, 3, 4, 5}
b = a[1:3] // len=2, cap=4
b = append(b, 100) // 修改了 a[3]
fmt.Println(a) // [1 2 3 100 5]
// 解决:使用三索引切片限制容量
b = a[1:3:3] // len=2, cap=2,append 会触发新分配
删除元素
s := []int{1, 2, 3, 4, 5}
// 删除索引 2 的元素
s = append(s[:2], s[3:]...) // [1 2 4 5]
4.3 映射(Map)
Map 是无序的键值对集合:
// 创建
m1 := map[string]int{
"apple": 5,
"banana": 3,
}
m2 := make(map[string]int) // 空 map
m2["key"] = 42
// 访问
v := m1["apple"] // 5
// 检查 key 是否存在(重要模式)
v, ok := m1["grape"]
if !ok {
fmt.Println("key 不存在")
}
// 删除
delete(m1, "apple")
// 遍历(顺序不确定)
for k, v := range m1 {
fmt.Printf("%s: %d\n", k, v)
}
// 获取长度
fmt.Println(len(m1))
Map 的注意事项
// 1. nil map 可以读,但不能写
var m map[string]int
fmt.Println(m["key"]) // 0(零值)
// m["key"] = 1 // panic: assignment to entry in nil map
// 2. map 不是并发安全的
// 多个 goroutine 同时读写会 panic
// 解决方案:sync.Map 或加锁
// 3. map 的 value 不可寻址
// m["key"].field = xxx // 不能直接修改 value 的字段
// 需要取出来修改后再放回去
4.4 结构体(Struct)
type Person struct {
Name string
Age int
City string
}
// 创建
p1 := Person{Name: "Alice", Age: 30, City: "Beijing"}
p2 := Person{"Bob", 25, "Shanghai"} // 按顺序赋值(不推荐)
p3 := new(Person) // 返回指针 *Person
// 访问字段
fmt.Println(p1.Name)
p1.Age = 31
// 指针访问(自动解引用)
pp := &p1
fmt.Println(pp.Name) // 等价于 (*pp).Name
方法
type Rectangle struct {
Width, Height float64
}
// 值接收者
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
// 指针接收者(可以修改接收者)
func (r *Rectangle) Scale(factor float64) {
r.Width *= factor
r.Height *= factor
}
rect := Rectangle{10, 5}
fmt.Println(rect.Area()) // 50
rect.Scale(2)
fmt.Println(rect.Area()) // 200
值接收者 vs 指针接收者的选择:
- 需要修改接收者 → 指针接收者
- 结构体较大 → 指针接收者(避免复制)
- 一致性:如果有一个方法用了指针接收者,其他方法也建议用指针接收者
结构体嵌入(组合)
Go 没有继承,通过嵌入实现组合:
type Address struct {
City string
Country string
}
type Employee struct {
Name string
Address // 匿名嵌入
}
e := Employee{
Name: "Alice",
Address: Address{
City: "Beijing",
Country: "China",
},
}
// 可以直接访问嵌入字段
fmt.Println(e.City) // Beijing(提升字段)
fmt.Println(e.Address.City) // Beijing(完整路径)
结构体标签(Tag)
type User struct {
Name string `json:"name" validate:"required"`
Email string `json:"email,omitempty"`
Age int `json:"age" validate:"min=0,max=150"`
}
// 标签通过反射读取,常用于 JSON 序列化、ORM、验证等
data, _ := json.Marshal(User{Name: "Alice", Age: 30})
fmt.Println(string(data)) // {"name":"Alice","age":30}
4.5 面试要点
-
切片和数组的区别?
- 数组是固定长度的值类型,切片是动态长度的引用类型
- 切片底层引用数组,包含指针、长度、容量三个字段
-
切片的扩容策略?
- < 256 翻倍,>= 256 增长约 25%
-
nil 切片和空切片的区别?
var s []int:nil 切片,指针为 nils := []int{}:空切片,指针非 nil,指向空数组- 两者
len和cap都是 0,append行为一致
-
map 是并发安全的吗?
- 不是。并发读写会 panic,需要用
sync.Map或加锁
- 不是。并发读写会 panic,需要用
-
值接收者和指针接收者的区别?
- 值接收者操作的是副本,指针接收者操作原对象
- 指针接收者可以修改接收者的状态
练习
- 实现一个函数,去除切片中的重复元素
- 使用 map 实现一个简单的词频统计
- 定义一个
Stack结构体,实现Push、Pop、Peek、IsEmpty方法 - 使用结构体嵌入实现一个简单的"动物-狗"组合关系
评论
登录 后发表评论
暂无评论