第 4 章:复合数据类型

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

深入理解数组、切片(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 面试要点

  1. 切片和数组的区别?

    • 数组是固定长度的值类型,切片是动态长度的引用类型
    • 切片底层引用数组,包含指针、长度、容量三个字段
  2. 切片的扩容策略?

    • < 256 翻倍,>= 256 增长约 25%
  3. nil 切片和空切片的区别?

    • var s []int:nil 切片,指针为 nil
    • s := []int{}:空切片,指针非 nil,指向空数组
    • 两者 lencap 都是 0,append 行为一致
  4. map 是并发安全的吗?

    • 不是。并发读写会 panic,需要用 sync.Map 或加锁
  5. 值接收者和指针接收者的区别?

    • 值接收者操作的是副本,指针接收者操作原对象
    • 指针接收者可以修改接收者的状态

练习

  1. 实现一个函数,去除切片中的重复元素
  2. 使用 map 实现一个简单的词频统计
  3. 定义一个 Stack 结构体,实现 PushPopPeekIsEmpty 方法
  4. 使用结构体嵌入实现一个简单的"动物-狗"组合关系

← 上一章:流程控制与函数 | 下一章:指针与内存模型 →

评论

登录 后发表评论

暂无评论