前言:变量在一定程度上能满足函数及代码要求。如果编写一些复杂算法、结构和逻辑,就需要更复杂的类型来实现。这类复杂类型一般情况下具有各种形式的存储和处理数据的功能,将它们称为“容器(container)”。

Go语言数组详解

数组的声明

1
var 数组变量名 [数量]类型

其中:
数组的数量可以是一个表达式,但是最终通过编译期计算的结果必须是整型数值,也就是说元素数量不能含有到运算时才能确认的大小的数值。
Example:

1
var team [3]string

数组的初始化

数组可以在声明时使用初始化列表进行元素设置

1
var team = [3]string {"hammer", "soldier", "num"}

这种方式编写时,需要保证大括号后面的元素数量与数组的大小一致。但一般情况下,这个过程可以交给编译器,让编译器在编译时,根据元素个数确定数组大小。

1
var team[...]string {"hammer", "soldier", "num"}

...表示让编译器确定数组的大小。

数组的遍历

一般使用forrange搭配使用进行数组的遍历,遍历出来的kv分别为数组的键和值。

1
2
3
4
5
var team = [...]string {"hammer", "soldier", "num"}

for k, v := range team {
fmt.println(k, v)
}

切片(slice)

切片(Slice)是一个拥有相同类型元素的可变长度的序列。如果你对Python熟悉的话,那么切片你也不会陌生。
Go 语言切片的内部结构包含地址、大小和容量。
切片一般用于快速地操作一块数据集合。如果将数据集合比作切糕的话,切片就是你要的“那一块”。切的过程包含从哪里开始(这个就是切片的地址)及切多大(这个就是切片的大小)。

从数组或者切片生成新的切片

切片默认指向一段连续的内存区域,可以是数组也可以是切片本身。
从连续内存区域生成切片是常见的操作,格式如下:

1
切片对象[开始位置:结束位置]
  • 从数组生成切片
    1
    2
    var a = [3]int {1, 2, 3}
    fmt.Println(a, a[1:2])

重置(清空)切片

当把切片的开头和结束位置都设置为0的时候,生成的切片将是空的切片。

1
2
3
a := []int {1, 2, 3}
//置空操作
b := a[0:0]

直接声明新的切片

除了可以在数组和切片中生成切边之外,也可直接声明各种类型的切片,切片声明格式如下:

1
var name []type

声明各种切片的例子:

1
2
3
4
5
6
//字符串切片
var strSlice []string
//整型切片
var intSlice []int
//空的整型切片
var emptyIntSlice []int{}

声明但是没有使用的切片默认值是nil,但是声明空的切片是没有元素填充的,所以切片是空的,不是nil,但是已经被分配了内存,只是没有元素。

使用make()函数构造切片

如果需要动态的创建一个切片,可以使用make()内建函数,格式如下:

1
make([]type, size, cap)
  • type: 切片的元素的类型
  • size: 为这个类型的切片分配多少个元素
  • cap: 预分配元素数量,做个值设定后不影响size,只是提前分配空间,降低多次分配空间造成的性能问题。
    1
    2
    a := make([]int, 2)
    b := make([]int, 2, 10)

a,b是预先分配的2个元素的切片,只是b的内部存储空间已经分配了10个,但实际只使用了2个元素。容量不会影响当前元素的个数,len(a),len(b)都是2。
提示:
使用make()函数生成的切片一定发生了内存分配操作,但给定开始于结束位置(包括切片复位)的切片只是将新的切片结构指向已经分配好的内存区域,设定开始于结束位置,不会发生内存分配操作。
切片不一定必须经过make()函数才能实现,生成切片,声明后使用append()函数均可以正常使用切片。

append()为切片添加元素

append() 可以为切片动态添加元素,每个切片会指向一片内存空间,这片空间能容纳一定数量的元素。当空间不能容纳足够多的元素时,切片就会进行“扩容”。“扩容”操作往往发生在 append() 函数调用时。
切片在扩容的时候,扩容规律按照容量的2倍数扩充

1
2
3
4
5
6
var car []string
car = append(car, "OldDriver")
car = append(car, "Ice", "Sniper", "Monk")
team := []string{"pig", "FlyingCake", "Chicken"}
car = append(car, team...)
//在team后面加了...表示将team整个添加到car后面

copy()切片复制(切片拷贝)

使用 Go 语言内建的 copy() 函数,可以迅速地将一个切片的数据复制到另外一个切片空间中,copy() 函数的使用格式如下:

1
copy(复制目标, 复制来源[]type)int

从切片中删除元素

需要声明的是,go语言本身是没有对删除切片元素提供专用的语法或者接口,需要使用切片的特性来删除元素。

Go 语言中切片删除元素的本质是:以被删除元素为分界点,将前后两个部分的内存重新连接起来。

map()映射

大多数语言的映射关系容器使用两种算法:散列表和平衡树
go语言提供的关系映射容器为map,map使用散列表(hash)实现。
go语言中map的定义:

1
2
3
4
map[键类型]值类型

scene := make(map[string]int)
scene["route"] = 66

某些情况下,需要明确知道查询中某个键是否在 map 中存在,可以使用一种特殊的写法来实现,看下面的代码:

1
v, ok = senen["route"]

在默认获取键值的基础上,多取了一个变量 ok,可以判断键 route 是否存在于 map 中,其中ok的值为bool值。
map 还有一种在声明时填充内容的方式,代码如下:

1
2
3
4
5
6
m := map[string]string{
"W": "forward",
"A": "left",
"D": "right",
"S": "backward",
}

遍历map

map 的遍历过程使用 for range 循环完成

1
2
3
4
5
6
7
8
9
10
m := map[string]string{
"W": "forward",
"A": "left",
"D": "right",
"S": "backward",
}

