GO语言 4 收集器

劳苦功高的数组

声明数组并访问其元素

以下数组不多不少正好包含 8 个元素

var planets [8]string

同一个数组中的每个元素都具有相同的类型,比如以上代码就是由 8 个字符串组成,简称字符串数组。

数组的长度可以通过内置的 len 函数确定。在声明数组时,未被赋值的元素将包含类型对应的零值。

var planets [8]string
fmt.Println(len(planets))
fmt.Println(planets)
//运行结果
8
[       ]

小心越界

包含 8 个元素的数组的合法索引为 0 至 7。go 编译器在检测到对越界数组的访问时会报错。

另外如果 go 编译器在编译时未能发现越界错误,那么程序将在运行时出现惊恐(错误)。

惊恐:运行时错误

错误会导致程序崩溃。

使用复合字面量初始化数组

复合字面量是一种使用给定值对任意复合类型实施初始化的紧凑语法。与先声明一个数组然后再一个接一个地为它的元素赋值相比,go 语言的复合字面量语法允许我们在单个步骤里面完成声明数组和初始化数组这两项工作。

dwarfs := [5]string{"ceres","pluto","haumea","makemake","eris"}

这段代码中的大括号 {} 包含了 5 个用逗号分隔的字符串,它们将被用于填充新创建的数组。

在初始化大型数组时,将复合字面量拆分成至多个行可以让代码变得更可读。为了方便,你还可以在复合字面量里面使用省略号...而不是具体的数字作为数组长度,然后让 go 编译器为你计算数组元素的数量。需要注意的是,无论使用哪种方式初始化数组,数组的长度都是固定的。

planets := [...]string{
	"Mercury",//让go编译器计算数组元素的数量
	"Venus",
	"Earth",
	"Mars",
	"Jupiter",
	"Saturn",
	"Uranus",
	"Neptune",//结尾的逗号是必需的,不能省略
}

迭代数组

迭代数组中各个元素的做法与迭代字符串中各个字符的做法非常类似。

dwarfs := [5]string{"Ceres","Pluto","Haumea","Makemake","Eris"}
for i:=0;i<len(dwarfs);i++{
	dwarf := dwarfs[i]
	fmt.Println(i,dwarf)
}
//运行结果
0 Ceres
1 Pluto
2 Haumea
3 Mkaemake
4 Eris

使用关键字range可以取得数组中每个元素的对应的索引和值,这种迭代方式使用的代码更少并且更不容易出错。

dwarfs := [5]string{"Ceres","Pluto","Haumea","Makemake","Eris"}
for i,dwarf := range dwarfs{
	fmt.Println(i,dwarf)
}
//运行结果
0 Ceres
1 Pluto
2 Haumea
3 Mkaemake
4 Eris

注意:如果你不需要 range 提供的索引变量,那么可以使用空白标识符(下划线)来省略它们

数组被复制

无论是将数组赋值给新的变量还是将它传递给函数,都会产生一个完整的数组副本。

planets := [...]string{
	"Mercury",
	"Venus",
	"Earth",
	"Mars",
	"Jupiter",
	"Saturn",
	"Uranus",
	"Neptune"
}

planetsMark := planets//复制planets数组
planets[2]="whoops"//修改数组元素
fmt.Println(planets)//打印planets数组
fmt.Println(planetsMark)//打印planetsMark
//运行结果
[Mercury Venus whoops Mars Jupiter Saturn Uranus Neptune]
[Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune]

因为数组也是一种值,而函数通过传递值接受参数,所以代码清单中的 terraform 函数将非常低效。

package main
import "fmt"
//terraform不会产生任何实际效果
func terraform(planets [8]string){
	for i :=range planets{
		planets[i]="New"+planets[i]
	}
}
func main(){
	planets := [...]string{
		"Mercury",
		"Venus",
		"Earth",
		"Mars",
		"Jupiter",
		"Saturn",
		"Uranus",
		"Neptune",
	}
	terraform(planets)
	fmt.Println(planets)
}
//运行结果
[Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune]

