配色: 字号:
Go语言中的Array、Slice、Map和Set使用详解
2016-12-28 | 阅:  转:  |  分享 
  
Go语言中的Array、Slice、Map和Set使用详解

这篇文章主要介绍了Go语言中的Array、Slice、Map和Set使用详解,本文给出了它们的创建、使用、多维等代码实例,需要的朋友可以参考下

Array(数组)

内部机制

在Go语言中数组是固定长度的数据类型,它包含相同类型的连续的元素,这些元素可以是内建类型,像数字和字符串,也可以是结构类型,元素可以通过唯一的索引值访问,从0开始。

数组是很有价值的数据结构,因为它的内存分配是连续的,内存连续意味着可是让它在CPU缓存中待更久,所以迭代数组和移动元素都会非常迅速。

数组声明和初始化

通过指定数据类型和元素个数(数组长度)来声明数组。

复制代码代码如下:

//声明一个长度为5的整数数组

vararray[5]int

一旦数组被声明了,那么它的数据类型跟长度都不能再被改变。如果你需要更多的元素,那么只能创建一个你想要长度的新的数组,然后把原有数组的元素拷贝过去。

Go语言中任何变量被声明时,都会被默认初始化为各自类型对应的0值,数组当然也不例外。当一个数组被声明时,它里面包含的每个元素都会被初始化为0值。

一种快速创建和初始化数组的方法是使用数组字面值。数组字面值允许我们声明我们需要的元素个数并指定数据类型:

复制代码代码如下:

//声明一个长度为5的整数数组

//初始化每个元素

array:=[5]int{7,77,777,7777,77777}

如果你把长度写成...,Go编译器将会根据你的元素来推导出长度:

复制代码代码如下:

//通过初始化值的个数来推导出数组容量

array:=[...]int{7,77,777,7777,77777}

如果我们知道想要数组的长度,但是希望对指定位置元素初始化,可以这样:

复制代码代码如下:

//声明一个长度为5的整数数组

//为索引为1和2的位置指定元素初始化

//剩余元素为0值

array:=[5]int{1:77,2:777}

使用数组

使用[]操作符来访问数组元素:

复制代码代码如下:

array:=[5]int{7,77,777,7777,77777}

//改变索引为2的元素的值

array[2]=1

我们可以定义一个指针数组:

复制代码代码如下:

array:=[5]int{0:new(int),1:new(int)}

//为索引为0和1的元素赋值

array[0]=7

array[1]=77

在Go语言中数组是一个值,所以可以用它来进行赋值操作。一个数组可以被赋值给任意相同类型的数组:

复制代码代码如下:

vararray1[5]string

array2:=[5]string{"Red","Blue","Green","Yellow","Pink"}

array1=array2

注意数组的类型同时包括数组的长度和可以被存储的元素类型,数组类型完全相同才可以互相赋值,比如下面这样就不可以:

复制代码代码如下:

vararray1[4]string

array2:=[5]string{"Red","Blue","Green","Yellow","Pink"}

array1=array2

//编译器会报错

CompilerError:

cannotusearray2(type[5]string)astype[4]stringinassignment

拷贝一个指针数组实际上是拷贝指针值,而不是指针指向的值:

复制代码代码如下:

vararray1[3]string

array2:=[3]string{new(string),new(string),new(string)}

array2[0]="Red"

array2[1]="Blue"

array2[2]="Green"

array1=array2

//赋值完成后,两组指针数组指向同一字符串

多维数组

数组总是一维的,但是可以组合成多维的。多维数组通常用于有父子关系的数据或者是坐标系数据:

复制代码代码如下:

//声明一个二维数组

vararray[4][2]int

//使用数组字面值声明并初始化

array:=[4][2]int{{10,11},{20,21},{30,31},{40,41}}

//指定外部数组索引位置初始化

array:=[4][2]int{1:{20,21},3:{40,41}}

//同时指定内外部数组索引位置初始化

array:=[4][2]int{1:{0:20},3:{1:41}}

同样通过[]操作符来访问数组元素:

复制代码代码如下:

vararray[2][2]int

array[0][0]=0

array[0][1]=1

array[1][0]=2

array[1][1]=3

也同样的相同类型的多维数组可以相互赋值:

复制代码代码如下:

vararray1=[2][2]int

vararray2=[2][2]int

array[0][0]=0

array[0][1]=1

array[1][0]=2

array[1][1]=3

array1=array2

因为数组是值,我们可以拷贝单独的维:

复制代码代码如下:

