数组、切片、Map、结构体——Go 的复合数据结构


一、数组(Array)

Go 的数组是固定长度、同类型的值类型。

1.1 声明与初始化

// 声明:长度是类型的一部分
var arr [5]int              // [0 0 0 0 0]
arr := [5]int{1, 2, 3, 4, 5}
arr := [...]int{1, 2, 3}   // 编译器自动推断长度 = 3

// 指定索引初始化
arr := [5]int{1: 10, 3: 30}  // [0 10 0 30 0]

1.2 数组特点

// 长度是类型的一部分
var a [3]int = [3]int{1, 2, 3}
var b [3]int = [3]int{1, 2, 3}
// a == b  ✅ 同类型、同长度、同元素 → 相等

var c [4]int = [4]int{1, 2, 3, 4}
// a == c  ❌ 编译错误:类型不同([3]int != [4]int)

// 值类型:赋值和传参会复制整个数组
arr1 := [3]int{1, 2, 3}
arr2 := arr1       // arr2 是 arr1 的完整副本
arr2[0] = 100      // 不影响 arr1
fmt.Println(arr1)  // [1 2 3]

// ⚠️ 实际开发中很少直接用数组,切片(slice)更常用

1.3 多维数组

matrix := [2][3]int{
    {1, 2, 3},
    {4, 5, 6},
}
fmt.Println(matrix[1][2])  // 6

二、切片(Slice)— 最核心的数据结构

切片是 Go 中最常用的复合类型。它是一个对底层数组的引用。

2.1 切片的内部结构

slice = { ptr, len, cap }
         │      │     │
         │      │     └─ 底层数组总容量
         │      └─ 当前元素个数
         └─ 指向底层数组的指针
// 用 reflect 包可以查看 slice 的内部结构
s := []int{1, 2, 3}
header := (*reflect.SliceHeader)(unsafe.Pointer(&s))
fmt.Printf("addr=%x len=%d cap=%d\n", header.Data, header.Len, header.Cap)

2.2 创建切片

// 1. 字面量(最常用)
s := []int{1, 2, 3, 4, 5}

// 2. make 创建指定长度/容量的切片
s := make([]int, 5)       // len=5, cap=5, 全零
s := make([]int, 0, 10)   // len=0, cap=10,预分配容量

// 3. 从数组/切片派生
arr := [5]int{10, 20, 30, 40, 50}
s1 := arr[1:3]    // [20 30],从索引1到索引3(不含)
s2 := arr[:3]     // [10 20 30]
s3 := arr[2:]     // [30 40 50]
s4 := arr[:]      // [10 20 30 40 50],整个数组

// 4. 从已有切片创建子切片
sub := s[1:3]     // 共享底层数组!
sub[0] = 99       // 原切片 s 也会被修改

2.3 append — 动态增长

s := []int{1, 2, 3}
fmt.Println(len(s), cap(s))  // 3 3

// 追加单个元素
s = append(s, 4)
fmt.Println(len(s), cap(s))  // 4 6(容量翻倍增长)

// 追加多个元素
s = append(s, 5, 6, 7)

// 合并两个切片
s2 := []int{8, 9}
s = append(s, s2...)

// ⚠️ append 可能触发扩容,返回新切片!必须用返回值
// s = append(s, 10)  // ✅ 正确
// append(s, 10)      // ⚠️ 返回值被丢弃,原 s 不变

2.4 copy — 安全复制

src := []int{1, 2, 3, 4, 5}
dst := make([]int, len(src))
n := copy(dst, src)   // n = 5,返回复制的元素个数
// dst 是 src 的独立副本,修改互不影响

// 部分复制
dst2 := make([]int, 3)
copy(dst2, src[1:])   // [2 3 4]

2.5 删除元素

// 删除索引 i 的元素(不保留顺序,O(1))
s := []int{1, 2, 3, 4, 5}
i := 2  // 删除元素 3
s = append(s[:i], s[i+1:]...)  // [1 2 4 5]

// 删除索引 i 的元素(保留顺序,O(n))
s = append(s[:i], s[i+1:]...)

// ⚠️ 从切片中间删除不会释放内存,底层数组不变
// 如果需要释放,复制到新切片
s = s[:len(s)-1]  // 缩小 len,但 cap 不变

2.6 切片陷阱

// 陷阱1:子切片共享底层数组
origin := []int{1, 2, 3, 4, 5}
sub := origin[:3]
sub = append(sub, 99)  // origin 也变了![1 2 3 99 5]

// 陷阱2:make 时 len > 0 的影响
s1 := make([]int, 0, 5)  // len=0, cap=5 → append 正常
s2 := make([]int, 5, 5)  // len=5, cap=5 → append 从末尾追加
// 还是复制底层数组的内容,而不是共享
s3 := make([]int, 3, 5)
s4 := s3[:2]
s4 = append(s4, 100)  // 修改了 s3 的第3个元素!

// 解决方案:三下标切片限制长度
s5 := s3[:2:2]  // [low:high:max] → len=2, cap=2
s5 = append(s5, 100)  // 安全,会触发扩容

三、Map — 哈希表

3.1 声明与初始化

// 声明(nil map,不能直接写入!)
var m map[string]int   // nil map

// make 初始化
m = make(map[string]int)
m["age"] = 25          // ✅

// 字面量初始化
m := map[string]int{
    "age":    25,
    "score":  100,
}

// 创建空 map(推荐写法)
m := make(map[string]int)    // 等价于 map[string]int{}
m := map[string]int{}        // 等价于上面

3.2 基本操作