由于 terraform 函数操作的是 planets 数组的副本,因此函数内部对数组的修改将不会影响 mian 函数中的 planets 数组。

除此之外,我们还需要意识到数组的长度实际上也是数组类型的一部分,这一点非常重要。例如,虽然[8]string类型和[5]string类型都属于字符串收集器,但它们实际上是不同的类型。尝试传递长度不相符的数组作为参数将导致go编译器报错:

dwarfs :=[5]string{"Ceres","Pluto","Haumea","Makemake","Eris"}
terraform(dwarfs)//只能接受 [8]string 类型的 terraform 函数无法使用 [5]string 类型的 dwarfs 作为实参

基于上述原因,函数一般使用切片而不是数组作为形参。

由数组组成的数组

我们除可以定义字符串数组之外,还可以定义整数数组、浮点数数组甚至数组的数组(嵌套数组或叫二维数组)。

var board [8][8]string//一个8x8嵌套数组,其中内层数组的每个元素都是一个字符串
board[0][0]="r"
board[0][7]="r"//将r放置到[行][列]指定的坐标上

for column := range board[1]{
	borad[1][column]="p"
}
fmt.Print(board)

切片:指向数组的窗口

切分数组

通过切分数组创建切片需要用到半开区间。

planets := [...]string{
	"mercury",
	"venus",
	"earth",
	"mars",
	"jupiter",
	"saturn",
	"uranus",
	"neptune",
}
terrestrial := planets[0:4]
gasGiants := planets[4:6]
iceGiants := planets[6:8]
fmt.Println(terrestrial,gasGiants,iceGiants)
//运行结果
[Mercury Venus Earth Mars] [Jupiter Saturn] [Uranus Neptune]

虽然 terrestrial、gasGiants 和 iceGiants 都是切片,但我们还是可以像数组那样根据索引获取切片中的指定元素:

fmt.Println(gasGiants[0])
//打印出jupiter

我们除可以创建数组的切片之外,还可以创建切片的切片。

giants := planets[4:8]
gas := giants[0:2]
ice := giants[2:4]
fmt.Println(giants,gas,ice)
//运行结果
[Jupiter Saturn Uranus Neptune] [Jupiter Saturn] [Uranus Neptune]

无论是 terrestial、gasGiants、iceGiants、giants、gas还是ice,它们都是同一个 planets 数组的视图::jjk,对切片中的任意一个元素赋予新的值都会导致 planets 数组发生变化,而这一变化同样会见诸指向 planets 数组的其他切片:

iceGiantsMarkII := iceGiants
iceGiants[1]="Poseidon"
fmt.Println(planets)
fmt.Println(iceGiants,iceGiantsMarkII,ice)
//运行结果
[Mercury Venus Earth Mars Jupiter Saturn Uranus Poseidon]
[Uranus Poseidon] [Uranus Poseidon] [Uranus Poseidon]

切片的默认索引

在切分数组创建切片的时候,省略半开区间中的起始索引表示使用数组的起始位置作为索引,而省略半开区间的结束索引则表示使用数组的长度作为索引。这种做法使得我们可以把上面代码清单中的切分操作修改为如下形式

terrestrial := planets[:4]
gasGiants := planets[4:6]
iceGiants := planets[6:]

注意:切片的索引不能是负数

除单独省略起始索引或者结束索引之外,我们还可以同时省略这两个索引。

下面就是数组全部内容的切片。

all:=planets[:]

切分字符串

切分数组的创建切片的语法也可以用于切分字符串

neptune := "Neptune"
tune := neptune[3:]
fmt.Println(tune)
//运行结果
tune

切分字符串将创建另一个字符串。不过为 neptune 变量赋予新值并不会改变 tune 变量的值,反之亦然。

neptune="Poseidon"
fmt.Println(tune)
//运行结果
tune

另外需要注意的是,在切分字符串时,索引代表的是字节号码而非符文号码

question := "come eatas?"
fmt.Println(question[:6])
//运行结果
come e

切片的复合字面量

go语言的许多函数都倾向于使用切片而不是数组作为输入。如果你需要一个跟底层数组具有同样元素的切片,那么其中一种方法就是声明数组然后使用[:]对其进行切分,就像这样:

dwarfArray := [...]string{"Ceres","Pluto","Haumea","Makemake","Eris"}
dwarfSlice :=dwarfArray[:]

切分数组并不是创建切片的唯一方法,我们还可以选择直接声明切片。与声明数组时需要在方括号内提供数组长度或者使用省略号不一样,声明切片不需要在方括号内提供任何值。

例如,如果我们想要声明一个字符串切片,那么只需要使用[]string作为类型即可。

dwarfs := []string{"Ceres","Pluto","Haumea","Makemake","Eris"}

直接声明的切片仍然会有相应的底层数组。以上面代码为例,go首先会在内部一个包含5个元素的数组,然后再创建一个能够看到数组所有元素的切片。

切片的威力

package main

import (
  "fmt"
  "strings"
)

func hyperspace(worlds []string){
  for i:= range worlds{
  //返回字符串参数的切片,删除所有前导和尾随空格
    worlds[i]=strings.TrimSpace(worlds[i])
  }
}

func main(){
  planets :=[]string{"Venus","Earth","Mars"}
  hyperspace(planets)
  //Join函数是go中用于将多个字符串连接为一个字符串的函数
  //第一个参数为字符串切片,第二个参数为分隔符
  fmt.Println(strings.Join(planets,""))
  //最终打印出VenusEarthMars
}

worlds 和 planets 都是切片,并且前者还是后者的副本,但是它们都指向相同的底层数组。

如果 heperspace 函数想要修改的是 worlds 切片的指向,无论是指向开头还是结尾,这些修改都不会对 planets 切片产生任何影响。但由于 hyperspace 函数能够访问 worlds 指向的底层数组并修改其包含的元素,因此这些修改将见诸同一数组的其他切片。

切片比数组通用的另一个地方在于,切片虽然也有长度,但这个长度与数组的长度不一样,它不是类型的一部分。基于这个原因,你可以将任意长度的切片传递给 hyperspace 函数:

dwarfs :=[]string{"Ceres","Pluto"}
hyperspace(dwarfs)

go 语言的使用者很少会直接使用数组,它们更愿意使用更为通用的切片,特别是在向函数传递实参的时候。

带有方法的切片

我们可以在 go 语言中声明底层为切片或者数组的类型,并为其绑定相应的方法。跟其他语言的类(class)相比,go语言在类型之上声明方法的能力五一更为通用。

例如,标准库的 sort 包声明了一种 StringSlice 类型:

type StringSlice []string

并且该类型还有关联的 Sort 方法:

func (p StringSlice) Sort()

为了按照字符顺序对数组进行排序,代码清单首先会将 planets 数组转换为 sort.StringSlice 类型,然后再调用相应的 Sort 方法:

package main
import (
	"fmt"
	"sort"
)
func main(){
	planets := []string{
		"Mercury","Venus","Earth","Mars",
		"Jupiter","Saturn","Uranus","Neptune",
	}
	sort.StringSlice(planets).Sort
	fmt.Println(planets)
}
//运行结果
[Earth Jupiter Mars Mercury Neptune Saturn Uranus Venus]

为了进一步简化上述操作,sort 包提供了 Strings 辅助函数,它会自动执行所需的类型转换并调用 Sort 方法:

sort.Strings(planets)

更大的切片

append函数

通过内置的 append 函数,我们可以将更多元素添加到 dwarfs 切片里面。

dwarfs := []string{"Ceres","Pluto","Haumea","Makemake","Eris"}
dwarfs = append(dwarfs,"Orcus")
fmt.Println(dwarfs)
//运行结果
[Ceres Pluto Haumea Makemake Eris Orcus]

和 Println 一样,append 也是一个可变参数函数,因为我们可以一次向切片追加多个元素:

dwarfs=append(dwarfs,"Salacia","Quaoar","Sedna")
fmt.Println(dwarfs)
//运行结果
[Ceres Pluto Haumea Makemake Eris Orcus Salacia Quaoar Sedna]

为了弄清楚这一切是如何实现的,我们必须先弄懂容量和内置的 cap 函数。