vararray3[2]int=array1[1]

varvalueint=array1[1][0]

在函数中传递数组

在函数中传递数组是非常昂贵的行为,因为在函数之间传递变量永远是传递值,所以如果变量是数组,那么意味着传递整个数组,即使它很大很大很大。。。

举个栗子,创建一个有百万元素的整形数组,在64位的机器上它需要8兆的内存空间,来看看我们声明它和传递它时发生了什么:

复制代码代码如下:

vararray[1e6]int

foo(array)

funcfoo(array[1e6]int){

...

}

每一次foo被调用,8兆内存将会被分配在栈上。一旦函数返回,会弹栈并释放内存,每次都需要8兆空间。

Go语言当然不会这么傻,有更好的方法来在函数中传递数组,那就是传递指向数组的指针,这样每次只需要分配8字节内存:

复制代码代码如下:

vararray[1e6]int

foo(&array)

funcfoo(array[1e6]int){

...

}

但是注意如果你在函数中改变指针指向的值,那么原始数组的值也会被改变。幸运的是slice(切片)可以帮我们处理好这些问题,来一起看看。

Slice(切片)

内部机制和基础

slice是一种可以动态数组,可以按我们的希望增长和收缩。它的增长操作很容易使用,因为有内建的append方法。我们也可以通过relice操作化简slice。因为slice的底层内存是连续分配的,所以slice的索引,迭代和垃圾回收性能都很好。

slice是对底层数组的抽象和控制。它包含Go需要对底层数组管理的三种元数据,分别是:

1.指向底层数组的指针

2.slice中元素的长度

3.slice的容量(可供增长的最大值)

创建和初始化

Go中创建slice有很多种方法,我们一个一个来看。

第一个方法是使用内建的函数make。当我们使用make创建时,一个选项是可以指定slice的长度:

复制代码代码如下:

slice:=make([]string,5)

如果只指定了长度,那么容量默认等于长度。我们可以分别指定长度和容量:

复制代码代码如下:

slice:=make([]int,3,5)

当我们分别指定了长度和容量,我们创建的slice就可以拥有一开始并没有访问的底层数组的容量。上面代码的slice中,可以访问3个元素,但是底层数组有5个元素。两个与长度不相干的元素可以被slice来用。新创建的slice同样可以共享底层数组和已存在的容量。

不允许创建长度大于容量的slice:

复制代码代码如下:

slice:=make([]int,5,3)

CompilerError:

lenlargerthancapinmake([]int)

惯用的创建slice的方法是使用slice字面量。跟创建数组很类似,不过不用指定[]里的值。初始的长度和容量依赖于元素的个数:

复制代码代码如下:

//创建一个字符串slice

//长度和容量都是5

slice:=[]string{"Red","Blue","Green","Yellow","Pink"}

在使用slice字面量创建slice时有一种方法可以初始化长度和容量,那就是初始化索引。下面是个例子:

复制代码代码如下:

//创建一个字符串slice

//初始化一个有100个元素的空的字符串slice

slice:=[]string{99:""}

nil和emptyslice

有的时候我们需要创建一个nilslice,创建一个nilslice的方法是声明它但不初始化它:

复制代码代码如下:

varslice[]int

创建一个nilslice是创建slice最基本的方法,很多标准库和内建函数都可以使用它。当我们想要表示一个并不存在的slice时它变得非常有用,比如一个返回slice的函数中发生异常的时候。

创建emptyslice的方法就是声明并初始化一下:

复制代码代码如下:

//使用make创建

silce:=make([]int,0)

//使用slice字面值创建

slice:=[]int{}

emptyslice包含0个元素并且底层数组没有分配存储空间。当我们想要表示一个空集合时它很有用处,比如一个数据库查询返回0个结果。

不管我们用nilslice还是emptyslice,内建函数append,len和cap的工作方式完全相同。

使用slice

为一个指定索引值的slice赋值跟之前数组赋值的做法完全相同。改变单个元素的值使用[]操作符:

复制代码代码如下:

slice:=[]int{10,20,30,40,50}

slice[1]=25

我们可以在底层数组上对一部分数据进行slice操作,来创建一个新的slice:

复制代码代码如下:

//长度为5,容量为5

slice:=[]int{10,20,30,40,50}

//长度为2,容量为4

newSlice:=slice[1:3]

在slice操作之后我们得到了两个slice,它们共享底层数组。但是它们能访问底层数组的范围却不同,newSlice不能访问它头指针前面的值。

