数组
数组是值类型,因此改变副本的值,不会影响到本身
数组的定义:var 变量名 [元素数量] T
- 变量名(符合标识符要求即可)
- 元素数量(整型,可以是const中的值)
- T(可以是任意基本类型,包括数组本身,当类型为数组时,可以实现多维数组)
- var a [5]int 和 var a [10]int 是不同的类型
定义一个字符串数组,然后赋值:
var team [3]string team[0] = "hammer" team[1] = "soldier" team[2] = "mum" fmt.Println(team) 输出结果: [hammer soldier mum]用
也可以在声明时进行元素赋值:
var team [3]string = [3]string{"hammer", "soldier", "mum"} var team = [3]string{"hammer", "soldier", "mum"}
遍历数组:
打印索引值(访问越界,会触发panic)
var arrayA = [3]string{"hammer", "soldier", "mum"} for index, value := range arrayA { fmt.Println(index, value) } 运行结果: 0 hammer 1 soldier 2 mum
用匿名标识符忽略 index
var arrayA = [3]string{"hammer", "soldier", "mum"} for _, value := range arrayA { fmt.Println(value) } 运行结果: hammer soldier mum
多维数组
func main(){ var manyArray [2][5]int = [2][5]int{{1,2,3,4,5}, {6,7,8,9,10}} for rowIndex, rowValue := range manyArray{ for columnIndex, columnValue := range rowValue{ fmt.Printf("(%d %d)→%d ", rowIndex, columnIndex, columnValue) } } } 运行结果: (0 0)→1 (0 1)→2 (0 2)→3 (0 3)→4 (0 4)→5 (1 0)→6 (1 1)→7 (1 2)→8 (1 3)→9 (1 4)→10
数组排序
不能对数组进行排序
切片(Slice)
是一个拥有相同类型元素的可变长度的序列,是数组的引用(引用类型),包含地址,大小和容量,如下图,切片结构和内存分配:
从数组或切片生成新的切片
slice [start:end] start:开始位置索引,end:结束位置索引 (顾头不顾尾)
两个位置索引都缺省时,与数组或切片本身等效
两个位置索引都为0时,等效于空切片(用于清空元素)
var arrayA = [3]string{"a", "b", "c"} fmt.Println("arrayA[:2]:", arrayA[:2]) 运行结果: arrayA[:2]: [a b]
创建切片的两种方式(make 和 声明)
使用 make() 函数构造切片(推荐)
make([]T, size, cap)
- T(切片的元素类型)
- size(分配元素的数量)
- cap(预分配的元素数量)
var arrayA []int = make([]int, 2, 10) fmt.Println(arrayA) // [0 0] fmt.Println(len(arrayA)) // 2
使用 make() 函数生成的切片一定发生了内存分配操作。但从数组或切片中获取新切片只是将新的切片结构指向已经分配好的内存区域,设定开始与结束位置,不会发生内存分配操作
声明新的切片
// 声明字符串切片 var strList []string // 声明整型切片 var numList []int // 声明一个空切片 var numListEmpty = []int{}
空切片不为 nil
声明新的切片后,可以使用 append() 函数来添加元素 (数组不行)
使用 append() 函数为切片添加元素
内建函数 append() 函数可以为切片添加元素,每个切片会指向一片内存空间,这片空间可以容纳一定数量的元素,当空间不足时,切片就会进行“扩容”,扩容操作往往发生在 append() 调用时
切片在扩容时,容量的扩展规律按容量的 2 倍扩充,例如 1,2,4,8,16...
var numSlice []int for i:=0; i<10; i++{ numSlice = append(numSlice, i) fmt.Printf("len:%d, cap:%d, pointer:%p ", len(numSlice), cap(numSlice), numSlice) } 运行结果: len:1, cap:1, pointer:0xc00001c090 len:2, cap:2, pointer:0xc00001c0c0 len:3, cap:4, pointer:0xc0000162c0 len:4, cap:4, pointer:0xc0000162c0 len:5, cap:8, pointer:0xc000018200 len:6, cap:8, pointer:0xc000018200 len:7, cap:8, pointer:0xc000018200 len:8, cap:8, pointer:0xc000018200 len:9, cap:16, pointer:0xc00008e080 len:10, cap:16, pointer:0xc00008e080
len() 返回切片内元素数量,cap() 函数返回切片容量大小
往一个切片中不断添加元素的过程,类似于公司搬家。公司发展初期,资金紧张,人员很少,所以只需要很小的房间即可容纳所有的员工。随着业务的拓展和收入的增加就需要扩充工位,但是办公地的大小是固定的,无法改变。因此公司选择搬家,每次搬家就需要将所有的人员转移到新的办公点
- 员工和工位就是切片中的元素。
- 办公地就是分配好的内存。
- 搬家就是重新分配内存。
- 无论搬多少次家,公司名称始终不会变,代表外部使用切片的变量名不会修改。
- 因为搬家后地址发生变化,因此内存“地址”也会有修改
添加多个元素
numSlice = append(numSlice, 1, 2)
添加切片
var sliceA []string var sliceB []string sliceA = append(sliceA, "a") sliceB = append(sliceB, "b", "c", "d") sliceC := append(sliceA, sliceB...) fmt.Println(sliceA, sliceB, sliceC) 输出结果: [a] [b c d] [a b c d]
copy切片
内建函数 copy() ,可以将一个切片的数据复制到另外一个空间
使用格式:copy(destSlice, srcSlice)
package main import( "fmt" ) func main(){ const elementCount int = 10 var srcSlice []int = make([]int, elementCount) for i:=0;i<elementCount;i++{ srcSlice[i] = i } // 引用切片数据 refSlice := srcSlice // 制造切片,copy 0到5,返回发生copy元素的数量 var copySlice []int = make([]int, elementCount) num := copy(copySlice, srcSlice[:6]) fmt.Println("copy count:", num) fmt.Println(srcSlice) fmt.Println(copySlice) // 修改srcSlice的值,观察 refSlice 和 copySlice 值的变化 srcSlice[0]= 666 fmt.Printf("srcSlice[0]:%d, refSlice[0]:%d, copySlice[0]:%d ", srcSlice[0], refSlice[0], copySlice[0]) } 运行结果: copy count: 6 copy元素的数量 [0 1 2 3 4 5 6 7 8 9] srcSlice内容 [0 1 2 3 4 5 0 0 0 0] copySlice内容 srcSlice[0]:666, refSlice[0]:666, copySlice[0]:0
从切片中删除数据
并没有什么函数来提供删除切片元素,需要切片本身的特性来删除元素
例子:删除 Slice 中的 "c" 元素
package main import( "fmt" ) func main(){ var seq []string = []string{"a", "b", "c", "d", "e"} fmt.Printf("seq pointer:%p ", seq) subSeq1 := seq[:2] fmt.Printf("subSeq1 pointer:%p ", subSeq1) subSeq2 := seq[3:] fmt.Printf("subSeq2 pointer:%p ", subSeq2) // 将截取的Slice连接起来,达到删除元素的效果 newSeq := append(subSeq1, subSeq2...) fmt.Printf("newSeq:%v, pointer:%p ", newSeq, newSeq) } 运行结果: seq pointer:0xc000090000 subSeq1 pointer:0xc000090000 subSeq2 pointer:0xc000090030 newSeq:[a b d e], pointer:0xc000090000
代码的删除过程,本质是以被删除元素为分界点,将前后两个部分的内存重新连接起来,如下图:
切片排序
导入 sort 包
- sort.Ints 整数类型排序
- sort.Strings 字符串排序
- sort.Float64s 浮点数排序
func main(){ var sliceInt []int = []int{4,6,2,7,8,1} var sliceString []string = []string{"abc", "ab", "de", "d"} var sliceFloat64 []float64 = []float64{2.34, 5.6, 1.23} sort.Ints(sliceInt) sort.Strings(sliceString) sort.Float64s(sliceFloat64) fmt.Printf("%v %v %v ", sliceInt, sliceString, sliceFloat64) } 运行结果: [1 2 4 6 7 8] [ab abc d de] [1.23 2.34 5.6]
string
string 是值类型,底层是一个 byte 数组,因此,可以对 string 进行切片操作
Go 语言中,string本身是不可变的,如何改变 string 中的字符?
func main(){ var str string = "hello, world" // 将字符串转换成字符切片 var slice []byte = []byte(str) // 改变切片的值 slice[0] = 'o' // 再将切片转换成 string 类型 str = string(slice) fmt.Println(str) } 运行结果: oello, world
映射(map)
map 使用散列表(hash)实现,和 python 的 dictionary 一样,是无序的
创建 map 的两种方式(make 和 声明)
使用 make 创建一个 map(推荐)
func main(){ var scene map[string]int = make(map[string]int) scene["route"] = 1 fmt.Println(scene["route"]) fmt.Println(scene["route2"]) } 运行结果: 1 0
声明 map 并填充内容(字面量方式)
就像 json 格式一样,冒号的左边是 key,右边是值,键值对之间使用逗号分隔(最后一个值后面的冒号也不能少)
func main(){ var mapA map[string]string mapA = map[string]string{ "W": "forward", "A": "left", "D": "right", "S": "backward", } fmt.Println(mapA) fmt.Println(mapA["W"]) } 运行结果: map[A:left D:right S:backward W:forward] forward
注意:map 声明后是 nil 值,必须要初始化,直接使用触发 panic: assignment to entry in nil map
var mapA map[string]string mapA["name"]="johny"
查询某个 key 是否在 map 中存在
与 python 里面不同,如果 key 不存在会得到 value 类型的默认值
func main(){ var scene map[string]int = make(map[string]int) scene["route"] = 1 value, ok := scene["route1"] fmt.Println(value, ok) } 运行结果: 0 false
遍历 map
map 的遍历过程也是使用 for range 完成
func main(){ var mapA map[string]string mapA = map[string]string{ "W": "forward", "A": "left", "D": "right", "S": "backward", } for key, value := range mapA{ fmt.Println(key, value) } } 运行结果: W forward A left D right S backward
只需要遍历键时:
for key := range mapA{ fmt.Println(key) }
也可以使用匿名变量选择忽略键或值:
for key, _ := range mapA{ fmt.Println(key) }
需要注意的是,遍历输出元素的顺序与填充顺序无关,不能期望 map 在遍历时返回某种期望顺序的结果,如果你得到了顺序的结果,也不要庆幸,也许是当前的数据量不够大
如果需要特定顺序的遍历结果,正确的做法是排序:把字典的 key 或 value 放入到切片中,然后进行排序
func main(){ var mapA map[string]string mapA = map[string]string{ "W": "forward", "A": "left", "D": "right", "S": "backward", } var sliceA []string for key, _ := range mapA{ sliceA = append(sliceA, key) } // 对切片进行排序 sort.Strings(sliceA) fmt.Println(sliceA) } 运行结果: [A D S W]
map 的删除与清空
使用内建函数 delete() 从 map 中删除一组键值对(delete 没有返回值):
func main(){ var mapA map[string]string mapA = map[string]string{ "W": "forward", "A": "left", "D": "right", "S": "backward", } delete(mapA, "W") fmt.Println(mapA) } 运行结果: map[A:left D:right S:backward]
清空 map
but it didn't
唯一的方法就是重新 make 一个新 map,不用担心垃圾回收的效率,Go 语言中的并行垃圾回收效率比写一个清空函数高效多了
sync.Map
同时读写 map 的问题
map 在并发情况下,只读线程是安全的,同时读写线程不安全
下面来看下并发情况下读写 map 时会出现的问题:
package main // 不停的对map进行写入 func write(mapA map[int]int){ for { mapA[1] = 1 } } func main(){ var mapA map[int]int = make(map[int]int) go write(mapA) // 不停的对map进行读取 for { _ = mapA[1] } } 运行结果: fatal error: concurrent map read and map write
运行时输出提示:并发的 map 读写,也就是说使用了两个并发函数不断地对 map 进行读和写而发生了竞态问题,map 内部会对这种并发操作进行检查并提前发现
引入 sync.Map
并发读写时,一般的做法是加锁,但这样性能并不高,Go 在 1.9 版本中提供了一种效率较高的并发安全的 sync.Map,与 map 不同,它不是以语言原生形态提供,而是在 sync 包下的特殊结构
声明一个 sync.Map(不能使用 make 创建)
var scene sync.Map
sync.Map 特性:
- 不能使用 map 的操作方式不同,是使用 sync.Map 的方法进行调用,Store 用来存储,Load 用来获取,Delete 用来删除
- 遍历:使用 Range 配合一个回调函数进行遍历操作,通过回调函数返回内部遍历出来的值,Range 参数回调函数的返回值是 bool 类型,需要继续迭代时,返回 true,终止迭代时,返回 false
- 没有提供获取数量的方法,替代方法是遍历时自行计算数量
- sync.Map 为了保证并发安全,会有一些性能损失,因此在非并发情况下,使用 map
并发安全的 sync.Map 演示代码如下:
func main(){ var syncMap sync.Map // 设置值 syncMap.Store("hebei", "beijing") syncMap.Store("hubei", "wuhan") syncMap.Store(1, "shenzhen") // 取值 value1, err1 := syncMap.Load("hubei") value2, err2 := syncMap.Load("err") fmt.Println(value1, err1) fmt.Println(value2, err2) // 删除值(没有返回值) syncMap.Delete("hubei") // 遍历 sync.Map 中的键值对(只取第一对) syncMap.Range(func(key, value interface{}) bool { fmt.Println("iterate:", key, value) if key == "hebei"{ return false } else { return true } }) } 运行结果: wuhan true <nil> false iterate: hebei beijing
列表(list)
和 python 的列表很类似,它可以存放任意类型,可以插入数据,可以移除数据,可以获取数据(不能通过索引取值)
有两种方式初始化列表
通过 container/list 包的 New 方法:
listA := list.New()
直接声明一个 list:
var listA list.List
列表插入数据
双链表支持从队列前方或后方插入元素,分别对应的方法是 PushFront 和 PushBack
func main(){ var listA list.List // 返回一个元素句柄 (*list.Element 结构) element1 := listA.PushFront("first") element2 := listA.PushBack(666) fmt.Println(element1) fmt.Println(element2) } 运行结果: &{0xc0000701b0 0xc000070150 0xc000070150 first} &{0xc000070150 0xc000070180 0xc000070150 666}
列表删除元素
列表的插入函数的返回值会提供一个 *list.Element 结构,这个结构记录着列表元素的值及和其他节点之间的关系等信息。从列表中删除元素时,需要用到这个结构进行快速删除
func main(){ var listA list.List // 添加元素,返回一个元素句柄 (*list.Element 结构) element := listA.PushFront("first") fmt.Println(element) // 删除元素,返回这个元素的值 ret := listA.Remove(element) fmt.Println(ret) } 运行结果: &{0xc000084150 0xc000084150 0xc000084150 first} first
遍历列表
遍历双链表需要配合 Front() 函数获取头元素句柄,Value 属性可以获取句柄的值,遍历时只要元素不为空就可以继续进行。每一次遍历调用元素的 Next
func main(){ var listA list.List // 取句柄的值 element := listA.PushFront("first") listA.PushBack(666) fmt.Println(element.Value) // 遍历列表 for i:=listA.Front(); i!=nil; i=i.Next(){ fmt.Println(i.Value) } } 运行结果: first first 666
end~