array 数组
介绍
- 数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。因为数组的长度是固定的,所以在Go语言中很少直接使用数组。
- 数组的每个元素都可以通过索引下标来访问,索引下标的范围是从 0 开始,内置函数 len() 可以返回数组中元素的个数。
- 数组在Go中为值类型;(这是和Java非常不同的一点)
- 数组之间可以使用
==
或者!=
进行比较,但不可以使用<
或>
- 可以使用
new
来创建数组,此方法返回一个指向数组的指针;var 数组变量名 [元素数量]Type
- 数组变量名:数组声明及使用时的变量名。
- 元素数量:数组的元素数量,可以是一个表达式,但最终通过编译期计算的结果必须是整型数值,元素数量不能含有到运行时才能确认大小的数值。
- Type:可以是任意基本类型,包括数组本身,类型为数组本身时,可以实现多维数组。
var a [2]string{"hello"}
a[1] = "world"
fmt.Println(a[0], a[1], a[len(a)-1]) // hello word word
fmt.Println(a) // [hello world]
注意
零值
如果数组的元素没有被赋值,那么每个元素都会被初始化为元素类型对应的零值。
var r [3]int = [3]int{1, 2}
fmt.Println(r[2]) // "0"
省略号
如果在数组长度的位置出现“…”省略号,则表示数组的长度是根据初始化值的个数来计算。
q := [...]int{1, 2, 3}
fmt.Printf("%T\n", q) // "[3]int"
长度决定类型
数组的长度是数组类型的一个组成部分,因此 [3]int 和 [4]int 是两种不同的数组类型,数组的长度必须是常量表达式,因为数组的长度需要在编译阶段确定。
q := [3]int{1, 2, 3}
q = [4]int{1, 2, 3, 4} // 编译错误:无法将 [4]int 赋给 [3]int
区分 指向数组的指针 和 指针数组
package main
import "fmt"
func main() {
//指向数组的指针
var p *[10]int=&a
fmt.Print(p) // &[0,0,0,0,0,0,0,0,0]
//指针数组
x,y :=1,2
a:=[...]*int{&x,&y}
fmt.Print(a) // [0xc0420080c0 0xc0420080c8]
}
比较两个数组是否相等
- 使用运算符(
==
和!=
)来判断两个数组是否相等,两个数组类型相同(包括数组的长度,数组中元素的类型)且所有元素都是相等的时候数组才是相等的。 - 不能比较两个类型不同的数组,否则程序将无法完成编译。
a := [2]int{1, 2}
b := [...]int{1, 2}
c := [2]int{1, 3}
fmt.Println(a == b, a == c, b == c) // "true false false"
d := [3]int{1, 2}
fmt.Println(a == d) // 编译错误:无法比较 [2]int == [3]int
多维数组
介绍
- 因为数组属于值类型,所以多维数组的所有维度都会在创建时自动初始化零值。
// array_name 为数组的名字
// size1、size2 等等为数组每一维度的长度
// array_type 为数组的类型
var array_name [size1][size2]...[sizen] array_type
二维数组
二维数组是最简单的多维数组,二维数组本质上是由多个一维数组组成的。// 声明一个二维整型数组,两个维度的长度分别是 4 和 2
var array [4][2]int
// 使用数组字面量来声明并初始化一个二维整型数组
array = [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}
// 声明并初始化数组中索引为 1 和 3 的元素
array = [4][2]int{1: {20, 21}, 3: {40, 41}}
// 声明并初始化数组中指定的元素
array = [4][2]int{1: {0: 20}, 3: {1: 41}}
示例
二维数组的每个元素赋值
// 声明一个 2×2 的二维整型数组
var array [2][2]int
// 设置每个元素的整型值
array[0][0] = 10
array[0][1] = 20
array[1][0] = 30
array[1][1] = 40
同样类型的多维数组赋值
// 声明两个二维整型数组
var array1 [2][2]int
var array2 [2][2]int
// 为array2的每个元素赋值
array2[0][0] = 10
array2[0][1] = 20
array2[1][0] = 30
array2[1][1] = 40
// 将 array2 的值复制给 array1
array1 = array2
使用索引为多维数组赋值
// 将 array1 的索引为 1 的维度复制到一个同类型的新数组里
var array3 [2]int = array1[1]
// 将数组中指定的整型值复制到新的整型变量里
var value int = array1[1][0]
slice 切片
介绍
- 切片(slice)是对数组的一个连续片段的引用,其本身并不是数组,它指向底层的数组。切片是一个引用类型。
- 切片默认指向一段连续内存区域,可以是数组,也可以是切片本身。
- 一般使用
make()
创建,使用len()
获取元素个数,cap()
获取容量; - 如果多个slice指向相同的底层数组,其中一个的值改变会影响全部。
声明
数组生成新切片
从数组或切片生成新的切片拥有如下特性:
- 取出的元素数量为:结束位置(取出时不包含结束位置对应的索引) - 开始位置;
- 当缺省开始位置时,表示从开头开始取;
- 当缺省结束位置时,表示截取到末尾;
- 两者同时缺省时,与切片本身等效;
- 两者同时为 0 时,等效于空切片,一般用于切片复位。
- 根据索引位置取切片时,超界会报运行时错误。 ```go slice:表示目标切片对象; 开始位置:对应目标切片对象的索引; 结束位置:对应目标切片的结束索引。
slice [开始位置 : 结束位置]
var a = [3]int{1, 2, 3} fmt.Println(a) // [1 2 3] fmt.Println(a[1:2]) // [2] fmt.Println(a[:2]) // [1 2] fmt.Println(a[1:]) // [2 3] fmt.Println(a[:]) // [1 2 3] fmt.Println(a[0:0]) //[]
<a name="zo82e"></a>
#### 声明新切片
1. 未被初始化的切片为nil。
```go
var name []Type
// 声明字符串切片
var strList []string
// 声明整型切片
var numList []int
// 声明一个空切片
var numListEmpty = []int{}
// 输出3个切片
fmt.Println(strList, numList, numListEmpty)
// 输出3个切片大小
fmt.Println(len(strList), len(numList), len(numListEmpty))
// 切片判定空的结果
fmt.Println(strList == nil)
fmt.Println(numList == nil)
fmt.Println(numListEmpty == nil)
// log
[] [] []
0 0 0
true
true
false
make 构造切片
介绍
- 动态地创建一个切片,可以使用 make() 内建函数。该方式创建后已对切片进行初始化。
- 使用 make() 函数生成的切片一定发生了内存分配操作,但给定开始与结束位置(包括切片复位)的切片只是将新的切片结构指向已经分配好的内存区域,设定开始与结束位置,不会发生内存分配操作。
make( []Type, size, cap )
- Type 是指切片的元素类型。
- size 指的是为这个类型分配多少个元素。
- cap 为预分配的元素数量,这个值设定后不影响 size,只是提前分配空间,降低多次分配空间造成性能问题。
示例
a := make([]int, 2)
b := make([]int, 2, 10)
fmt.Println(a, b)
fmt.Println(len(a), len(b), cap(b))
fmt.Println(a == nil)
// log
[0 0] [0 0]
2 2 10
false
append 添加元素
介绍
- 使用 append() 函数为切片添加元素。
- 如果最终长度未超过追加到slice的容量则返回原始slice,如果超过追加到的slice的容量则将重新分配内容地址并拷贝原始数据。
- 如果空间不足以容纳足够多的元素,切片就会进行“扩容”,此时新切片的长度会发生改变。容量的扩展规律是按容量的 2 倍数进行扩充,例如 1、2、4、8、16……。
添加元素
尾部追加
var a []int
a = append(a, 1) // 追加1个元素
a = append(a, 1, 2, 3) // 追加多个元素, 手写解包方式
a = append(a, []int{1,2,3}...) // 追加一个切片, 切片需要解包
开头添加
切片开头添加元素一般都会导致内存的重新分配,而且会导致已有元素全部被复制 1 次,因此,从切片的开头添加元素的性能要比从尾部追加元素的性能差很多。
var a = []int{1,2,3}
a = append([]int{0}, a...) // 在开头添加1个元素
a = append([]int{-3,-2,-1}, a...) // 在开头添加1个切片
链式操作
因为 append 函数返回新切片的特性,所以切片也支持链式操作,我们可以将多个 append 操作组合起来。
var a []int
a = append(a[:i], append([]int{x}, a[i:]...)...) // 在第i个位置插入x
a = append(a[:i], append([]int{1,2,3}, a[i:]...)...) // 在第i个位置插入切片
自动扩容
var numbers []int
for i := 0; i < 10; i++ {
numbers = append(numbers, i)
fmt.Printf("len: %d cap: %d pointer: %p\n", len(numbers), cap(numbers), numbers)
}
// log
len: 1 cap: 1 pointer: 0xc0420080e8
len: 2 cap: 2 pointer: 0xc042008150
len: 3 cap: 4 pointer: 0xc04200e320
len: 4 cap: 4 pointer: 0xc04200e320
len: 5 cap: 8 pointer: 0xc04200c200
len: 6 cap: 8 pointer: 0xc04200c200
len: 7 cap: 8 pointer: 0xc04200c200
len: 8 cap: 8 pointer: 0xc04200c200
len: 9 cap: 16 pointer: 0xc042074000
len: 10 cap: 16 pointer: 0xc042074000
删除元素
介绍
- Go语言并没有对删除切片元素提供专用的语法或者接口,需要使用切片本身的特性来删除元素
- 根据要删除元素的位置有三种情况:
- 从开头位置删除
- 从中间位置删除
- 从尾部删除(速度最快)
- 删除开头的元素和删除尾部的元素都可以认为是删除中间元素操作的特殊情况。
Go语言中删除切片元素的本质是,以被删除元素为分界点,将前后两个部分的内存重新连接起来。
连续容器的元素删除无论在任何语言中,都要将删除点前后的元素移动到新的位置,随着元素的增加,这个过程将会变得极为耗时,因此,当业务需要大量、频繁地从一个切片中删除元素时,如果对性能要求较高的话,就需要考虑更换其他的容器了(如双链表 list 等能快速从删除点删除元素)。
删除
从开头删除
删除开头的元素可以直接移动数据指针。
a := []int{1, 2, 3}
// 删除开头1个元素
a = a[1:] // [2,3]
也可以不移动数据指针,但是将后面的数据向开头移动,可以用 append 原地完成(所谓原地完成是指在原有的切片数据对应的内存区间内完成,不会导致内存空间结构的变化)
a := []int{1, 2, 3}
a = append(a[:0], a[1:]...) // 删除开头1个元素
a = append(a[:0], a[N:]...) // 删除开头N个元素
还可以用 copy() 函数来删除开头的元素:
a := []int{1, 2, 3}
a = a[:copy(a, a[1:])] // 删除开头1个元素
a = a[:copy(a, a[N:])] // 删除开头N个元素
从中间位置删除
删除中间的元素,需要对剩余的元素进行一次整体挪动,同样可以用 append 或 copy 原地完成:
a = []int{1, 2, 3, ...}
a = append(a[:i], a[i+1:]...) // 删除中间1个元素
a = append(a[:i], a[i+N:]...) // 删除中间N个元素
a = a[:i+copy(a[i:], a[i+1:])] // 删除中间1个元素
a = a[:i+copy(a[i:], a[i+N:])] // 删除中间N个元素
从尾部删除
a = []int{1, 2, 3}
a = a[:len(a)-1] // 删除尾部1个元素
a = a[:len(a)-N] // 删除尾部N个元素
copy 拷贝
介绍
- 内置函数 copy() 可以将一个数组切片复制到另一个数组切片中。
- 如果加入的两个数组切片不一样大,就会按照其中较小的那个数组切片的元素个数进行复制。
- 目标切片必须分配过空间且足够承载复制的元素个数,并且来源和目标的类型必须一致,copy() 函数的返回值表示实际发生复制的元素个数。
// 将 srcSlice 复制到 destSlice
copy( destSlice, srcSlice []T) int
示例
切片大小不一致
```go slice1 := []int{1, 2, 3, 4, 5} slice2 := []int{5, 4, 3} copy(slice2, slice1) // 只会复制slice1的前3个元素到slice2中 fmt.Println(slice2) copy(slice1, slice2) // 只会复制slice2的3个元素到slice1的前3个位置 fmt.Println(slice1)
// log [1 2 3] [1 2 3 4 5]
<a name="qHZpi"></a>
##### 复制对原值影响
```go
func main() {
// 设置元素数量为1000
const elementCount = 1000
// 预分配足够多的元素切片
srcData := make([]int, elementCount)
// 将切片赋值
for i := 0; i < elementCount; i++ {
srcData[i] = i
}
// 引用切片数据
refData := srcData
// 预分配足够多的元素切片
copyData := make([]int, elementCount)
// 将数据复制到新的切片空间中
copy(copyData, srcData)
// 修改原始数据的第一个元素
srcData[0] = 999
// 打印引用切片的第一个元素
fmt.Println(refData[0])
// 打印复制切片的第一个和最后一个元素
fmt.Println(copyData[0], copyData[elementCount-1])
// 复制原始数据从4到6(不包含)
copy(copyData, srcData[4:6])
for i := 0; i < 5; i++ {
fmt.Printf("%d ", copyData[i])
}
}
// log
999
0 999
4 5 2 3 4
多维切片
sliceName 为切片的名字
sliceType为切片的类型,每个[]代表着一个维度,切片有几个维度就需要几个[]
var sliceName [][]...[]sliceType
// 声明一个二维整型切片并赋值
slice := [][]int{{10}, {100, 200}}
// 为第一个切片追加值为 20 的元素
slice[0] = append(slice[0], 20)
list 列表
链表
- 列表是一种非连续的存储容器,由多个节点组成,节点通过一些变量记录彼此之间的关系。列表有多种实现方法,如单链表、双链表等。
- 列表能够高效地进行任意位置的元素插入和删除操作。
单链表
A、B、C 三个人都有电话号码,如果 A 把号码告诉给 B,B 把号码告诉给 C,这个过程就建立了一个单链表结构
多链表
如果在这个基础上,再从 C 开始将自己的号码告诉给自己所知道号码的主人,这样就形成了双链表结构。
遍历过程
那么如果需要获得所有人的号码,只需要从 A 或者 C 开始,要求他们将自己的号码发出来,然后再通知下一个人如此循环,这样就构成了一个列表遍历的过程。删除过程
如果 B 换号码了,他需要通知 A 和 C,将自己的号码移除,这个过程就是列表元素的删除操作。
介绍
- Go语言中,列表使用 container/list 包来实现,内部的实现原理是双链表。
- list与slice和 map 不同的是,列表并没有具体元素类型的限制,因此,列表的元素可以是任意类型,这既带来了便利,也引来一些问题,例如给列表中放入了一个 interface{} 类型的值,取出值后,如果要将 interface{} 转换为其他类型需要使用断言,增加了代码量。
初始化
list 的初始化有两种方法:分别是使用 New() 函数和 var 关键字声明,两种方法的初始化效果都是一致的。
变量名 := list.New()
var 变量名 list.List
//demo
list1 := list.New()
var list2 list.List
插入元素
普通插入
- 双链表支持从队列前方或后方插入元素,分别对应的方法是 PushFront 和 PushBack。
l := list.New()
l.PushBack("fist")
l.PushFront(67)
提示
这两个方法都会返回一个 list.Element 结构,如果在以后的使用中需要删除插入的元素,则只能通过 list.Element 配合 Remove() 方法进行删除,这种方法可以让删除更加效率化,同时也是双链表特性之一。
其他插入
方 法 | 功 能 |
---|---|
InsertAfter(v interface {}, mark Element) Element | 在 mark 点之后插入元素,mark 点由其他插入函数提供 |
InsertBefore(v interface {}, mark Element) Element | 在 mark 点之前插入元素,mark 点由其他插入函数提供 |
PushBackList(other *List) | 添加 other 列表元素到尾部 |
PushFrontList(other *List) | 添加 other 列表元素到头部 |
删除元素
- 列表插入函数的返回值会提供一个 *list.Element 结构,这个结构记录着列表元素的值以及与其他节点之间的关系等信息,从列表中删除元素时,需要用到这个结构进行快速删除。
func main() {
l := list.New()
// 尾部添加
l.PushBack("canon")
// 头部添加
l.PushFront(67)
// 尾部添加后保存元素句柄
element := l.PushBack("fist")
// 在fist之后添加high
l.InsertAfter("high", element)
// 在fist之前添加noon
l.InsertBefore("noon", element)
// 使用
l.Remove(element)
}
遍历列表
遍历双链表需要配合 Front() 函数获取头元素,遍历时只要元素不为空就可以继续进行,每一次遍历都会调用元素的 Next() 函数。l := list.New()
// 尾部添加
l.PushBack("canon")
// 头部添加
l.PushFront(67)
for i := l.Front(); i != nil; i = i.Next() {
fmt.Println(i.Value)
}
map 集合
介绍
- map 是一种元素对pair(key-value)的无序集合,类似其他语言的哈希表或字典。是引用类型。
- 未初始化的 map 的值是 nil,使用函数 len() 可以获取 map 中 pair 的数目。
- map查找比线性搜索快很多,但比使用索引访问数据的类型慢100倍;
- map使用
make([keyType] valueType,cap)
创建,支持:=
这种简写方式。cap
表示容量,可省略;超出容量时会自动扩容,但尽量提供一个合理的初始值。 ```go mapname 为 map 的变量名 keytype 为键类型 valuetype 是键对应的值类型
var mapname map[keytype]valuetype
var m map[string]int
<a name="AY6qE"></a>
### 声明
<a name="xB2jZ"></a>
#### 普通声明
将音阶和对应的音频映射
```go
noteFrequency := map[string]float32 {
"C0": 16.35, "D0": 18.35, "E0": 20.60, "F0": 21.83,
"G0": 24.50, "A0": 27.50, "B0": 30.87, "A4": 440}
make 构造切片
map 可以根据新增的 key-value 动态的伸缩,因此它不存在固定长度或者最大限制,但是也可以选择标明 map 的初始容量 cap。
m := make(map[string]int, 10)
fmt.Println(m, len(m)) // [] 0
当 map 增长到容量上限的时候,如果再增加新的 key-value,map 的大小会自动加 1,所以出于性能的考虑,对于大的 map 或者会快速扩张的 map,即使只是大概知道容量,也最好先标明。
用 slice 作为map 的值
mp1 := make(map[int][]int)
mp2 := make(map[int]*[]int)
delete 删除
删除
Go语言提供了一个内置函数 delete(),用于删除map内的元素。
delete(map, 键)
示例:
scene := make(map[string]int)
// 准备map数据
scene["route"] = 66
scene["brazil"] = 4
scene["china"] = 960
delete(scene, "brazil")
for k, v := range scene {
fmt.Println(k, v)
}
// log
route 66
china 960
清空
Go语言中并没有为 map 提供任何清空所有元素的函数。
- 清空 map 的唯一办法就是重新 make 一个新的 map,不用担心垃圾回收的效率,Go语言中的并行垃圾回收效率比写一个清空函数要高效的多。
sync.Map 并发安全
介绍
- map 在并发情况下,只读是线程安全的,同时读写是线程不安全的。
- 需要并发读写时,一般的做法是加锁,但这样性能并不高,Go语言在 1.9 版本中提供了一种效率较高的并发安全的 sync.Map,sync.Map 和 map 不同,不是以语言原生形态提供,而是在 sync 包下的特殊结构。
- sync.Map 有以下特性:
- 无须初始化,直接声明即可。
- sync.Map 不能使用 map 的方式进行取值和设置等操作,而是使用 sync.Map 的方法进行调用,Store 表示存储,Load 表示获取,Delete 表示删除。
- 使用 Range 配合一个回调函数进行遍历操作,通过回调函数返回内部遍历出来的值,Range 参数中回调函数的返回值在需要继续迭代遍历时,返回 true,终止迭代遍历时,返回 false。
- sync.Map 没有提供获取 map 数量的方法,替代方法是在获取 sync.Map 时遍历自行计算数量。
使用
map
// 创建一个int到int的映射
m := make(map[int]int)
// 开启一段并发代码
go func() {
// 不停地对map进行写入
for {
m[1] = 1
}
}()
// 开启一段并发代码
go func() {
// 不停地对map进行读取
for {
_ = m[1]
}
}()
// 无限循环, 让并发程序在后台执行
for {
}
panic:fatal error: concurrent map read and map write
sync.Map
func main() {
var scene sync.Map
// 将键值对保存到sync.Map
scene.Store("greece", 97)
scene.Store("london", 100)
scene.Store("egypt", 200)
// 从sync.Map中根据键取值
fmt.Println(scene.Load("london"))
// 根据键删除对应的键值对
scene.Delete("london")
// 遍历所有sync.Map中的键值对
scene.Range(func(k, v interface{}) bool {
fmt.Println("iterate:", k, v)
return true
})
}
// log
100 true
iterate: egypt 200
iterate: greece 97