计算任意newslice的长度和容量可以使用下面的公式:

复制代码代码如下:

对于slice[i:j]和底层容量为k的数组

长度:j-i

容量:k-i

必须再次明确一下现在是两个slice共享底层数组,因此只要有一个slice改变了底层数组的值,那么另一个也会随之改变:

复制代码代码如下:

slice:=[]int{10,20,30,40,50}

newSlice:=slice[1:3]

newSlice[1]=35

改变newSlice的第二个元素的值,也会同样改变slice的第三个元素的值。

一个slice只能访问它长度范围内的索引,试图访问超出长度范围的索引会产生一个运行时错误。容量只可以用来增长,它只有被合并到长度才可以被访问:

复制代码代码如下:

slice:=[]int{10,20,30,40,50}

newSlice:=slice[1:3]

newSlice[3]=45

RuntimeException:

panic:runtimeerror:indexoutofrange

容量可以被合并到长度里,通过内建的append函数。

slice增长

slice比数组的优势就在于它可以按照我们的需要增长,我们只需要使用append方法,然后Go会为我们做好一切。

使用append方法时我们需要一个源slice和需要附加到它里面的值。当append方法返回时,它返回一个新的slice,append方法总是增长slice的长度,另一方面,如果源slice的容量足够,那么底层数组不会发生改变,否则会重新分配内存空间。

复制代码代码如下:

//创建一个长度和容量都为5的slice

slice:=[]int{10,20,30,40,50}

//创建一个新的slice

newSlice:=slice[1:3]

//为新的sliceappend一个值

newSlice=append(newSlice,60)

因为newSlice有可用的容量,所以在append操作之后slice索引为3的值也变成了60,之前说过这是因为slice和newSlice共享同样的底层数组。

如果没有足够可用的容量,append函数会创建一个新的底层数组,拷贝已存在的值和将要被附加的新值:

复制代码代码如下:

//创建长度和容量都为4的slice

slice:=[]int{10,20,30,40}

//附加一个新值到slice,因为超出了容量,所以会创建新的底层数组

newSlice:=append(slice,50)

append函数重新创建底层数组时,容量会是现有元素的两倍(前提是元素个数小于1000),如果元素个数超过1000,那么容量会以1.25倍来增长。

slice的第三个索引参数

slice还可以有第三个索引参数来限定容量,它的目的不是为了增加容量,而是提供了对底层数组的一个保护机制,以方便我们更好的控制append操作,举个栗子:

复制代码代码如下:

source:=[]string{"apple","orange","plum","banana","grape"}

//接着我们在源slice之上创建一个新的slice

slice:=source[2:3:4]

新创建的slice长度为1,容量为2,可以看出长度和容量的计算公式也很简单:

复制代码代码如下:

对于slice[i:j:k]或者[2:3:4]

长度:j-i或者3-2

容量:k-i或者4-2

如果我们试图设置比可用容量更大的容量,会得到一个运行时错误:

复制代码代码如下:

slice:=source[2:3:6]

RuntimeError:

panic:runtimeerror:sliceboundsoutofrange

限定容量最大的用处是我们在创建新的slice时候限定容量与长度相同,这样以后再给新的slice增加元素时就会分配新的底层数组,而不会影响原有slice的值:

复制代码代码如下:

source:=[]string{"apple","orange","plum","banana","grape"}

//接着我们在源slice之上创建一个新的slice

//并且设置长度和容量相同

slice:=source[2:3:3]

//添加一个新元素

slice=append(slice,"kiwi")

如果没有第三个索引参数限定,添加kiwi这个元素时就会覆盖掉banana。

内建函数append是一个变参函数,意思就是你可以一次添加多个元素,比如:

复制代码代码如下:

s1:=[]int{1,2}

s2:=[]int{3,4}

fmt.Printf("%v\n",append(s1,s2...))

Output:

[1234]

迭代slice

slice也是一种集合,所以可以被迭代,用for配合range来迭代:

复制代码代码如下:

slice:=[]int{10,20,30,40,50}

forindex,value:=rangeslice{

fmt.Printf("Index:%dValue:%d\n",index,value)

}

Output:

Index:0Value:10

Index:1Value:20

Index:2Value:30

Index:3Value:40

Index:4Value:50

当迭代时range关键字会返回两个值,第一个是索引值,第二个是索引位置值的拷贝。注意:返回的是值的拷贝而不是引用,如果我们把值的地址作为指针使用,会得到一个错误,来看看为啥:

复制代码代码如下:

slice:=[]int{10,20,30,40}

forindex,value:=rangeslice{

fmt.Printf("Value:%dValue-Addr:%XElemAddr:%X\n",value,&value,&slice[index])

}

Output:

Value:10Value-Addr:10500168ElemAddr:1052E100

Value:20Value-Addr:10500168ElemAddr:1052E104

Value:30Value-Addr:10500168ElemAddr:1052E108

Value:40Value-Addr:10500168ElemAddr:1052E10C

value变量的地址总是相同的因为它只是包含一个拷贝。如果想得到每个元素的真是地址可以使用&slice[index]。

如果不需要索引值,可以使用_操作符来忽略它:

复制代码代码如下:

slice:=[]int{10,20,30,40}

for_,value:=rangeslice{

fmt.Printf("Value:%d\n",value)

}

Output:

Value:10

Value:20

Value:30

Value:40

range总是从开始一次遍历,如果你想控制遍历的step,就用传统的for循环:

复制代码代码如下:

slice:=[]int{10,20,30,40}

forindex:=2;index
fmt.Printf("Index:%dValue:%d\n",index,slice[index])

}

Output:

Index:2Value:30

Index:3Value:40

同数组一样,另外两个内建函数len和cap分别返回slice的长度和容量。

多维slice

也是同数组一样,slice可以组合为多维的slice:

复制代码代码如下:

slice:=[][]int{{10},{20,30}}

需要注意的是使用append方法时的行为,比如我们现在对slice[0]增加一个元素:

复制代码代码如下:

slice:=[][]int{{10},{20,30}}

slice[0]=append(slice[0],20)

那么只有slice[0]会重新创建底层数组,slice[1]则不会。

在函数间传递slice

在函数间传递slice是很廉价的,因为slice相当于是指向底层数组的指针,让我们创建一个很大的slice然后传递给函数调用它:

复制代码代码如下:

slice:=make([]int,1e6)

slice=foo(slice)

funcfoo(slice[]int)[]int{

...

returnslice

}

Map

内部机制

map是一种无序的键值对的集合。map最重要的一点是通过key来快速检索数据,key类似于索引,指向数据的值。

map是一种集合,所以我们可以像迭代数组和slice那样迭代它。不过,map是无序的,我们无法决定它的返回顺序,这是因为map是使用hash表来实现的。

map的hash表包含了一个桶集合(collectionofbuckets)。当我们存储,移除或者查找键值对(key/www.visa158.comluepair)时,都会从选择一个桶开始。在映射(map)操作过程中,我们会把指定的键值(key)传递给hash函数(又称散列函数)。hash函数的作用是生成索引,索引均匀的分布在所有可用的桶上。hash表算法详见:July的博客—从头到尾彻底解析hash表算法

创建和初始化

Go语言中有多种方法创建和初始化map。我们可以使用内建函数make也可以使用map字面值:

复制代码代码如下:

//通过make来创建

dict:=make(map[string]int)

//通过字面值创建

dict:=map[string]string{"Red":"#da1337","Orange":"#e95a22"}

使用字面值是创建map惯用的方法(为什么不使用make)。初始化map的长度依赖于键值对的数量。

map的键可以是任意内建类型或者是struct类型,map的值可以是使用==操作符的表达式。slice,function和包含slice的struct类型不可以作为map的键,否则会编译错误:

复制代码代码如下:

dict:=map[[]string]int{}

CompilerException:

invalidmapkeytype[]string

使用map

给map赋值就是指定合法类型的键,然后把值赋给键:

复制代码代码如下:

colors:=map[string]string{}

colors["Red"]="#da1337"

如果不初始化map,那么就会创建一个nilmap。nilmap不能用来存放键值对,否则会报运行时错误:

复制代码代码如下:

varcolorsmap[string]string

colors["Red"]="#da1337"

RuntimeError:

panic:runtimeerror:assiwww.hunanwang.netgnmenttoentryinnilmap

测试map的键是否存在是map操作的重要部分,因为它可以让我们判断是否可以执行一个操作,或者是往map里缓存一个值。它也可以被用来比较两个map的键值对是否匹配或者缺失。

从map里检索一个值有两种选择,我们可以同时检索值并且判断键是否存在:

复制代码代码如下:

value,exists:=colors["Blue"]

ifexists{

fmt.Println(value)

}

另一种选择是只返回值,然后判断是否是零值来确定键是否存在。但是只有你确定零值是非法值的时候这招才管用:

复制代码代码如下:

value:=colors["Blue"]