长度和容量

切片中可见元素的数量决定了切片的长度。如果切片底层的数组比切片大,那么我们就说该切片还有容量可供增长。

代码清单声明的函数能够打印出切片的长度和容量。

len 函数用于获取切片长度,cap 函数用于获取切片容量

package main
import "fmt"
//dump函数会打印出切片的长度、容量和内容
func dump(label string,slice []string){
	fmt.Printf("%v:length %v,capacity %v %v\n",label,len(slice),cap(slice),slice)
}
func main(){
	dwarfs := []string{"Ceres","Pluto","Haumea","Makemake","Eris"}
	dump("dwarfs",dwarfs)
	dump("dwarfs[1:2]",dwarfs[1:2])
}
//运行结果
dwarfs:length 5 capacity 5 [Ceres Pluto Haumea Makemake Eris]
dwarfs[1:2]:length 1 capacity 4 [Pluto]

根据打印结果可知,dwarfs[1:2]创建的切片虽然长度只有 1,但它的容量却足以容纳 4 个元素。

详解 append 函数

下列代码展示了 append 函数对切片容量的影响

dwarfs1 :=[]string{"Ceres","Pluto","Haumea","Makemake","Eris"}//长度为5,容量为5
dwarfs2 :=append(dwarfs1,"Orcus")//长度为6,容量为10
dwarfs3 :=append(dwarfs2,"Salacia","Quaoar","Sedna")//长度为9,容量为10

如上,由于支撑 dwarfs1 切片的底层数组没有足够的空间(容量)执行追加 “Orcus” 的操作,因此 append 函数将把 dwarfs1 包含的元素复制到新分配的数组里面。新数组的容量是原数组的两倍,其中额外分配的容量将为后续可能发生的 append 操作提供空间。

为了证明 dwarfs1 与 dwarfs2 和 dwarfs3 指向的是两个不同的数组,我们可以修改这两个数组中的任意一个元素,然后打印这 3 个切片。

package main

import (
  "fmt"
)

func main(){
  dwarfs1:=[]string{"Ceres","Pluto","Haumea","Makemake","Eris"}
  dwarfs2:=append(dwarfs1,"Orcus")
  dwarfs3:=append(dwarfs2,"Sqlacia","Quaoar","Sedna")
  dwarfs3[0]="Pluto"
  fmt.Println("dwarfs1",len(dwarfs1),cap(dwarfs1),dwarfs1)
  fmt.Println("dwarfs2",len(dwarfs2),cap(dwarfs2),dwarfs2)
  fmt.Println("dwarfs3",len(dwarfs3),cap(dwarfs3),dwarfs3)
}
//运行结果
dwarfs1 5 5 [Ceres Pluto Haumea Makemake Eris]
dwarfs2 6 10 [Pluto Pluto Haumea Makemake Eris Orcus]
dwarfs3 9 10 [Pluto Pluto Haumea Makemake Eris Orcus Sqlacia Quaoar Sedna]

三索引切分操作

go 语言在 1.2 版本引入了能够限制新建切片容量的三索引切分操作。新创建的 terrestrial 切片的长度和容量都为 4,对其追加 Ceres 将导致 terrestrial 指向新分配的数组,而 terrestrial 原来指向的数组(也就是 planets仍在指向的数组)将不会发生任何变化。

planets := []string{
	"Mercury","Venus","Earth","Mars",
	"Jupiter","Saturn","Uranus","Neptune",
}
terrestrial := planets[0:4:4]//长度为4,容量为4
worlds := append(terrestrial,"Ceres")
fmt.Println(planets)
fmt.Println(worlds)
//打印出
[Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune]
[Mercury Venus Earth Mars Ceres]

相反,如果我们在执行切片操作时没有指定第 3 个索引,那么 terrestrial 的容量将为 8,并且也不会因为追加 Ceres 而分配新的数组,而是会覆盖原数组中的 Jupiter:

terrestrial=planets[0:4]//长度为4,容量为8
worlds=append(terrestrial,"Ceres")
fmt.Println(planets)
fmt.Println(worlds)
//运行结果
[Mercury Venus Earth Mars Ceres Saturn Uranus Neptune]
[Mercury Venus Earth Mars Ceres]