for k, v := range m {
fmt.Println(k, v)
}

如只遍历值,将不需要的键改为匿名变量形式,可以使用下面的形式:

1
2
3
for _, v := range m {
...
}

如只遍历键时,使用下面的形式:

1
2
3
for K := range m {
...
}

map元素的删除和清空

使用delete()函数从map()中删除键值对
使用delete()函数从map中删除一组键值对,格式如下:

1
delete(map, 键)

清空map中的所有元素
和切片的元素删除一样,go语言同样没有为map提供清空所有元素的函数、方法,清空map的唯一方法就是重新make一个新的map,不用担心垃圾回收的效率,Go 语言中的并行垃圾回收效率比写一个清空函数高效多了。

sync.Map(在并发环境中使用的map)

go语言作为一门为高并发设计的语言,所以高并发状态是几乎不可避免的。但是go语言中的map在并发情况下,只读是线程安全的,同时读写是线程不安全的。
模拟一个并发情况下的读写map的问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
m := make(map[int]int)
//开启一段并发代码
go func(){
//不停的对map进行写入
for {
m[1] = 1
}
}()

//开启另外一个并发代码
go func(){
//不停的进行读写
for {
_ = m[1]
}
}()

//无限循环,让并发程序在后台执行
for {

}

运行代码会报错,运行时输出提示:并发的 map 读写。也就是说使用了两个并发函数不断地对 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:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    package main
    import (
    "fmt"
    "sync"
    )

    func main() {
    //声明一个sync.Map类型映射
    var scene sync.Map

    //将键值对存储到sync.Map
    scene.Store("greece", 97)
    scene.Store("london", 100)
    scene.Store("egypt", 200)

    //从sync.Map中根据键取值
    value := scene.Load("london")
    fmt.Println(value)

    //根据键删除对应的键值对
    scene.Delete("london")

    //遍历所有的sync.Map中的键值对
    scene.Range(func(k, v interface{}) bool {
    fmt.Println("iterate:", k, v)
    return true
    })
    }

sync.Map 没有提供获取 map 数量的方法,替代方法是获取时遍历自行计算数量。sync.Map 为了保证并发安全有一些性能损失,因此在非并发情况下,使用 map 相比使用 sync.Map 会有更好的性能。

list(列表)

列表是一种非连续的存储的容器(container),由多个节点组成,节点通过一些变量记录彼此的之间的关系。列表有多种实现方式,比如单链表、双链表等。
列表的原理可以这样理解:假设 A、B、C 三个人都有电话号码,如果 A 把号码告诉给 B,B 把号码告诉给 C,这个过程就建立了一个单链表结构,如下图所示。
图1. 三人单向通知电话码形成单像链表
如果在这个基础上,再从c开始将自己的电话号码给自己知道的人,这样就形成了双向链表,如下图所示:
图2. 三人相互通知电话号码形成双向链表
那么如果需要获得所有人的号码,只需要从 A 或者 C 开始,要求他们将自己的号码发出来,然后再通知下一个人如此循环。这个过程就是列表遍历。
如果 B 换号码了,他需要通知 A 和 C,将自己的号码移除。这个过程就是列表元素的删除操作,如下图所示。
图3. 从双向链表中删除一个人的电话号码
在go语言中,列表的实现使用container/list包来实现,内部实现采用双向链表,列表能够高效的进行任意位置的元素插入和删除操作。

初始化列表

go语言的列表初始化有两种方式:New和声明

  • New方法初始化列表

    1
    变量名 := list.New()
  • 通过声明初始化列表

    1
    var 变量名 list.List

列表与切片和 map 不同的是,列表并没有具体元素类型的限制。因此,列表的元素可以是任意类型。这既带来便利,也会引来一些问题。给一个列表放入了非期望类型的值,在取出值后,将 interface{} 转换为期望类型时将会发生宕机。

列表中插入元素

双向链表支持从头部或者尾部插入元素,分别对应的方法是PushFront和PushBack
提示:
这两个方法都会返回一个*list.Element结构,如果在以后的使用中需要执行删除插入元素的操作,则只能通过*list.Element配合Remove()方法进行删除,这种方法可以让删除更加效率化,这也是双向链表的特性之一。

1
2
3
4
5
6
7
//使用New方式进行初始化列表
list_a := list.New()
//使用声明初始化列表
var list_b list.List
//添加元素的操作
list_a.PushFront("First")
list_b.PushBack("Last")

从列表中删除元素

列表的插入函数的返回值会提供一个 *list.Element 结构,这个结构记录着列表元素的值及和其他节点之间的关系等信息。从列表中删除元素时,需要用到这个结构进行快速删除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

improt "container/list"

func main() {
list_test := list.New()
list_test.PushBack("canon")
list_test.PushFront(67)
//尾部天骄元素之后保存元素句柄
element := list_test.PushBack("first")
//在first之后添加high
list_test.InsertAfter("high", element)
//在first之前添加low
list_test.InsertBefore("noon", element)
//使用remove进行删除元素操作
list_test.Remove(element)
}

列表的遍历

访问双向链表需要配合Front()函数获取头元素,遍历的时候只要元素不为空就可以继续进行遍历,每次遍历调用函数的Next,是不是有点像Python的生成器?

1
2
3
4
5
6
var list_b := list.List
list_b.PushBack("canon")
list_b.PushFront(67)
for i := list_b.Front(); i != nil; i = i.Next() {
fmt.Println(i.Value)
}

使用for语句进行遍历,其中i:=list_b.Front()表示初始赋值,只会在一开始执行一次;每次循环会进行一次 i!=nil语句判断,如果返回false,表示退出循环,反之则会执行i=i.Next()