ifvalue!=""{

fmt.Println(value)

}

当索引一个map取值时它总是会返回一个值,即使键不存在。上面的例子就返回了对应类型的零值。

迭代一个map和迭代数组和slice是一样的,使用range关键字,不过在迭代map时我们不使用index/value而使用key/value结构:

复制代码代码如下:

colors:=map[string]string{

"AliceBlue":"#f0f8ff",

"Coral":"#ff7F50",

"DarkGray":"#a9a9a9",

"ForestGreen":"#228b22",

}

forkey,value:=rangecolors{

fmt.Printf("Key:%sValue:%s\n",key,value)

}

如果我们想要从map中移除一个键值对,使用内建函数delete(要是也能返回移除是否成功就好了,哎。。。):

复制代码代码如下:

delete(colors,"Coral")

forkey,value:=rangecolors{

fmt.Println("Key:%sValue:%s\n",key,value)

}

在函数间传递map

在函数间传递map不是传递map的拷贝。所以如果我们在函数中改变了map,那么所有引用map的地方都会改变:

复制代码代码如下:

funcmain(){

colors:=map[string]string{

"AliceBlue":"#f0f8ff",

"Coral":"#ff7F50",

"DarkGray":"#a9a9a9",

"ForestGreen":"#228b22",

}

forkey,value:=rangecolors{

fmt.Printf("Key:%sValue:%s\n",key,value)

}

removeColor(colors,"Coral")

forkey,value:=rangecolors{

fmt.Printf("Key:%sValue:%s\n",key,value)

}

}

funcremoveColor(colorsmap[string]string,keystring){

delete(colors,key)

}

执行会得到以下结果:

复制代码代码如下:

Key:AliceBlueValue:#F0F8FF

Key:CoralValue:#FF7F50

Key:DarkGrayValue:#A9A9A9

Key:ForestGreenValue:#228B22

Key:AliceBlueValue:#F0F8FF

Key:DarkGrayValue:#A9A9A9

Key:ForestGreenValue:#228B22

可以看出来传递map也是十分廉价的,类似slice。

Set

Go语言本身是不提供set的,但是我们可以自己实现它,下面就来试试:

复制代码代码如下:

packagemain

import(

"fmt"

"sync"

)

typeSetstruct{

mmap[int]bool

sync.RWMutex

}

funcNew()Set{

return&Set{

m:map[int]bool{},

}

}

func(sSet)Add(itemint){

s.Lock()

defers.Unlock()

s.m[item]=true

}

func(sSet)Remove(itemint){

s.Lock()

s.Unlock()

delete(s.m,item)

}

func(sSet)Has(itemint)bool{

s.RLock()

defers.RUnlock()

_,ok:=s.m[item]

returnok

}

func(sSet)Len()int{

returnlen(s.List())

}

func(sSet)Clear(){

s.Lock

defers.Unlock()

s.m=map[int]bool{}

}

func(sSet)IsEmpty()bool{

ifs.Len()==0{

returntrue

}

returnfalse

}

func(sSet)List()[]int{

s.RLock()

defers.RUnlock()

list:=[]int{}

foritem:=ranges.m{

list=append(list,item)

}

returnlist

}

funcmain(){

//初始化

s:=New()

s.Add(1)

s.Add(1)

s.Add(2)

s.Clear()

ifs.IsEmpty(){

fmt.Println("0item")

}

s.Add(1)

s.Add(2)

s.Add(3)

ifs.Has(2){

fmt.Println("2doesexist")

}

s.Remove(2)

s.Remove(3)

fmt.Println("listofallitems",S.List())

}

注意我们只是使用了int作为键,你可以自己实现用interface{}作为键,做成更通用的Set,另外,这个实现是线程安全的。

总结

1.数组是slice和map的底层结构。

2.slice是Go里面惯用的集合数据的方法,map则是用来存储键值对。

3.内建函数make用来创建slice和map,并且为它们指定长度和容量等等。slice和map字面值也可以做同样的事。

4.slice有容量的约束,不过可以通过内建函数append来增加元素。

5.map没有容量一说,所以也没有任何增长限制。

6.内建函数len可以用来获得slice和map的长度。

7.内建函数cap只能作用在slice上。

8.可以通过组合方式来创建多维数组和slice。map的值可以是slice或者另一个map。slice不能作为map的键。

9.在函数之间传递slice和map是相当廉价的,因为他们不会传递底层数组的拷贝。





















献花(0)
+1
(本文系白狐一梦首藏)