如果覆盖 Jupiter 并非你想要的行为,那么你就应该在创建切片的时候使用三索引切片操作。

使用make函数对切片实行预分配

当切片的容量不足以执行 append 操作时,go 必须创建新数组并复制旧数组中的内容。但是通过内置的 make 函数对切片进行预分配策略,我们可以尽量避免额外的内存分配和数组复制操作。

make函数分别指定了 0 和 10 作为 dwarfs 切片的长度和容量,从而使改切片可以追加 10 个元素。在 dwarfs 切片被填满之前,append 函数将不需要为其分配任何新数组。

func main(){
	//创建了一个长度为0,容量为10的切片
	//如果make函数只有两个参数,那么他的第二个参数表示长度和容量
	dwarfs := make([]string,0,10)
}

make 函数的容量参数是可选的。执行语句 make([]string,10) 将创建长度和容量都为 10 的切片,其中每个切片元素都包含一个与类型对应的零值,也就是一个空字符串。对于这种包含零值元素的切片,执行 append 函数将向切片追加第 11 个元素。

声明可变参数函数

为了声明 Printf 和 append 这样能够接受可变数量的实参的可变参数函数,我们需要在改函数的最后一个形参前面加上省略号...

package main
import "fmt"
//可变参数为字符串类型的切片
func terraform(prefix string,worlds ...string)[]string{
	newWorlds := make([]string,len(worlds))//创建新的切片而不是直接修改 worlds
	for i:=range(worlds){
		newWorlds[i]=prefix+""+worlds[i]
	}
	return newWorlds
}

worlds 形参是一个字符串切片,它包含传递给 terraform 函数的零个或多个实参:

twoWorlds:=terraform("New","Venus","Mars")
fmt.Println(twoWorlds)

通过省略号可以展开切片中的多个元素,并将它们用作传递给函数的多个实参:

planets := []string{"Venus","Mars","Jupiter"}
newPlanets := terraform("New",planets...)
fmt.Println(newPlanets)

如果 terraform 函数直接修改或者改变(mutate)worlds 形参中的元素,那么这些修改将见诸 planets 切片,但是 terraform 函数通过使用 newWorlds 切片避免了这一点。

无所不能的映射

go 提供了一种名为映射的(map)的收集器,它可以将键映射至值,并帮助你快速找到指定的元素。与数组和切片使用序列整数作为索引的做法不同,映射的键几乎是任何类型。

映射收集器在不同编程语言中通常都具有不同的名称:Python 将其称为字典,Ruby 将其称为散列,而 JavaScript 则将其称为对象。PHP 对它的叫法是关联数组,至于 Lua 的表则可以同时充当映射和传统的数组。

声明映射

在这里插入图片描述

代码中声明的映射在声明和初始化的时候还跟其它收集器一样使用了复合字面量。对于映射中的每个元素,我们都需要根据它们的类型给定正确的键(key)和值(value),然后通过方括号[]执行诸如按键查值、使用新值覆盖旧值以及为映射添加新值等操作。

package main
import "fmt"

func main(){
	//声明map
	//声明一个string类型,返回值为int类型的map
	temp:=map[string]int{
		"Earth":15,
		"Mars":-65,
	}
	//通过key获取对应value的值
	t := temp["Earth"]
	//修改key对应的value
	temp["Earth"]=30
	//如果映射中没有对应的键,则会返回零值
	moon:=temp["Moon"]
}

为了区分 “键Moon不存在映射中” 和 “键Moon存在于映射中并且它的值为0” 这两种情况,go语言提供了 “逗号与ok” 语法:

//如果这个key存在与map,则ok的值为true
//如果ok的值为true,则执行后面的语句
if moon,ok:=temp["Moon"];ok{
	fmt.Printf("On average the moon is %v",moon)
}
else{
	fmt.Println("Where is the moon?")
}

