跳到主要内容

Go 数据结构

本章将介绍 Go 语言中的主要数据结构:数组、切片、映射和结构体。

数组

数组声明

// 声明指定长度的数组
var arr1 [5]int

// 声明并初始化
var arr2 [5]int = [5]int{1, 2, 3, 4, 5}

// 简短声明
arr3 := [5]int{1, 2, 3, 4, 5}

// 让编译器推断长度
arr4 := [...]int{1, 2, 3, 4, 5}

// 指定索引初始化
arr5 := [5]int{1: 10, 3: 30}
fmt.Println(arr5) // [0 10 0 30 0]

数组赋值和访问

arr := [5]int{1, 2, 3, 4, 5}

// 访问元素
fmt.Println(arr[0]) // 1
fmt.Println(arr[4]) // 5

// 修改元素
arr[0] = 100
fmt.Println(arr[0]) // 100

数组遍历

arr := [5]int{1, 2, 3, 4, 5}

// 使用索引遍历
for i := 0; i < len(arr); i++ {
fmt.Println(arr[i])
}

// 使用 range 遍历
for index, value := range arr {
fmt.Printf("索引: %d, 值: %d\n", index, value)
}

// 只获取值
for _, value := range arr {
fmt.Println(value)
}

数组是值类型

Go 中数组是值类型,赋值会复制整个数组:

arr1 := [3]int{1, 2, 3}
arr2 := arr1 // 复制整个数组
arr2[0] = 100

fmt.Println(arr1) // [1 2 3]
fmt.Println(arr2) // [100 2 3]
提示

如果需要修改原数组或避免复制,使用数组的指针。

多维数组

// 声明二维数组
var matrix [3][3]int

// 初始化
matrix2 := [3][3]int{
{1, 2, 3},
{4, 5, 6},
{7, 8, 9},
}

// 遍历
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
fmt.Print(matrix2[i][j], " ")
}
fmt.Println()
}

切片

切片是对数组的抽象,比数组更灵活和常用。

切片声明

// 声明切片(nil 切片)
var s1 []int

// 声明并初始化
s2 := []int{1, 2, 3, 4, 5}

// 使用 make 创建切片
s3 := make([]int, 5) // len=5, cap=5
s4 := make([]int, 3, 10) // len=3, cap=10

// 从数组创建切片
arr := [5]int{1, 2, 3, 4, 5}
s5 := arr[1:4] // [2, 3, 4]
s6 := arr[:3] // [1, 2, 3]
s7 := arr[2:] // [3, 4, 5]

len 和 cap

  • len():切片的长度(元素个数)
  • cap():切片的容量(底层数组的大小)
s := make([]int, 3, 10)

fmt.Println(len(s)) // 3
fmt.Println(cap(s)) // 10

切片是引用类型

切片是引用类型,修改会影响底层数组:

arr := [5]int{1, 2, 3, 4, 5}
s := arr[1:4]

s[0] = 100 // 修改切片
fmt.Println(arr) // [1 100 3 4 5]
fmt.Println(s) // [100 3 4]

append 添加元素

s := []int{1, 2, 3}

// 添加一个或多个元素
s = append(s, 4, 5)

// 添加另一个切片
s2 := []int{6, 7}
s = append(s, s2...)

fmt.Println(s) // [1 2 3 4 5 6 7]

切片自动扩容

当切片容量不足时,append 会自动扩容:

s := make([]int, 0, 1) // len=0, cap=1

s = append(s, 1) // 扩容
s = append(s, 2) // 再扩容

fmt.Println(s) // [1 2]

copy 复制切片

s1 := []int{1, 2, 3}
s2 := make([]int, len(s1))

n := copy(s2, s1)
fmt.Println(n, s2) // 3 [1 2 3]

delete 删除元素

Go 没有内置的删除函数,需要手动实现:

s := []int{1, 2, 3, 4, 5}

// 删除索引为 2 的元素
index := 2
s = append(s[:index], s[index+1:]...)

fmt.Println(s) // [1 2 4 5]

切片常见操作

// 清空切片
s = s[:0]

// 扩展切片
s = append(s, 1, 2, 3)

// 插入元素
s = append(s[:1], append([]int{100}, s[1:]...)...)

// 插入切片
s = append(s[:1], append([]int{100, 200}, s[1:]...)...)

映射 (Map)

映射是键值对的无序集合。

声明映射

// 声明映射(nil 映射)
var m1 map[string]int

// 声明并初始化
m2 := map[string]int{
"apple": 5,
"banana": 3,
"orange": 8,
}

// 使用 make 创建
m3 := make(map[string]int)
m4 := make(map[string]int, 10) // 指定初始容量

基本操作

m := map[string]int{
"apple": 5,
"banana": 3,
}

// 添加/修改元素
m["orange"] = 8

// 访问元素
count := m["apple"]
fmt.Println(count) // 5

// 访问不存在的键,返回零值
count = m["grape"]
fmt.Println(count) // 0