m := map[string]int{"Go": 1, "Java": 2}

// 读取
v := m["Go"]           // 1
v, ok := m["Python"]   // 0, false(ok=false 表示不存在)

// 写入/更新
m["Python"] = 3        // 不存在则新增,存在则覆盖

// 删除
delete(m, "Java")      // 删除键 "Java"

// 长度
len(m)                 // 2

// 遍历
for key, value := range m {
    fmt.Printf("%s: %d\n", key, value)
}

遍历顺序是随机的。 Go 故意打乱 map 遍历顺序,防止开发者依赖顺序。

3.3 Map 注意事项

// 1. nil map 读取返回零值,写入 panic
var m map[string]int
v := m["key"]     // 0(零值,不 panic)
// m["key"] = 1   // ❌ panic: assignment to entry in nil map

// 2. map 是引用类型
m1 := map[string]int{"a": 1}
m2 := m1       // m2 和 m1 指向同一个 map
m2["a"] = 99   // m1["a"] 也变成 99

// 3. map 不支持取地址
// &m["key"]  // ❌ 编译错误

// 4. 并发不安全
// 多个 goroutine 同时读写同一个 map 会导致竞态
// 解决方案:sync.Mutex 或 sync.Map(并发专题详述)

四、结构体(Struct)— Go 的"类"

Go 没有 class,struct + 方法 实现了面向对象的核心能力。

4.1 定义与使用

// 定义结构体
type Person struct {
    Name string
    Age  int
    Email string
}

// 创建实例
p1 := Person{Name: "Alice", Age: 30, Email: "alice@example.com"}
p2 := Person{"Bob", 25, "bob@example.com"}  // 按字段顺序赋值
p3 := new(Person)  // 返回 *Person(指针),字段零值

// 访问和修改字段
fmt.Println(p1.Name)  // Alice
p1.Age = 31

4.2 值类型 vs 指针

// 结构体是值类型,赋值和传参会复制
func modify(p Person) {
    p.Name = "Modified"  // 修改的是副本
}

func modifyPtr(p *Person) {
    p.Name = "Modified"  // 修改的是原始值
}

p := Person{Name: "Alice"}
modify(p)
fmt.Println(p.Name)   // Alice(未变)

modifyPtr(&p)
fmt.Println(p.Name)   // Modified(已变)

4.3 结构体标签(Tag)

结构体字段可以附加元数据标签,常用于 JSON 序列化、ORM 映射等。

type User struct {
    ID        int    `json:"id"`
    Name      string `json:"name"`
    Password  string `json:"-"`                    // `-` 表示忽略该字段
    Email     string `json:"email,omitempty"`      // 空值时省略
    CreatedAt string `json:"created_at" db:"created_at"`
}

// JSON 序列化
u := User{ID: 1, Name: "Alice", Password: "secret"}
data, _ := json.Marshal(u)
fmt.Println(string(data))
// {"id":1,"name":"Alice"}  (Password 被忽略)

4.4 匿名字段(嵌入)

// 匿名字段实现"组合"(Go 用组合代替继承)
type Address struct {
    City    string
    Country string
}

type Employee struct {
    Person   // 匿名嵌入(没有字段名)
    Address  // 可以嵌入多个
    Dept     string
}

e := Employee{
    Person:  Person{Name: "Alice", Age: 30},
    Address: Address{City: "Beijing", Country: "China"},
    Dept:    "Engineering",
}

// 可以直接访问嵌入字段(提升)
fmt.Println(e.Name)    // Alice(来自 Person)
fmt.Println(e.City)    // Beijing(来自 Address)

// 同名字段需要显式指定
fmt.Println(e.Person.Name)  // 显式通过类型访问

这就是 Go 的组合优于继承——没有 extends,用嵌入实现代码复用。


五、指针

5.1 基本用法

x := 42
p := &x      // p 的类型是 *int,指向 x
fmt.Println(*p)  // 42(解引用)
*p = 100     // 通过指针修改 x
fmt.Println(x)   // 100

5.2 new 与 make

函数用途返回值
new(T)分配内存,返回 *T(零值初始化)指针
make(T, len, cap)初始化 slice/map/channel,返回 T值(非指针)
// new:适用于结构体等
p := new(Person)    // *Person,字段零值
// p := &Person{}   // 等价写法,更常用

// make:仅适用于 slice/map/channel
s := make([]int, 10)
m := make(map[string]int)
ch := make(chan int)

// ⚠️ make 不能用于结构体,new 不能用于 slice/map/channel 的初始化

5.3 指针的优势

// 1. 避免复制大结构体
func processUser(user *User) {  // 只传指针,不复制整个 User
    user.Name = "updated"
}

// 2. 函数可以修改外部变量
func increment(n *int) {
    *n++
}

// 3. nil 指针可以表示"空"
var p *int  // nil,可以用于判断

六、本节要点速查

概念要点
数组固定长度、值类型,实际很少直接使用
切片ptr/len/cap 三元组,动态数组的引用
append可能触发扩容,必须 s = append(s, x)
切片陷阱子切片共享底层数组,修改会互相影响
Mapm[key] 读取,delete(m, key) 删除,range 遍历顺序随机
nil Map可读不可写,写入会 panic
Struct值类型,赋值/传参复制,方法用指针接收者修改
Struct Tagjson:"name" 等元数据,用于序列化/ORM
匿名嵌入Go 的组合机制,替代继承
new(T)返回 *T(零值指针)
make(T)初始化 slice/map/channel

上一篇:02-变量与类型 下一篇:04-流程控制