Go 语言数组类型的值(以下简称数组)的长度是固定的,而切片类型的值(以下简称切片)是可变长的。切片看做是对数组的一层简单的封装,因为在每个切片的底层数据结构中,一定会包含一个数组。数组可以被叫做切片的底层数组,而切片也可以被看作是对数组的某个连续片段的引用。
注意: 在 Go 语言中,不用判断“传值”或者“传引用”, 只要看作被传递的值的类型就好了。如果传递的值是引用类型的,那么就是“传引用”。如果传递的值是值类型的,那么就是“传值”。从传递成本的角度讲,引用类型的值往往要比值类型的值低很多。
数组
Go 语言提供了数组类型的数据结构。
数组是具有相同唯一类型的一组已编号且长度固定的数据项序列,这种类型可以是任意的原始类型例如整形、字符串或者自定义类型。数组容量不会发生变化。
声明数组语句:
// var variable_name [SIZE] variable_type
var balance [10] float32
初始化数组
// 初始化元素的个数不能超过 []中的数字。
var array1 = [5]float32{100.0, 2.0, 1.0, 7.0, 4.0}
// 根据 {} 内数据的数量来确认数组长度
var array2 = [...]float32{1000.0, 2.0, 3.4}
// 初始化同时给指定索引赋值,结果为 [0, 0, 1, 2, 3]
var array3 = [5] int { 2:1,3:2,4:3}
访问数组
// 通过索引来读取数组数据,索引从 0 开始
var salary float32 = balance[9] // 读取 balance 数组第 10 个数据
遍历数组
//下标遍历数组
for i := 0; i < len(array); i++ {
fmt.Println("i=", i, array[i])
}
//range遍历数组
for k, v := range array {
fmt.Println(k, v) // k 指数组索引,v 指索引对应的值
}
// 使用 _ 来省略索引
for _, value := range strArr {
fmt.Println("value=", value)
}
数组比较
数组的长度、元素的类型相同的情况下,可以通过较运算符(==
和!=
)判断数组是否相等,不能比较两个类型不同的数组,否则程序将无法完成编译。
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"
数组作为函数参数
函数传递数组参数,你需要在函数定义时,声明形参为数组:
// 函数指定数组长度
void myFunction(param [10]int)
// 函数未指定数组长度
void myFunction(param []int)
- 未定义长度的数组只能传给不限制数组长度的函数
- 定义了长度的数组只能传给限制了相同数组长度的函数
注意:
Go 语言的数组是值,其长度是其类型的一部分,作为函数参数时,是 值传递,函数中的修改对调用者不可见
[10]int
和[20]int
是不同类型- 数组作为函数参数传递的是副本,函数内修改数组并不改变原来的数组。如果想要修改,需要传入数组指针。
- Go 语言一般不直接使用 数组,而是使用切片
切片
Go 语言切片是对数组的抽象,是对数组的一个连续片段的引用,所以切片是一个引用类型。
切片(动态数组)是 Go 语言的一种灵活,功能强悍的内置类型,长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。
切片由三部分组成:指向底层数组的指针、len、cap。通过调用内建函数len
,得到数组和切片的长度。通过调用内建函数cap
,得到它们的容量。
定义切片
切片不需要说明长度
// 通过声明未指定大小数组来定义切片
var identifier []type
// 使用 make() 函数来创建切片
// make([]T, length, capacity)
var slice1 []type = make([]type, len)
// 如果不指定 capacity,那么生成的切片的容量和长度相同,都是 len
make
函数参数
- length 是数组的长度并且也是切片的初始长度
- capacity (可选),指定切片容量
切片初始化
// 直接初始化切片,[] 未指定数量,表示切片类型,{1,2,3}初始化值依次是1,2,3.其cap=len=3
s := [] int {1, 2, 3}
// 初始化切片 s ,是数组 arr 的引用
s := arr[:]
// 从下标为 startIndex 到 endIndex-1 为切片的取值范围
s1 := s[startIndex:endIndex]
// 使用 make 函数初始化
s :=make([]int,len,cap)
-
切片是可索引的,并且可以由
len()
方法获取长度。 -
切片提供了计算容量的方法
cap()
可以测量切片最长可以达到多少。
注意:
一个切片在未初始化之前默认为 nil,长度为 0,也叫空(nil)切片。
切片扩容
当切片无法容纳更多的元素,Go 语言便会扩容。先生成一个容量更大的切片,把原有的元素和新元素一并拷贝到新切片中。在一般的情况下,可以认为新切片的容量是原切片容量的 2 倍。
但是,当原切片的长度大于或等于1024
时,Go 语言会以原容量的1.25
倍作为新容量,而不是原容量乘以2。之后,扩容的时候不断与1.25相乘,以此类推。
如果一次追加的元素过多,以至于使新长度比原容量的 2 倍还要大,那么新容量就会以新长度为基准,最终的新容量在很多时候都要比新容量基准更大一些。
s8 := make([]int, 10)
fmt.Printf("The capacity of s8: %d\n", cap(s8))
s8a := append(s8, make([]int, 11)...)
fmt.Printf("s8a: len: %d, cap: %d\n", len(s8a), cap(s8a))
s8b := append(s8a, make([]int, 23)...)
fmt.Printf("s8b: len: %d, cap: %d\n", len(s8b), cap(s8b))
s8c := append(s8b, make([]int, 45)...)
fmt.Printf("s8c: len: %d, cap: %d\n", len(s8c), cap(s8c))
// 执行结果
// The capacity of s8: 10
// s8a: len: 21, cap: 22
// s8b: len: 44, cap: 44
// s8c: len: 89, cap: 96
切片的底层数组永远不会被替换
在扩容的时候 Go 语言一定会生成新的底层数组,但同时生成了新的切片。它把新的切片作为了新底层数组的窗口,而没有对原切片及其底层数组做任何改动。在无需扩容时,append
函数返回的是指向原底层数组的新切片,而在需要扩容时,append
函数返回的是指向新底层数组的新切片。
只要长度不超过切片容量,那么使用append
函数追加元素的时候就不会引起扩容。只会使切片窗口右边的(底层数组中的)元素被新的元素替换掉。
package main
import "fmt"
func main() {
a1 := [7]int{1, 2, 3, 4, 5, 6, 7}
fmt.Printf("a1: %v (len: %d, cap: %d)\n",
a1, len(a1), cap(a1))
s9 := a1[1:4]
fmt.Printf("s9: %v (len: %d, cap: %d)\n",
s9, len(s9), cap(s9))
// 往切片中添加新元素
for i := 1; i <= 5; i++ {
s9 = append(s9, i)
fmt.Printf("s9(%d): %v (len: %d, cap: %d)\n",
i, s9, len(s9), cap(s9))
}
fmt.Printf("a1: %v (len: %d, cap: %d)\n",
a1, len(a1), cap(a1))
fmt.Println()
}
// 执行结果
// a1: [1 2 3 4 5 6 7] (len: 7, cap: 7)
// s9: [2 3 4] (len: 3, cap: 6)
// s9(1): [2 3 4 1] (len: 4, cap: 6)
// s9(2): [2 3 4 1 2] (len: 5, cap: 6)
// s9(3): [2 3 4 1 2 3] (len: 6, cap: 6)
// s9(4): [2 3 4 1 2 3 4] (len: 7, cap: 12)
// s9(5): [2 3 4 1 2 3 4 5] (len: 8, cap: 12)
// a1: [1 2 3 4 1 2 3] (len: 7, cap: 7)
切片操作
append() 和 copy() 函数
如果想增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝过来。
注意: 在使用 append()
函数为切片动态添加元素时,如果空间不足以容纳足够多的元素,切片就会进行“扩容”,此时新切片的长度会发生改变。容量的扩展规律是按容量的 2 倍数进行扩充。
package main
import "fmt"
func main() {
var numbers []int
/* 向切片添加一个元素 */
numbers = append(numbers, 0)
printSlice(numbers)
/* 同时添加多个元素 */
numbers = append(numbers, 1,2,3)
printSlice(numbers)
/* 创建切片 numbers1 是之前切片的两倍容量*/
numbers1 := make([]int, len(numbers), (cap(numbers))*2)
/* 拷贝 numbers 的内容到 numbers1 */
copy(numbers1,numbers)
printSlice(numbers1)
}
func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}
执行结果为
len=1 cap=1 slice=[0]
len=4 cap=4 slice=[0 1 2 3]
len=4 cap=8 slice=[0 1 2 3]
Go语言的内置函数 copy()
可以将一个数组切片复制到另一个数组切片中,如果加入的两个数组切片不一样大,就会按照其中较小的那个数组切片的元素个数进行复制。
copy( destSlice, srcSlice []T) int
srcSlice
为数据来源切片,destSlice
为复制的目标(也就是将 srcSlice 复制到 destSlice),目标切片必须空间足够且和目标的类型一致,copy()
函数的返回值表示实际发生复制的元素个数。
删除切片元素
从头删除
a := []int{1, 2, 3}
// 方式一
a = a[1:] // 删除开头1个元素
a = a[N:] // 删除开头N个元素
// 方式二
a = append(a[:0], a[1:]...) // 删除开头1个元素
a = append(a[:0], a[N:]...) // 删除开头N个元素
// 方式三
a = a[:copy(a, a[1:])] // 删除开头1个元素
a = a[:copy(a, a[N:])] // 删除开头N个元素
从中间删除
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 = a[:len(a)-1] // 删除尾部1个元素
a = a[:len(a)-N] // 删除尾部N个元素
双向链表
Go 语言的链表实现在标准库的container/list
代码包中。包中有两个公开的程序实体——List
和Element
,List 实现了一个双向链表(以下简称链表),而 Element 则代表了链表中元素的结构。
链表元素
链表中元素定义如下:
type Element struct {
Value interface{}
}
func (e *Element) Next() *Element
func (e *Element) Prev() *Element
通过Value属性来获取元素的值,此外Element还有两个方法Next和Prev分别获取当前元素的前一个元素和后一个元素。
链表常用方法
// 把元素移动到另一个元素的前面和后面
func (l *List) MoveBefore(e, mark *Element)
func (l *List) MoveAfter(e, mark *Element)
// 把元素移动到链表的最前端和最后端
func (l *List) MoveToFront(e *Element)
func (l *List) MoveToBack(e *Element)
参数都是*Element
类型的,*Element
类型是Element
类型的指针类型,*Element
的值就是元素的指针。不会接受自定义的 Element类型。因为自定义Element
值并不在链表中,所以无法调用,而且链表不允许我们把自己生成的Element
值插入其中。
在List
包含的方法中,用于插入新元素的那些方法都只接受interface{}
类型的值。这些方法在内部会使用Element
值,包装接收到的新元素。这样做正是为了避免直接使用我们自己生成的元素,主要原因是避免链表的内部关联,遭到外界破坏。
// 于获取链表中最前端和最后端的元素
func (l *List) Front() *Element
func (l *List) Back() *Element
// 在指定的元素之前和之后插入新元素
func (l *List) InsertBefore(v interface{}, mark *Element) *Element
func (l *List) InsertAfter(v interface{}, mark *Element) *Element
// 在链表的最前端和最后端插入新元素
func (l *List) PushFront(v interface{}) *Element
func (l *List) PushBack(v interface{}) *Element
方法都返回一个Element
值的指针。
List
和Element
都是结构体类型。结构体类型有一个特点,那就是它们的零值都会是拥有特定结构,但是没有任何定制化内容的值,相当于一个空壳。值中的字段也都会被分别赋予各自类型的零值。经过语句var l list.List
声明的变量l
的值是一个长度为0
的链表。这个链表持有的根元素也将会是一个空壳,其中只会包含缺省的内容。可以直接使用这样的链表的。称为“开箱即用”。语句var l list.List
声明的链表l
可以直接使用依赖于延迟初始化。
延迟初始化,你可以理解为把初始化操作延后,仅在实际需要的时候才进行。延迟初始化可以分散初始化操作带来的计算量和存储空间消耗,但频繁的判断对象是否初始化也会造成性能的浪费。
链表在删除元素、移动元素,以及一些插入元素的方法中,只要判断一下传入的元素中指向所属链表的指针,是否与当前链表的指针相等就可以了。如果不相等,就一定说明传入的元素不是这个链表中的,后续的操作就不用做了。反之,就一定说明这个链表已经被初始化了。链表的PushFront
方法、PushBack
方法、PushBackList
方法以及PushFrontList
方法总会先判断链表的状态,并在必要时进行初始化,这就是延迟初始化。
package main
import (
"fmt"
"container/list"
)
func main() {
link := list.New()
// 给链表添加元素。
for i := 0; i <= 10; i++ {
link.PushBack(i)
}
for p := link.Front(); p != link.Back(); p = p.Next() {
fmt.Println("Number", p.Value)
}
}
Ring
与List
的区别
container/ring
包中的Ring
类型实现的是一个循环链表。其实List
在内部就是一个循环链表。它的根元素永远不会持有任何实际的元素值,而该元素的存在就是为了连接这个循环链表的首尾两端。区别:
- 数据结构不同:
Ring
类型的数据结构仅由它自身即可代表,而List
类型则需要由它以及Element
类型联合表示。 - 表示维度上的不同:一个
Ring
类型的值严格来讲,只代表了其所属的循环链表中的一个元素,而一个List
类型的值则代表了一个完整的链表。 - 长度变化差异:创建并初始化一个
Ring
值可以指定它包含的元素的数量,循环链表一旦被创建,其长度是不可变的。 - 初始化值不同:仅通过
var r ring.Ring
语句声明的r
将会是一个长度为1
的循环链表,而List
类型的零值则是一个长度为0
的链表。 - 性能差异:
Ring
值的Len
方法的算法复杂度是 O(N) 的,而List
值的Len
方法的算法复杂度则是 O(1) 的。
Map
Map 是一种无序的键值对的集合。Map 重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值,Map 是无序的,我们无法决定它的返回顺序 。
字典类型的底层原理是哈希表(hash table)。在这个实现中,键和元素的最大不同在于,前者的类型是受限的,而后者却可以是任意类型的。当要在哈希表中查找与某个键对应的值,需要先把键值作为参数传给这个哈希表。哈希表会先用哈希函数(hash function)把键值转换为哈希值。
哈希值通常是一个无符号的整数。一个哈希表会持有一定数量的桶(bucket),也可称之为哈希桶,每个哈希桶都会把自己包含的所有键的哈希值存起来。
查找时哈希表会先用键的哈希值的低几位去定位到一个哈希桶,然后再去这个哈希桶中,用被查找键的哈希值与桶中哈希值逐个对比。如果一个相等的都没有,那么就说明这个桶中没有要查找的键值,这时 Go 语言就会立刻返回结果了。如果有相等的,那就再用键值本身去对比一次。由于键 - 元素对总是被捆绑在一起存储的,所以一旦找到了键,就一定能找到对应的元素值。随后,哈希表就会把相应的元素值作为结果返回。在 Go 语言的字典中,每一个键值都是由它的哈希值代表的。也就是说,字典不会独立存储任何键的值,但会独立存储它们的哈希值。
在声明的时候不需要知道 map 的长度,因为 map 是可以动态增长的,未初始化的 map 的值是 nil,使用函数 len()
可以获取 map 中 键值对的数目。
Go 语言字典的键类型不可以是函数类型、字典类型和切片类型。
Map键的选择
为了提高查找效率,(求哈希和判等操作的速度越快,对应的类型就越适合作为键类型)通常用宽度小的类型作为键类型。对于布尔类型、整数类型、浮点数类型、复数类型和指针类型来说都是如此。对于字符串类型,由于它的宽度是不定的,所以要看它的值的具体长度,长度越短求哈希越快。
- 比如,
bool
、int8
和uint8
类型的一个值需要占用的字节数都是1
,因此这些类型的宽度就都是1
。
对数组类型或结构体类型的值求哈希实际上是依次求得它的每个元素的哈希值并进行合并,所以速度就取决于它的元素类型以及它的长度(不建议使用)。改变数组的元素值会导致哈希值发生变化,使得键失效。所以,选择键时,优先选用数值类型和指针类型,通常情况下类型的宽度越小越好。如果非要选择字符串类型的话,最好对键值的长度进行额外的约束。
字典是引用类型,所以当我们仅声明而不初始化一个字典类型的变量的时候,它的值会是nil
。除了添加键 - 元素对,我们在一个值为nil
的字典上做任何操作都不会引起错误。当我们试图在一个值为nil
的字典中添加键 - 元素对的时候,Go 语言的运行时系统就会立即抛出一个 panic。
Map操作
Map 声明
/* 声明变量,默认 map 是 nil */
var map_variable map[key_data_type]value_data_type
/* 使用 make 函数 */
map_variable := make(map[key_data_type]value_data_type)
nil
map 不能用来存放键值对。
注意:可以使用 make()
,但不能使用 new()
来构造 map,如果错误的使用 new()
分配了一个引用对象,会获得一个空引用的指针,相当于声明了一个未初始化的变量并且取了它的地址
获取元素
map[key]
- key 不存在时,获得 Value 类型的初始值
- 可以使用
value, ok:= map[key]
来判断是否存在 key
Map遍历
- 使用 range 遍历 key,或者遍历 key, value 对
- 不保证遍历顺序,如需顺序,需要对 key 进行排序
for k, v := range map {
fmt.Println(k, v)
}
Map删除和清空
内置函数 delete(),用于删除容器内的元素
// 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)
}
// 执行结果
// route 66
// china 960
Go语言中并没有为 map 提供任何清空所有元素的函数、方法,清空 map 的唯一办法就是重新 make 一个新的 map。
字符串
字符串是一种值类型,且值不可变,即创建某个文本后将无法再次修改这个文本的内容,更深入地讲,字符串是字节的定长数组。
Go语言中字符串的字节使用UTF-8编码表示Unicode文本,因此Go语言字符串是变宽字符序列,每一个字符都用一个或者多个字符表示,这跟其他的(C++,Java,Python 3)的字符串类型有着本质上的不同,后者为定宽字符序列。
Go 只有在字符串只包含7位的ASCII字符(因为它们都是用一个单一的UTF-8字节表示)时才可以被字节索引,可以将字符串转化为 Unicode 码切片( 类型为 []rune
),切片支持直接索引。
可以使用[index]方式,访问到字符串中的字符。可以访问,不可以修改。
s := "Hank"
fmt.Printf("%c", s[2])
// 返回 n
字符串可以通过 +
号进行拼接
字符串函数
字符串长度
- 内建函数
len()
,可以用来获取切片、字符串、通道(channel)等的长度。 - Unicode 字符串长度要使用
utf8.RuneCountInString()
函数。
字符串比较: 比较机制是字符的对称比较。
strings.Compare(a, b string) int
// 0,表示a == b
// -1,表示a < b
// 1,表示a > b
strings.Compare("abc", "abcd")
// 返回 1
-
strings.EqualFold(s, t string) bool
检测字符串 s 和 t 在忽略大小写的情况下是否相等。
字符串查找
strings.Contains(s, substr string) bool
检测字符串 substr 是否在 s 中。strings.ContainsAny(s, chars string) bool
检测字符串 chars 的中任意字符是否出现在 s 中。strings.ContainsRune(s string, r rune) bool
检测 rune字符是否出现在 s 中。strings.Index(s, substr string) int
返回字符串 substr 在字符串 s 中第一次出现的索引位置,若没有出现,返回-1。
strings.ContainsRune("Hank", 'a')
// 返回 true
strings.ContainsRune("Hank", 97)
// 返回 true,a的码值97
字符串统计
统计字符串 s 中非重叠substr的数量。若统计空字符串"",会返回 s 的长度加1。
strings.Count("HanZhongKang", "n")
// 返回 3
strings.Count("Hank", "")
// 返回 5,"Hank"每个rune的前后都算