// 检查键是否存在
count, exists := m["apple"]
fmt.Println(count, exists) // 5 true

_, exists = m["grape"]
fmt.Println(exists) // false

// 删除元素
delete(m, "banana")

// 获取长度
fmt.Println(len(m))

遍历映射

m := map[string]int{
"apple": 5,
"banana": 3,
"orange": 8,
}

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

// 只遍历键
for key := range m {
fmt.Println(key)
}

// 只遍历值
for _, value := range m {
fmt.Println(value)
}
注意

映射的遍历顺序是随机的,每次运行可能不同。

映射是引用类型

映射是引用类型,赋值只会复制指针:

m1 := map[string]int{"a": 1}
m2 := m1

m2["a"] = 100
fmt.Println(m1["a"]) // 100

nil 映射

nil 映射不能添加元素,但可以读取:

var m map[string]int

// 读取正常
fmt.Println(m["a"]) // 0

// 添加会 panic
m["a"] = 1 // panic: assignment to entry in nil map
提示

使用前应该用 make 初始化映射。

映射的键

映射的键必须是可比较的类型:

  • 可以作为键:整数、浮点数、复数、字符串、数组、指针、结构体
  • 不能作为键:切片、函数、映射

结构体 (Struct)

结构体是由多个字段组成的复合类型。

定义结构体

type Person struct {
Name string
Age int
City string
}

创建结构体

// 方式一:按顺序初始化
p1 := Person{"张三", 25, "北京"}

// 方式二:指定字段初始化
p2 := Person{
Name: "李四",
Age: 30,
City: "上海",
}

// 方式三:创建指针
p3 := new(Person)
p3.Name = "王五"
p3.Age = 28
p3.City = "广州"

// 方式四:使用 & 取地址
p4 := &Person{Name: "赵六", Age: 35}

访问字段

p := Person{Name: "张三", Age: 25}

// 普通结构体
fmt.Println(p.Name, p.Age)

// 结构体指针
pPtr := &p
fmt.Println((*pPtr).Name) // 繁琐
fmt.Println(pPtr.Name) // Go 自动解引用

结构体嵌套

type Address struct {
City string
Street string
ZipCode string
}

type Person struct {
Name string
Age int
Address Address // 嵌套
}

p := Person{
Name: "张三",
Age: 25,
Address: Address{
City: "北京",
Street: "建国路",
ZipCode: "100000",
},
}

fmt.Println(p.Address.City)

结构体匿名字段

type Person struct {
Name string
Age int
Address
}

p := Person{
Name: "张三",
Age: 25,
Address: Address{
City: "北京",
},
}

fmt.Println(p.Name)
fmt.Println(p.City) // 直接访问匿名字段

结构体是值类型

结构体默认是值类型,赋值和传参都会复制:

type Person struct {
Name string
Age int
}

p1 := Person{Name: "张三", Age: 25}
p2 := p1
p2.Name = "李四"

fmt.Println(p1.Name) // 张三
fmt.Println(p2.Name) // 李四

结构体方法

type Person struct {
Name string
Age int
}

// 值接收者
func (p Person) greet() {
fmt.Printf("你好,我是 %s\n", p.Name)
}

// 指针接收者
func (p *Person) setAge(age int) {
p.Age = age
}

p := Person{Name: "张三", Age: 25}
p.greet() // 打印问候语
p.setAge(30) // 修改年龄
fmt.Println(p.Age) // 30
提示

如果方法需要修改结构体,应该使用指针接收者。

常见问题

切片和数组的选择

// 数组:固定大小,值类型
var arr [5]int // 用于固定长度的场景

// 切片:动态大小,引用类型
var s []int // 用于长度不确定的场景

映射的并发安全

Go 的映射不是并发安全的:

// ❌ 错误:并发读写会 panic
var m map[string]int

go func() {
m["a"] = 1
}()

go func() {
_ = m["a"]
}()

// ✅ 使用 sync.RWMutex
var mu sync.RWMutex
m := make(map[string]int)

go func() {
mu.Lock()
m["a"] = 1
mu.Unlock()
}()

go func() {
mu.RLock()
_ = m["a"]
mu.RUnlock()
}()

// ✅ 使用 sync.Map(Go 1.9+)
var sm sync.Map
sm.Store("a", 1)
value, _ := sm.Load("a")

小结

  1. 数组:固定长度,值类型
  2. 切片:动态大小,引用类型,基于数组
  3. 映射:键值对集合,引用类型
  4. 结构体:自定义类型,可以有方法

练习

  1. 创建一个数组并遍历打印所有元素
  2. 使用切片实现一个动态数组,支持添加和删除操作
  3. 创建一个 map 存储水果名称和价格,遍历打印
  4. 定义一个学生结构体,包含姓名、年龄、成绩,创建实例并访问字段
  5. 给学生结构体添加一个计算平均成绩的方法
  6. 实现一个函数,从切片中删除指定索引的元素