Go基础:Go语言中集合详解(包括:数组、切片、Map、列表等)
文章目录
- 一、集合概述
- 二、数组(array)详解
- 2.1 定义与特性
- 2.2 数组的语法
- 2.3 数组循环
- 三、切片(slice)详解
- 3.1 定义与特性
- 3.2 切片的语法
- 3.3 初始化方式
- 3.4 数组与切片的区别
- 3.5 切片的操作
- 四、映射(map)详解
- 4.1 Map 的定义与初始化
- 4.2 Map 的基本操作
- 4.3 Map 的并发安全
- 五、列表(list)详解
- 5.1 List定义和特点
- 5.2 数组与列表的对比
- 5.3 List的基本操作
- 5.4 List使用的案例代码
一、集合概述
在实际需求中,我们会有很多同一类型的元素放在一起的场景,这就是集合,例如 100 个数字,10 个字符串等。在 Go 语言中,数组(array)、切片(slice)、映射(map)这些都是集合类型,用于存放同一类元素。虽然都是集合,但用处又不太一样。
二、数组(array)详解
2.1 定义与特性
数组存放的是固定长度、相同类型的数据,而且这些存放的元素是在内存中是连续的。所存放的数据类型没有限制,可以是整型、字符串甚至自定义。其长度在定义时确定,不可更改。数组的特性:
- 固定长度:长度是数组类型的一部分,
[3]int
和[5]int
是不同的类型。 - 值类型:数组是值类型,赋值或作为参数传递时会复制整个数组。
2.2 数组的语法
1、基本语法
// var arrayName [length]elementType
array:=[5]string{"a","b","c","d","e"}
示例代码:
package main
import "fmt"
func main() {// 定义一个长度为3的整型数组var arr [3]intarr[0] = 10arr[1] = 20arr[2] = 30fmt.Println("数组:", arr) // 输出: [10 20 30]
}
2、初始化方式
静态初始化:arr := [3]int{10, 20, 30}
动态初始化:arr := [...]int{10, 20, 30} // 编译器自动推断长度
3、示例代码
package main
import "fmt"
func modifyArray(arr [3]int) {arr[0] = 100
}
func main() {arr := [3]int{10, 20, 30}modifyArray(arr)fmt.Println("数组:", arr) // 输出: [10 20 30],原数组未被修改
}
2.3 数组循环
使用传统的 for 循环遍历数组,输出对应的索引和对应的值,这种方式很烦琐,一般不使用,大部分情况下,我们使用的是 for range 这种 Go 语言的新型循环,如下面的代码所示:
for i,v:=range array{fmt.Printf("数组索引:%d,对应值:%s\n", i, v)
}
这种方式和传统 for 循环的结果是一样的。对于数组,range 表达式返回两个结果:
- 第一个是数组的索引;
- 第二个是数组的值。
在上面的示例中,把返回的两个结果分别赋值给 i 和 v 这两个变量,就可以使用它们了。相比传统的 for 循环,for range 要更简洁,如果返回的值用不到,可以使用 _ 下划线丢弃,如下面的代码所示:
for _,v:=range array{fmt.Printf("对应值:%s\n", v)
}
数组的索引通过 _ 就被丢弃了,只使用数组的值 v 即可。
三、切片(slice)详解
3.1 定义与特性
切片是对数组的一个连续片段的引用,其长度可以动态变化。切片和数组类似,可以把它理解为动态数组。切片是基于数组实现的,它的底层就是一个数组。对数组任意分隔,就可以得到一个切片。切片的特性:
- 动态长度:切片的长度可以动态调整。
- 引用类型:切片是对底层数组的引用,修改切片会影响底层数组。
- 容量:切片的容量是从切片的第一个元素到底层数组末尾的元素数量。
3.2 切片的语法
var sliceName []elementType
示例代码
package main
import "fmt"
func main() {// 定义一个整型切片var slice []intslice = append(slice, 10, 20, 30)fmt.Println("切片:", slice) // 输出: [10 20 30]
}
3.3 初始化方式
从数组创建切片
arr := [5]int{10, 20, 30, 40, 50}
slice := arr[1:4] // 包含索引1到3的元素
fmt.Println("切片:", slice) // 输出: [20 30 40]
使用 make 创建切片
slice := make([]int, 3, 5) // 长度为3,容量为5
fmt.Println("切片:", slice) // 输出: [0 0 0]
示例代码
package main
import "fmt"
func modifySlice(slice []int) {slice[0] = 100
}
func main() {slice := []int{10, 20, 30}modifySlice(slice)fmt.Println("切片:", slice) // 输出: [100 20 30],原切片被修改
}
3.4 数组与切片的区别
特性 | 数组 | 切片 |
---|---|---|
长度 | 固定 | 动态 |
类型 | 长度是类型的一部分 | 长度不是类型的一部分 |
传递方式 | 值传递 | 引用传递 |
使用场景 | 长度固定的数据集合 | 动态变化的数据集合 |
3.5 切片的操作
1. 添加元素
使用 append
函数向切片添加元素:
slice := []int{10, 20, 30}
slice = append(slice, 40)
fmt.Println("切片:", slice) // 输出: [10 20 30 40]
2. 删除元素
通过切片操作删除元素:
slice := []int{10, 20, 30, 40}
slice = append(slice[:1], slice[2:]...) // 删除索引1的元素
fmt.Println("切片:", slice) // 输出: [10 30 40]
3. 遍历切片
使用 for
或 for-range
遍历切片:
slice := []int{10, 20, 30}
for i, v := range slice {fmt.Printf("索引: %d, 值: %d\n", i, v)
}
四、映射(map)详解
在 Go 语言中,map 是一个无序的 K-V 键值对集合,结构为 map[K]V。其中 K 对应 Key,V 对应 Value。map 中所有的 Key 必须具有相同的类型,Value 也同样,但 Key 和 Value 的类型可以不同。此外,Key 的类型必须支持 == 比较运算符,这样才可以判断它是否存在,并保证 Key 的唯一。
Go 语言中的 map
是一种非常重要的数据结构,类似于其他语言中的字典或哈希表。map
提供了高效的查找、插入和删除操作,平均时间复杂度为 O(1)。
4.1 Map 的定义与初始化
定义:map
的语法为 map[KeyType]ValueType
,其中 KeyType
必须是可比较的类型(如 int
、string
、指针等),ValueType
可以是任意类型。示例代码如下:
package main
import "fmt"
func main() {// 定义一个 map,key 是 string,value 是 intvar m map[string]intfmt.Println(m) // 输出: map[]
}
初始化:map
必须使用 make
函数或字面量初始化,否则为 nil
,无法直接赋值。示例代码如下
// 使用 make 初始化
// nameAgeMap:=make(map[string]int)
m1 := make(map[string]int)
m1["age"] = 25
fmt.Println(m1) // 输出: map[age:25]
// 使用字面量初始化
m2 := map[string]int{"age": 30, "height": 180}
fmt.Println(m2) // 输出: map[age:30 height:180]
4.2 Map 的基本操作
1、添加和修改
直接通过 key
赋值即可,如果 key
不存在则添加,存在则修改。示例代码
m := make(map[string]int)
m["age"] = 25 // 添加
m["age"] = 26 // 修改
fmt.Println(m) // 输出: map[age:26]
2、删除
使用 delete
函数删除指定 key
的键值对。示例代码
m := map[string]int{"age": 25, "height": 180}
delete(m, "height")
fmt.Println(m) // 输出: map[age:25]
3、查找
通过 value, ok := m[key]
判断 key
是否存在。示例代码
m := map[string]int{"age": 25}
if value, ok := m["age"]; ok {fmt.Println("age 存在,值为:", value) // 输出: age 存在,值为: 25
} else {fmt.Println("age 不存在")
}
4、Map 的遍历
使用 for-range
遍历 map
,注意遍历顺序是随机的(Go 1.0+ 保证每次遍历顺序不同)。示例代码
m := map[string]int{"age": 25, "height": 180, "weight": 70}
for key, value := range m {fmt.Printf("key: %s, value: %d\n", key, value)
}
输出
key: age, value: 25
key: height, value: 180
key: weight, value: 70
需要注意的是 map 的遍历是无序的,也就是说你每次遍历,键值对的顺序可能会不一样。如果想按顺序遍历,可以先获取所有的 Key,并对 Key 排序,然后根据排序好的 Key 获取对应的 Value。
5、Map 的大小
和数组切片不一样,map 是没有容量的,它只有长度,也就是 map 的大小(键值对的个数)。要获取 map 的大小,使用内置的 len 函数即可,如下代码所示:
fmt.Println(len(nameAgeMap))
4.3 Map 的并发安全
Go 的原生 map
不是并发安全的,多个 goroutine 同时读写会导致 panic。解决方案包括:
- 使用
sync.Mutex
加锁。 - 使用
sync.Map
(Go 1.9+ 提供)。
1、使用 sync.Mutex
,示例代码:
package main
import ("fmt""sync"
)
func main() {var m map[string]intm = make(map[string]int)var mu sync.Mutexvar wg sync.WaitGroupfor i := 0; i < 10; i++ {wg.Add(1)go func(i int) {defer wg.Done()mu.Lock()m["key"] = imu.Unlock()}(i)}wg.Wait()fmt.Println(m) // 输出: map[key:9](值可能不同)
}
2、使用 sync.Map
,示例代码:
package main
import ("fmt""sync"
)
func main() {var m sync.Mapvar wg sync.WaitGroupfor i := 0; i < 10; i++ {wg.Add(1)go func(i int) {defer wg.Done()m.Store("key", i)}(i)}wg.Wait()if value, ok := m.Load("key"); ok {fmt.Println("key 的值为:", value) // 输出: key 的值为: 9(值可能不同)}
}
五、列表(list)详解
5.1 List定义和特点
Go语言中的列表(List)是一种常用的数据结构,通常通过标准库中的 container/list
包实现。它是一种双向链表,支持高效的插入、删除和遍历操作。以下从定义、特点、基本操作和实例应用四个方面进行详细解析,并附上案例代码。
Go语言并没有内置的列表类型,但通过 container/list
包提供了双向链表的实现。其主要特点包括:
- 双向链表结构:每个节点包含指向前驱和后继节点的指针,支持双向遍历。
- 动态大小:列表的长度可以动态调整,无需预先分配固定空间。
- 支持任意类型:通过
interface{}
类型存储元素,可以容纳不同类型的数据。
5.2 数组与列表的对比
特性 | 数组(Array) | 列表(List) |
---|---|---|
长度 | 固定,定义时确定 | 动态,可以随时调整 |
内存结构 | 连续存储,支持高效随机访问 | 非连续存储,通过指针链接节点 |
访问方式 | 支持通过索引直接访问(如arr[i] ) | 不支持索引访问,需遍历节点 |
插入/删除 | 效率低,需移动元素 | 效率高,只需调整指针 |
适用场景 | 需要固定大小、高效随机访问的场景 | 需要频繁插入、删除操作的场景 |
5.3 List的基本操作
container/list
包提供了丰富的方法来操作列表,以下是常用操作及其说明:
1. 初始化列表:
- 使用
list.New()
创建一个空列表。 - 示例代码:
l := list.New()
2. 插入元素:
PushFront(v interface{})
:在列表头部插入元素。PushBack(v interface{})
:在列表尾部插入元素。InsertBefore(v interface{}, mark *Element)
:在指定节点前插入元素。InsertAfter(v interface{}, mark *Element)
:在指定节点后插入元素。- 示例代码:
l.PushBack("尾部元素") l.PushFront("头部元素")
3. 删除元素:
Remove(e *Element)
:删除指定节点,并返回节点的值。- 示例代码:
element := l.Front() // 获取头部节点 l.Remove(element) // 删除头部节点
4. 遍历列表:
- 使用
Front()
和Next()
方法从头到尾遍历列表。 - 示例代码:
for e := l.Front(); e != nil; e = e.Next() {fmt.Println(e.Value) }
5. 获取列表长度:
Len()
方法返回列表的长度。- 示例代码:
length := l.Len() fmt.Println("列表长度:", length)
5.4 List使用的案例代码
以下是一个完整的案例代码,展示如何初始化列表、插入元素、遍历列表以及删除元素:
package main
import ("container/list""fmt"
)
func main() {// 初始化列表l := list.New()// 插入元素l.PushBack("尾部元素1")l.PushBack("尾部元素2")l.PushFront("头部元素1")// 遍历列表fmt.Println("列表遍历结果:")for e := l.Front(); e != nil; e = e.Next() {fmt.Println(e.Value)}// 删除元素element := l.Front() // 获取头部节点l.Remove(element) // 删除头部节点// 再次遍历列表fmt.Println("\n删除头部节点后的列表:")for e := l.Front(); e != nil; e = e.Next() {fmt.Println(e.Value)}// 获取列表长度fmt.Println("\n当前列表长度:", l.Len())
}
运行结果:
列表遍历结果:
头部元素1
尾部元素1
尾部元素2
删除头部节点后的列表:
尾部元素1
尾部元素2
当前列表长度: 2