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")
小结
- 数组:固定长度,值类型
- 切片:动态大小,引用类型,基于数组
- 映射:键值对集合,引用类型
- 结构体:自定义类型,可以有方法
练习
- 创建一个数组并遍历打印所有元素
- 使用切片实现一个动态数组,支持添加和删除操作
- 创建一个 map 存储水果名称和价格,遍历打印
- 定义一个学生结构体,包含姓名、年龄、成绩,创建实例并访问字段
- 给学生结构体添加一个计算平均成绩的方法
- 实现一个函数,从切片中删除指定索引的元素