这样以来,变量 moon 将继续包含键 “Moon” 的值或者零值,至于额外的 ok 变量则会在键 “Moon” 存在时被设置为 true,并在键 “Moon” 不存在时被设置为 false。

在使用逗号与 ok 语法时,你可以使用自己喜欢的任何名字命名第二个变量,并不是非得用 ok 不可

temp,found:=temperature["Venus"]

映射不会被复制

map 不会被复制

数组、int、float64 等基本类型在赋值给新变量或传递至函数/方法的时候会创建相应的副本,但map不会

delete 函数

map共享相同的底层数据,修改这两者中的任何一个都将导致另一个发送变化。

planets := map[string]string{
	"Earth":"Sector ZZ9",
	"Mars":"Sector ZZ9",
}
planets2:=planets
planets["Earth"]="whoops"
fmt.Println(planets)
fmt.Println(planets2)
delete(planets,"Earth")
fmt.Println(planets2)
//运行结果
map[Earth:whoops Mars:Sector ZZ9]
map[Earth:whoops Mars:Sector ZZ9]
map[Mars:Sector ZZ9]

如代码所示,在使用内置的 delete 函数将映射从映射中移除之后,planets 和 planets2 都会受到相应的影响。与此类似,如果我们将映射传递给函数或者方法,那么映射的内容就有可能被修改。这种行为就跟多个切片同时指向相同的底层数组类似。

使用make函数对映射实行预分配

除非你使用复合字面值来初始化 map,否则必须使用内置的 make 函数来为 map 分配空间。

创建 map 时,make 函数可接受一个或两个参数,第二个参数用于为指定数量的键预先分配空间,就像分配切片的容量一样。

使用 make 函数创建的 map 的初始长度为 0。

func main(){
	temp:=make(map[float64]int,8)
}

使用映射进行计数

利用映射键的唯一性对切片进行计数。

func main(){
  temp:=[]int{
    28,32,-31,29,28,-33,
  }
  fre:=make(map[int]int)
  for _,t:=range temp{
    fre[t]++
  }
  for t,num:=range fre{
    fmt.Printf("%v  %d \n",t,num)
  }
}
//运行结果
28  2
32  1
-31  1
29  1
-33  1

使用关键字 range 迭代映射的方法跟我们之前看到过的迭代切片以及数组的方法非常相似,不同的地方在于,range 在每次迭代时提供的将不再是索引和值,而是键和值。需要注意的是,go 在迭代映射时并不保证键的顺序,因此,同样的映射在进行多次迭代时可能会产生不同的输出。

使用映射和切片实现数据分组

利用映射和切片对数据进行奇偶数进行分组

func main(){
  temp:=[]int{3,5,6,8,12,11,15,13,}
	groups := make(map[string][]int)

  for _,num:=range temp{
    if num%2==0{
      groups["even"]=append(groups["even"],num)
    }else{
      groups["odd"]=append(groups["odd"],num)
    }
  }
  fmt.Println("odd number",groups["odd"])
  fmt.Println("even number",groups["even"])
}
//运行结果
odd number [3 5 11 15 13]
even number [6 8 12]

将映射用作集合

集合这种收集器与数组非常相似,唯一的区别在于,集合保证其中的每个元素只会出现一次。虽然 go 语言没有直接提供集合搜集器,但我们总是可以像代码展示的那样,使用映射临时拼凑出一个集合。对被用作集合的映射来说,键的值通常并不重要,但是为了便于检查集合成员关系,键的值通常会被设置为 true。

func main(){
  var temp=[]int{
    10,23,43,65,34,45,12,
  }
  set:=make(map[int]bool)
  for _,t:=range temp{
    set[t]=true
  }
  //如果值为true,则相应的数存在于集合中
  if set[10]{
    fmt.Println("set number")
  }
  fmt.Println(set)
  un:=make([]int,0,len(set))
  for t:=range set{
    un=append(un,t)
  }
  sort.Ints(un)
  fmt.Println(un)
}
//运行结果
set number
map[10:true 12:true 23:true 34:true 43:true 45:true 65:true]
[10 12 23 34 43 45 65]

后言

参考书籍:go语言趣学指南

  • 30
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值