目录
前言
1. 数组(array)
1.1 声明数组
1.2 初始化数组
1.3 遍历数组
2. 切片(slice)
2.1 从数组生成一个新的切片
2.2 从切片生成一个新的切片
2.3 直接生成一个新的切片
2.4 切片添加元素
2.5 切片删除元素
2.6 遍历切片
3. 映射(map)
3.1 声明和初始化
3.2 遍历映射
3.3 映射操作
3.4 并发操作map
range
前言
变量和常量虽能存储数据,但是在编写一些逻辑稍复杂的程序中,往往需要存储更多、更复 杂且不同类型的数据,这些数据一般存储在Go语言的内置容器中。
Go语言的内置容器主要有数组、切片和映射。 本章将详细介绍以上三种内置容器的特点和使用方法,在编程中能使用恰当的容器存储数据并对其进行增加、删除和修改等操作
1. 数组(array)
数组是具有相同类型且长度固定的一组数据项序列,这组数据项序列对应存放在内存中的一 块连续区域中。
1.1 声明数组
数组在使用前需先声明,声明时必须指定数组的大小且数组大小之后不可再变。数组中存放的元素类型可以是整型、字符串或其他自定义类型。
- 声明方式:var 数组变量名 [数组长度]元素类型,如:
var a [3]string
注意:长度是类型的一部分,[5]int 和 [10]int 是不同类型
1.2 初始化数组
数组可在声明时进行赋值,样例如下:
var friend = [3]string{"Tom","Jerry","Peter"}
使用这种方式初始化数组,需要保证大括号里面的元素数量和数组大小一致。 如果忽略中括号内的数字,不设置数组大小,Go语言编译器在编译时也可根据元素的个数来设置数组的大小,通过用“...”代替数组大小来实现。样例如下:
var friend = [...]string{"Tom","Jerry","Peter"}
package main
import "fmt"
func main() {
var friend = [...]string{"Tom", "Jerry", "Peter"}
fmt.Println(friend)
}
//result
[Tom Jerry Peter]
1.3 遍历数组
数组元素可以通过数组下标来读取或修改,数组下标从0开始,第一个元素的数组下标为0, 第二个元素的数组下标为1,以此类推。
我们现在可以通过遍历数组的方式(for循环)来对其进行打印。
package main
import "fmt"
func main() {
var friend = [...]string{"Tom", "Jerry", "Peter"}
for k, v := range friend {
fmt.Println("index:", k, "friend:", v)
}
}
//result
index: 0 friend: Tom
index: 1 friend: Jerry
index: 2 friend: Peter
2. 切片(slice)
相对于数组,切片(slice)是一种更方便和强大的数据结构,它同样表示多个同类型元素的连续集合,但是切片本身并不存储任何元素,而只是对现有数组的引用
定义:
- 切片是对数组的一个连续片段的引用
- 是一个轻量级的数据结构,用三个字段描述一个片段:指针、长度和容量
结构:
- 指针:切片的指针一般指切片中第一个元素所指向的内存地址,用十六进制表示。
- 长度:切片中实际存在元素的个数。
- 容量:从切片的起始元素开始到其底层数组中的最后一个元素的个数。
长度和容量:
- 长度是切片当前的元素个数,容量是从切片的开始位置到底层数组的结尾。
- 使用len()函数 可获得当前切片长度,cap()函数可获得当前切片容量。
- 切片的长度和容量都是不固定的,可以通过追加元素使切片的长度和容量增大
注意:切片的容量值必须大于等于切片长度值,否则程序会报错。对于切片的容量应该有个 大概的估值,若容量值过小,对切片的多次扩充会造成性能损耗。
切片创建方式:
- 直接声明:
var s []int
- 使用字面量:
s := []int{1, 2, 3}
- 使用make函数:
s := make([]int, length, capacity)
- 从数组或其他切片创建:
s := arr[start:end]
切片主要有三种生成方式:
1.从数组生成一个新的切片; 2.从切片生成一个新的切片; 3.直接生成一个新的切片。
2.1 从数组生成一个新的切片
s := arr[start:end]
package main
import "fmt"
func main() {
var friend = [...]string{"Tom", "Jerry", "Peter"}
var my_friend = friend[1:2]
fmt.Println("friend数组:", friend)
fmt.Println("my_friend切片:", my_friend)
fmt.Println("friend数组地址为", &friend[1])
fmt.Println("my_friend切片地址为", &my_friend[0])
fmt.Println("my_friend切片长度为:", len(my_friend))
fmt.Println("my_friend切片容量为:", cap(my_friend))
}
//result
friend数组: [Tom Jerry Peter]
my_friend切片: [Jerry]
friend数组地址为 0xc000026100
my_friend切片地址为 0xc000026100
my_friend切片长度为: 1
my_friend切片容量为: 2
根据运行结果,可以归纳出从数组或切片生成新的切片有如下特性:
◇ 新生成的切片长度:结束位置-开始位置。
◇ 新生成的切片取出的元素不包括结束位置对应的元素。
◇ 新生成的切片是对现有数组或切片的引用,其地址与截取的数组或切片开始位置对应的元 素地址相同。(缘由)
- (结论)切片比数组更灵活,且更节省内存。多个切片可以共享同一个底层数组,节省内存
◇ 新生成的切片容量指从切片的起始元素开始到其底层数组中的最后一个元素的个数。
2.2 从切片生成一个新的切片
我们重新从friend数组生成my_friend切片,再从my_friend切片生成my_friend2切片。
package main
import "fmt"
func main() {
var friend = [...]string{"Tom", "Jerry", "Peter"}
var my_friend = friend[1:3]
var my_friend2 = friend[0:1]
fmt.Println("friend数组:", friend)
fmt.Println("my_friend切片:", my_friend)
fmt.Println("my_friend切片:", my_friend2)
fmt.Println("friend数组地址为", &friend[1])
fmt.Println("my_friend切片地址为", &my_friend[0])
fmt.Println("my_friend2切片地址为", &my_friend2[0])
fmt.Println("my_friend切片长度为:", len(my_friend))
fmt.Println("my_friend切片容量为:", cap(my_friend))
fmt.Println("my_friend2切片长度为:", len(my_friend2))
fmt.Println("my_friend2切片容量为:", cap(my_friend2))
}
//result
friend数组: [Tom Jerry Peter]
my_friend切片: [Jerry Peter]
my_friend切片: [Tom]
friend数组地址为 0xc000026100
my_friend切片地址为 0xc000026100
my_friend2切片地址为 0xc0000260f0
my_friend切片长度为: 2
my_friend切片容量为: 2
my_friend2切片长度为: 1
my_friend2切片容量为: 3
Q:为什么my_friend2
地址不同
A:它们指向同一数组中的不同位置。
- 虽然
my_friend
和my_friend2
都是从friend
创建的,但它们指向底层数组的不同位置。 -
my_friend
指向 "Jerry"(第二个元素),而my_friend2
指向 "Tom"(第一个元素)。
2.3 直接生成一个新的切片
- 直接声明切片:
var s []int
- 初始化切片
(1) 在声明的同时初始化 我们可以在声明切片的同时进行初始化赋值
(2) 声明完切片后,可以通过内建函数make()来初始化切片,格式如下:
s := make([]int, length, capacity)
demo
package main
import "fmt"
func main() {
// 1. 声明和初始化
var s1 []int // 声明一个空切片
s2 := []int{1, 2, 3, 4, 5} // 声明并初始化
s3 := make([]int, 5, 10) // 使用make创建切片,长度5,容量10
fmt.Println("s1:", s1, "len:", len(s1), "cap:", cap(s1))
fmt.Println("s2:", s2, "len:", len(s2), "cap:", cap(s2))
fmt.Println("s3:", s3, "len:", len(s3), "cap:", cap(s3))
}
//result
s1: [] len: 0 cap: 0
s2: [1 2 3 4 5] len: 5 cap: 5
s3: [0 0 0 0 0] len: 5 cap: 10
2.4 切片添加元素
Go语言中,我们可以使用append()
函数可以向切片添加元素。
当切片不能再容纳其他元素 时(即当前切片长度值等于容量值),下一次使用append()函数对切片进行元素添加,容量会按2倍数进行扩充。
append()
函数
append
是Go语言内置函数,用于向切片添加元素基本语法:
slice = append(slice, element1, element2, ...)
append
返回一个新的切片,通常赋值给原切片变量
package main
import "fmt"
func main() {
student := make([]int, 1, 1)
for i := 0; i < 8; i++ {
student = append(student, i)
fmt.Println("当前切片长度:", len(student), "当前切片容量:", cap(student))
}
}
//result
当前切片长度: 2 当前切片容量: 2
当前切片长度: 3 当前切片容量: 4
当前切片长度: 4 当前切片容量: 4
当前切片长度: 5 当前切片容量: 8
当前切片长度: 6 当前切片容量: 8
当前切片长度: 7 当前切片容量: 8
当前切片长度: 8 当前切片容量: 8
当前切片长度: 9 当前切片容量: 16
这个例子展示了:切片如何引用底层数组。append
如何在容量允许的情况下修改底层数组,当 append
超出容量时,如何创建新的底层数组。
package main
import "fmt"
func main() {
// 创建原始水果切片
水果 := []string{"苹果", "香蕉", "樱桃"}
fmt.Println("原始水果切片:", 水果)
// 创建新切片(引用原切片的一部分)
新切片 := 水果[0:2]
fmt.Println("新切片(添加前):", 新切片)
// 使用append向新切片添加元素
新切片 = append(新切片, "枣")
fmt.Println("新切片(添加后):", 新切片)
fmt.Println("原始水果切片(新切片添加后):", 水果)
// 再次append,这次会超出原切片容量
新切片 = append(新切片, "无花果")
fmt.Println("新切片(第二次添加后):", 新切片)
fmt.Println("原始水果切片(新切片第二次添加后):", 水果)
}
//result
原始水果切片: [苹果 香蕉 樱桃]
新切片(添加前): [苹果 香蕉]
新切片(添加后): [苹果 香蕉 枣]
原始水果切片(新切片添加后): [苹果 香蕉 枣]
新切片(第二次添加后): [苹果 香蕉 枣 无花果] //注意无花果
原始水果切片(新切片第二次添加后): [苹果 香蕉 枣]
- 切片操作创建的是原数组的"视图",而不是新的独立数组。对这个视图(切片)进行修改可能会影响原始数组。
- 使用append时,如果超出了原数组的容量,Go会创建一个新的、更大的数组,这时就不会影响原始数组了。
2.5 切片删除元素
从切片中删除元素在Go语言中是一个常见的操作,但与添加元素不同,Go并没有提供内置的删除函数。我们需要使用一些技巧来实现这个功能。
package main
import "fmt"
func main() {
// 初始切片
s := []int{1, 2, 3, 4, 5}
fmt.Println("Original slice:", s)
// 1. 从切片中间删除元素
index := 2
s = append(s[:index], s[index+1:]...)
fmt.Println("After removing element at index 2:", s)
// 2. 从切片末尾删除元素
s = s[:len(s)-1]
fmt.Println("After removing last element:", s)
// 3. 从切片开头删除元素
s = s[1:]
fmt.Println("After removing first element:", s)
// 4. 删除多个连续元素
s = []int{1, 2, 3, 4, 5, 6, 7, 8}
start, end := 2, 5
s = append(s[:start], s[end:]...)
fmt.Println("After removing elements from index 2 to 4:", s)
// 5. 保持顺序的删除(适用于顺序不重要的情况)
s = []int{1, 2, 3, 4, 5}
index = 2
s[index] = s[len(s)-1]
s = s[:len(s)-1]
fmt.Println("After removing element at index 2 (order not preserved):", s)
}
//result
Original slice: [1 2 3 4 5]
After removing element at index 2: [1 2 4 5]
After removing last element: [1 2 4]
After removing first element: [2 4]
After removing elements from index 2 to 4: [1 2 6 7 8]
After removing element at index 2 (order not preserved): [1 2 5 4]
2.6 遍历切片
切片的遍历和数组类似,可以通过切片下标来进行遍历。
package main
import "fmt"
func main() {
var friend = []string{"Tom", "Ben", "Peter", "Danny"}
for k, v := range friend {
fmt.Println("切片下标:", k, ",对应元素", v)
}
}
//result
切片下标: 0 ,对应元素 Tom
切片下标: 1 ,对应元素 Ben
切片下标: 2 ,对应元素 Peter
切片下标: 3 ,对应元素 Danny
3. 映射(map)
类似于python的字典,当我们的程序中需要存放有关联关系的数据时,往往就会用到map。
定义:
- 映射是一种无序的键值对集合。
- 键必须是唯一的,且必须是可比较的类型(如整数、浮点数、字符串、结构体等)。
- 值可以是任何类型。
3.1 声明和初始化
- 声明映射
使用 map[KeyType]ValueType
语法声明。如:
var studentScoreMap map[string]int
- 初始化映射
1.在声明的同时初始化:我们可以在声明map的同时对其进行初始化:
可以使用字面量初始化:map[string]int{"key": value}
package main
import "fmt"
func main() {
//声明映射 + 初始化
var studentScoreMap = map[string]int{
"Tom": 80,
"Jerry": 85,
"Peter": 90,
}
fmt.Println(studentScoreMap)
}
//result
map[Tom:80 Ben:85 Peter:90]
2. 使用make()函数初始化:与切片的初始化类似,map也可以使用make()函数来进行初始化,格式如下:
使用 make
函数格式:make(map[键类型]值类型,map容量),如
map_variable := make(map[KeyType]ValueType, initialCapacity)
注意:使用make()函数初始化map时可以不指定map容量,但是对于map的多次扩充会造成性能 损耗。
cap()函数:只能用于获取切片的容量,无法获得map的容量,因此可以通过len()函数获取map的 当前长度
package main
import "fmt"
func main() {
//声明映射 + 初始化
var studentScoreMap = map[string]int{
"Tom": 80,
"Jerry": 85,
"Peter": 90,
}
fmt.Println(studentScoreMap)
fmt.Println("map长度为:", len(studentScoreMap))
}
//result
map[Jerry:85 Peter:90 Tom:80]
map长度为: 3
3.2 遍历映射
map的遍历主要通过for循环来完成,遍历时可同时获得map的键和值。遍历顺序是不确定的
package main
import "fmt"
func main() {
//声明映射 + 初始化
var studentScoreMap = map[string]int{
"Tom": 80,
"Jerry": 85,
"Peter": 90,
}
for k, v := range studentScoreMap {
fmt.Println(k, v)
}
}
//result
Tom 80
Jerry 85
Peter 90
3.3 映射操作
- 添加/修改元素:
map[key] = value
- 获取元素:
value := map[key]
- 删除元素:
delete(map, key)
注意:delete()函数会直接删除指定的键值对,而不是仅仅删除键或值。 另外,Go语言没有为map提供清空所有元素的方法,想要清空map的唯一方法就是重新定义一 个新的map
package main
import "fmt"
func main() {
// 创建一个 map
person := make(map[string]string)
// 添加/修改元素
person["name"] = "Alice"
person["age"] = "30"
fmt.Println("After adding elements:", person)
// 获取元素
name := person["name"]
age := person["age"]
fmt.Printf("Name: %s, Age: %s\n", name, age)
// 删除元素
delete(person, "age")
fmt.Println("After deleting age:", person)
}
//result
After adding elements: map[age:30 name:Alice]
Name: Alice, Age: 30
After deleting age: map[name:Alice]
3.4 并发操作map
对 map的操作都是在单协程的情况下完成的,这种情况下一般不会出现错误。如果是多个协程并发访 问一个map,就有可能会导致程序异常退出,具体示例程序如下:
package main
func main() {
GoMap := make(map[int]int)
for i := 0; i < 10000; i++ {
go writeMap(GoMap, i, i)
go readMap(GoMap, i)
}
}
func readMap(Gomap map[int]int, key int) int {
return Gomap[key]
}
func writeMap(Gomap map[int]int, key int, value int) {
Gomap[key] = value
}
//result
执行结果如下:
从运行结果可以发现,程序异常终止,原因是出现了严重错误:多个协程在尝试对map进行同 时写入。 由于map不是协程安全的,同一时刻只能有一个协程对map进行操作。
最常见的解决方案就是
- 互斥锁 (Mutex):
加锁的本质其实就是当前协程在对map操作前需先加上锁,加锁后其他任何协程无法对map进 行任何操作,直至当前协程解锁。样例如下:
package main
import (
"fmt"
"sync"
)
var lock sync.RWMutex
func main() {
GoMap := make(map[int]int)
for i := 0; i < 10000; i++ {
go writeMap(GoMap, i, i)
go readMap(GoMap, i)
}
fmt.Println("Done")
}
func readMap(Gomap map[int]int, key int) int {
lock.Lock()
m := Gomap[key]
lock.Unlock()
return m
}
func writeMap(Gomap map[int]int, key int, value int) {
lock.Lock()
Gomap[key] = value
lock.Unlock()
}
//result
Done
由于加锁对程序性能会有一定影响,因此,如果需要在多协程情况下对map进行操作,推荐使用Go在1.9版本中提供的一种效率较高的并发安全的map——sync.Map。
- sync.Map
在 Go 语言中,map
本身并不是线程安全的。如果多个 goroutine 同时读写一个 map,会导致数据竞争问题。要在并发环境中安全地操作 map,可以使用 sync.Mutex
或 sync.RWMutex
来保护 map 的访问,或者使用 sync.Map
,这是 Go 标准库提供的线程安全的 map 实现。
sync.Map有以下特点:
- 内部通过冗余的数据结构降低加锁对性能的影响。
- 使用前无须初始化,直接声明即可。
- sync.Map不使用map中的方式来进行读取和赋值等操作
package main
import (
"fmt"
"sync"
)
func main() {
var GoMap sync.Map
for i := 0; i < 10000; i++ {
go writeMap(GoMap, i, i)
go readMap(GoMap, i)
}
fmt.Println("Done")
}
func readMap(Gomap sync.Map, key int) int {
res, ok := Gomap.Load(key)
if ok {
return res.(int)
} else {
return 0
}
}
func writeMap(Gomap sync.Map, key int, value int) {
Gomap.Store(key, value)
}
//result
Done
注意:
- sync.Map无须使用make创建。
- Load()方法的第一个返回值是接口类型,需要将其转换为map值的类型。
- 目前sync.Map没有提供获取map数量的方法,解决方案是通过循环遍历map。
- 与较普通的map相比,sync.Map为了保证并发安全,会有性能上的损失,因此在非并发情况下,推荐使用map。
这两种方案的比较:
互斥锁 (Mutex) | sync.Map | |
优点 | 简单直接,适用于大多数场景 | 专为并发设计,在某些场景下性能更好,特别是当读操作远多于写操作时。 |
缺点 | 高并发情况下可能会有性能瓶颈 | API 不如普通 map 直观,不能像普通 map 那样使用 len() 函数 |
range
range是Go语言中非常常用的一个关键字,其主要作用就是配合for关键字对数组以及之后会介 绍到的切片和映射等数据结构进行迭代
语法 range的基本语法是 for index, value := range container { ... }
适用的数据类型
- 数组或切片:返回索引和值
- 字符串:返回字节索引和rune(Unicode码点)
- map:返回键和值
- channel:返回通道中的值
package main
import "fmt"
func main() {
// 1. 用于切片
fruits := []string{"apple", "banana", "cherry"}
for index, value := range fruits {
fmt.Printf("Index: %d, Value: %s\n", index, value)
}
// 2. 用于数组
numbers := [3]int{1, 2, 3}
for index, value := range numbers {
fmt.Printf("Index: %d, Value: %d\n", index, value)
}
// 3. 用于映射
ages := map[string]int{
"Alice": 25,
"Bob": 30,
"Charlie": 35,
}
for key, value := range ages {
fmt.Printf("Key: %s, Value: %d\n", key, value)
}
// 4. 用于字符串
for index, runeValue := range "Hello" {
fmt.Printf("Index: %d, Unicode: %U, Character: %c\n", index, runeValue, runeValue)
}
//result
Index: 0, Value: apple
Index: 1, Value: banana
Index: 2, Value: cherry
Index: 0, Value: 1
Index: 1, Value: 2
Index: 2, Value: 3
Key: Alice, Value: 25
Key: Bob, Value: 30
Key: Charlie, Value: 35
Index: 0, Unicode: U+0048, Character: H
Index: 1, Unicode: U+0065, Character: e
Index: 2, Unicode: U+006C, Character: l
Index: 3, Unicode: U+006C, Character: l
Index: 4, Unicode: U+006F, Character: o