golang数组和切片
数组
数组是类型相同的元素的集合。例如,整数5,8,9,79,76的集合就构成了一个数组。Go不允许在数组中混合使用不同类型的元素(比如整数和字符串)。
声明
数组的类型为n[T]
,其中n
表示数组中元素的个数,T
表示数组中元素的类型。元素的个数n
也是数组类型的一部分(我们将在稍后详细讨论)。
有很多声明数组的方式,让我们一个一个地介绍。
packagemain import( "fmt" ) funcmain(){ vara[3]int//intarraywithlength3 fmt.Println(a) }12345678910
vara[3]int
声明了一个长度为3的整型数组。**数组中的所有元素都被自动赋值为元素类型的0值。**比如这里a
是一个整型数组,因此a
中的所有元素都被赋值为0(即整型的0值)。运行上面的程序,输出为:[000]
。
数组的索引从0
开始到length-1
结束。下面让我们给上面的数组赋一些值。
packagemain import( "fmt" ) funcmain(){ vara[3]int//intarraywithlength3 a[0]=12//arrayindexstartsat0 a[1]=78 a[2]=50 fmt.Println(a) }1234567891011121314
a[0]
表示数组中的第一个元素。程序的输出为:[127850]
。
(译者注:可以用下标运算符([]
)来访问数组中的元素,下标从0开始,例如a[0]
表示数组a
的第一个元素,a[1]
表示数组a
的第二元素,以此类推。)
可以利用**速记声明(shorthanddeclaration)**的方式来创建同样的数组:
packagemain import( "fmt" ) funcmain(){ a:=[3]int{12,78,50}//shorthanddeclarationtocreatearray fmt.Println(a) }12345678910
上面的程序输出为:[127850]
。
(译者注:这个例子给出了速记声明的方式:在数组类型后面加一对大括号({}
),在大括号里面写元素初始值列表,多个值用逗号分隔。)
在速记声明中,没有必要为数组中的每一个元素指定初始值。
packagemain import( "fmt" ) funcmain(){ a:=[3]int{12} fmt.Println(a) }12345678910
上面程序的第8行:a:=[3]int{12}
声明了一个长度为3的数组,但是只提供了一个初值12。剩下的两个元素被自动赋值为0。程序的输出为:[1200]
。
在声明数组时你可以忽略数组的长度并用...
代替,让编译器为你自动推导数组的长度。比如下面的程序:
packagemain import( "fmt" ) funcmain(){ a:=[...]int{12,78,50}//...makesthecompilerdeterminethelength fmt.Println(a) }12345678910
上面已经提到,**数组的长度是数组类型的一部分。**因此[5]int
和[25]int
是两个不同类型的数组。正是因为如此,一个数组不能动态改变长度。不要担心这个限制,因为切片(slices
)可以弥补这个不足。
packagemain funcmain(){ a:=[3]int{5,78,8} varb[5]int b=a//notpossiblesince[3]intand[5]intaredistincttypes }1234567
在上面程序的第6行,我们试图将一个[3]int
类型的数组赋值给一个[5]int
类型的数组,这是不允许的。编译会报错:main.go:6:cannotusea(type[3]int)astype[5]intinassignment
。
数组是值类型
在Go中数组是值类型而不是引用类型。这意味着当数组变量被赋值时,将会获得原数组(译者注:也就是等号右面的数组)的拷贝。新数组中元素的改变不会影响原数组中元素的值。
packagemain import"fmt" funcmain(){ a:=[...]string{"USA","China","India","Germany","France"} b:=a//acopyofaisassignedtob b[0]="Singapore" fmt.Println("ais",a) fmt.Println("bis",b) }1234567891011
上面程序的第7行,将数组a
的拷贝赋值给数组b
。第8行,b
的第一个元素被赋值为Singapore
。这将不会影响到原数组a
。程序的输出为:
ais[USAChinaIndiaGermanyFrance] bis[SingaporeChinaIndiaGermanyFrance]12
同样的,如果将数组作为参数传递给函数,仍然是值传递,在函数中对(作为参数传入的)数组的修改不会造成原数组的改变。
packagemain import"fmt" funcchangeLocal(num[5]int){ num[0]=55 fmt.Println("insidefunction",num) } funcmain(){ num:=[...]int{5,6,7,8,8} fmt.Println("beforepassingtofunction",num) changeLocal(num)//numispassedbyvalue fmt.Println("afterpassingtofunction",num) }123456789101112131415
上面程序的第13行,数组num
是通过值传递的方式传递给函数changeLocal
的,因此该函数执行过程中不会造成num
的改变。程序输出如下:
beforepassingtofunction[56788]
insidefunction[556788]
afterpassingtofunction[56788]123
数组的长度
内置函数len
用于获取数组的长度:
packagemain import"fmt" funcmain(){ a:=[...]float64{67.7,89.8,21,78} fmt.Println("lengthofais",len(a)) }123456789
上面程序的输出为:lengthofais4
。
使用range遍历数组
for
循环可以用来遍历数组中的元素:
packagemain import"fmt" funcmain(){ a:=[...]float64{67.7,89.8,21,78} fori:=0;i<len(a);i++{//loopingfrom0tothelengthofthearray fmt.Printf("%dthelementofais%.2f\n",i,a[i]) } }12345678910
上面的程序使用for
循环遍历数组中的元素(索引从0
到len(a)-1
)。上面的程序输出如下:
0thelementofais67.70
1thelementofais89.80
2thelementofais21.00
3thelementofais78.001234
Go提供了一个更简单,更简洁的遍历数组的方法:使用rangefor。range返回数组的索引和索引对应的值。让我们用rangefor重写上面的程序(除此之外我们还计算了数组元素的总和)。
packagemain import"fmt" funcmain(){ a:=[...]float64{67.7,89.8,21,78} sum:=float64(0) fori,v:=rangea{//rangereturnsboththeindexandvalue fmt.Printf("%dtheelementofais%.2f\n",i,v) sum+=v } fmt.Println("\nsumofallelementsofa",sum) }12345678910111213
上面的程序中,第8行fori,v:=rangea
是range形式的for循环。range将返回数组的索引和相对应的元素。我们打印这些值并计算数组a
中所有元素的总和。程序的输出如下:
0theelementofais67.70
1theelementofais89.80
2theelementofais21.00
3theelementofais78.00
sumofallelementsofa256.5123456
如果你只想访问数组元素而不需要访问数组索引,则可以通过空标识符来代替索引变量:
for_,v:=rangea{//ignoresindex }12
上面的代码忽略了索引。同样的,也可以忽略值。
多维数组
目前为止我们创建的数组都是一维的。也可以创建多维数组。
packagemain import( "fmt" ) funcprintarray(a[3][2]string){ for_,v1:=rangea{ for_,v2:=rangev1{ fmt.Printf("%s",v2) } fmt.Printf("\n") } } funcmain(){ a:=[3][2]string{ {"lion","tiger"}, {"cat","dog"}, {"pigeon","peacock"},//thiscommaisnecessary.Thecompilerwillcomplainifyouomitthiscomma } printarray(a) varb[3][2]string b[0][0]="apple" b[0][1]="samsung" b[1][0]="microsoft" b[1][1]="google" b[2][0]="AT&T" b[2][1]="T-Mobile" fmt.Printf("\n") printarray(b) }1234567891011121314151617181920212223242526272829303132
上面的程序中,第17行利用速记声明创建了一个二维数组a
。第20行的逗号是必须的,这是因为词法分析器会根据一些简单的规则自动插入分号。如果你想了解更多,请阅读:https://golang.org/doc/effective_go.html#semicolons。
在第23行声明了另一个二维数组b
,并通过索引的方式给数组b
中的每一个元素赋值。这是初始化二维数组的另一种方式。
第7行声明的函数printarray
通过两个嵌套的rangefor打印二维数组的内容。上面程序的输出为:
liontiger
catdog
pigeonpeacock
applesamsung
microsoftgoogle
AT&TT-Mobile1234567
以上就是对数组的介绍。尽管数组看起来足够灵活,但是数组的长度是固定的,没办法动态增加数组的长度。而切片却没有这个限制,实际上在Go中,切片比数组更为常见。
切片
切片(slice)是建立在数组之上的更方便,更灵活,更强大的数据结构。切片并不存储任何元素而只是对现有数组的引用。
创建切片
元素类型为T
的切片表示为:[]T
。
packagemain import( "fmt" ) funcmain(){ a:=[5]int{76,77,78,79,80} varb[]int=a[1:4]//createsaslicefroma[1]toa[3] fmt.Println(b) }1234567891011
通过a[start:end]
这样的语法创建了一个从a[start]
到a[end-1]
的切片。在上面的程序中,第9行a[1:4]
创建了一个从a[1]
到a[3]
的切片。因此b
的值为:[777879]
。
下面是创建切片的另一种方式:
packagemain import( "fmt" ) funcmain(){ c:=[]int{6,7,8}//createsandarrayandreturnsaslicereference fmt.Println(c) }12345678910
在上面的程序中,第9行c:=[]int{6,7,8}
创建了一个长度为3的int数组,并返回一个切片给c。
修改切片
切片本身不包含任何数据。它仅仅是底层数组的一个上层表示。对切片进行的任何修改都将反映在底层数组中。
packagemain import( "fmt" ) funcmain(){ darr:=[...]int{57,89,90,82,100,78,67,69,59} dslice:=darr[2:5] fmt.Println("arraybefore",darr) fori:=rangedslice{ dslice[i]++ } fmt.Println("arrayafter",darr) }123456789101112131415
上面程序的第9行,我们创建了一个从darr[2]
到darr[5]
的切片dslice
。for
循环将这些元素值加1
。执行完for
语句之后打印原数组的值,我们可以看到原数组的值被改变了。程序输出如下:
arraybefore[5789908210078676959]
arrayafter[5789918310178676959]12
当若干个切片共享同一个底层数组时,对每一个切片的修改都会反映在底层数组中。
packagemain import( "fmt" ) funcmain(){ numa:=[3]int{78,79,80} nums1:=numa[:]//createsaslicewhichcontainsallelementsofthearray nums2:=numa[:] fmt.Println("arraybeforechange1",numa) nums1[0]=100 fmt.Println("arrayaftermodificationtoslicenums1",numa) nums2[1]=101 fmt.Println("arrayaftermodificationtoslicenums2",numa) }12345678910111213141516
可以看到,在第9行,numa[:]
中缺少了开始和结束的索引值,这种情况下开始和结束的索引值默认为0
和len(numa)
。这里nums1
和nums2
共享了同一个数组。程序的输出为:
arraybeforechange1[787980]
arrayaftermodificationtoslicenums1[1007980]
arrayaftermodificationtoslicenums2[10010180]123
从输出结果可以看出,当多个切片共享同一个数组时,对每一个切片的修改都将会反映到这个数组中。
切片的长度和容量
切片的长度是指切片中元素的个数。切片的容量是指从切片的起始元素开始到其底层数组中的最后一个元素的个数。
(译者注:使用内置函数cap
返回切片的容量。)
让我们写一些代码来更好地理解这一点。
packagemain import( "fmt" ) funcmain(){ fruitarray:=[...]string{"apple","orange","grape","mango","watermelon","pineapple","chikoo"} fruitslice:=fruitarray[1:3] fmt.Printf("lengthofslice%dcapacity%d",len(fruitslice),cap(fruitslice))//lengthofis2andcapacityis6 }1234567891011
在上面的程序中,创建了一个以fruitarray
为底层数组,索引从1
到3
的切片fruitslice
。因此fruitslice
长度为2
。
fruitarray
的长度是7。fruiteslice
是从fruitarray
的索引1
开始的。因此fruiteslice
的容量是从fruitarray
的第1
个元素开始算起的数组中的元素个数,这个值是6
。因此fruitslice
的容量是6
。程序的输出为:lengthofslice2capacity6。
切片的长度可以动态的改变(最大为其容量)。任何超出最大容量的操作都会发生运行时错误。
packagemain import( "fmt" ) funcmain(){ fruitarray:=[...]string{"apple","orange","grape","mango","watermelon","pineapple","chikoo"} fruitslice:=fruitarray[1:3] fmt.Printf("lengthofslice%dcapacity%d\n",len(fruitslice),cap(fruitslice))//lengthofis2andcapacityis6 fruitslice=fruitslice[:cap(fruitslice)]//re-slicingfuritslicetillitscapacity fmt.Println("Afterre-slicinglengthis",len(fruitslice),"andcapacityis",cap(fruitslice)) }12345678910111213
在上面的程序中,第11
行修改fruitslice
的长度为它的容量。上面的程序输出如下:
lengthofslice2capacity6 Afterre-slicinglengthis6andcapacityis612
用make创建切片
内置函数funcmake([]T,len,cap)[]T
可以用来创建切片,该函数接受长度和容量作为参数,返回切片。容量是可选的,默认与长度相同。使用make
函数将会创建一个数组并返回它的切片。
packagemain import( "fmt" ) funcmain(){ i:=make([]int,5,5) fmt.Println(i) }12345678910
用make
创建的切片的元素值默认为0值。上面的程序输出为:[00000]
。
追加元素到切片
我们已经知道数组是固定长度的,它们的长度不能动态增加。而切片是动态的,可以使用内置函数append
添加元素到切片。append
的函数原型为:append(s[]T,x...T)[]T
。
x…T表示append
函数可以接受的参数个数是可变的。这种函数叫做变参函数。
你可能会问一个问题:如果切片是建立在数组之上的,而数组本身不能改变长度,那么切片是如何动态改变长度的呢?实际发生的情况是,当新元素通过调用append
函数追加到切片末尾时,如果超出了容量,append
内部会创建一个新的数组。并将原有数组的元素被拷贝给这个新的数组,最后返回建立在这个新数组上的切片。这个新切片的容量是旧切片的二倍(译者注:当超出切片的容量时,append
将会在其内部创建新的数组,该数组的大小是原切片容量的2倍。最后append
返回这个数组的全切片,即从0到length-1的切片)。下面的程序使事情变得明朗:
packagemain import( "fmt" ) funcmain(){ cars:=[]string{"Ferrari","Honda","Ford"} fmt.Println("cars:",cars,"hasoldlength",len(cars),"andcapacity",cap(cars))//capacityofcarsis3 cars=append(cars,"Toyota") fmt.Println("cars:",cars,"hasnewlength",len(cars),"andcapacity",cap(cars))//capacityofcarsisdoubledto6 }123456789101112
在上面的程序中,cars
的容量开始时为3。在第10行我们追加了一个新的元素给cars
,并将append(cars,"Toyota")
的返回值重新复制给cars
。现在cars
的容量翻倍,变为6。上面的程序输出为:
cars:[FerrariHondaFord]hasoldlength3andcapacity3
cars:[FerrariHondaFordToyota]hasnewlength4andcapacity612
切片的0值为nil
。一个nil
切片的长度和容量都为0。可以利用append
函数给一个nil
切片追加值。
packagemain import( "fmt" ) funcmain(){ varnames[]string//zerovalueofasliceisnil ifnames==nil{ fmt.Println("sliceisnilgoingtoappend") names=append(names,"John","Sebastian","Vinay") fmt.Println("namescontents:",names) } }1234567891011121314
在上面的程序中names
为nil
,并且我们把3个字符串追加给names
。程序的输出为:
sliceisnilgoingtoappend
namescontents:[JohnSebastianVinay]12
可以使用...
操作符将一个切片追加到另一个切片末尾:
packagemain import( "fmt" ) funcmain(){ veggies:=[]string{"potatoes","tomatoes","brinjal"} fruits:=[]string{"oranges","apples"} food:=append(veggies,fruits...) fmt.Println("food:",food) }123456789101112
上面的程序中,在第10行将fruits
追加到veggies
并赋值给food
。...
操作符用来展开切片。程序的输出为:food:[potatoestomatoesbrinjalorangesapples]
。
切片作为函数参数
可以认为切片在内部表示为如下的结构体:
typeslicestruct{ Lengthint Capacityint ZerothElement*byte }12345
可以看到切片包含长度、容量、以及一个指向首元素的指针。当将一个切片作为参数传递给一个函数时,虽然是值传递,但是指针始终指向同一个数组。因此将切片作为参数传给函数时,函数对该切片的修改在函数外部也可以看到。让我们写一个程序来验证这一点。
packagemain import( "fmt" ) funcsubtactOne(numbers[]int){ fori:=rangenumbers{ numbers[i]-=2 } } funcmain(){ nos:=[]int{8,7,6} fmt.Println("slicebeforefunctioncall",nos) subtactOne(nos)//functionmodifiestheslice fmt.Println("sliceafterfunctioncall",nos)//modificationsarevisibleoutside }1234567891011121314151617181920
在上面的程序中,第17行将切片中的每个元素的值减2
。在函数调用之后打印切片的的内容,发现切片内容发生了改变。你可以回想一下,这不同于一个数组,对函数内部的数组所做的更改在函数外不可见。上面的程序输出如下:
arraybeforefunctioncall[876] arrayafterfunctioncall[654]12
多维切片
同数组一样,切片也可以有多个维度。
packagemain import( "fmt" ) funcmain(){ pls:=[][]string{ {"C","C++"}, {"JavaScript"}, {"Go","Rust"}, } for_,v1:=rangepls{ for_,v2:=rangev1{ fmt.Printf("%s",v2) } fmt.Printf("\n") } }1234567891011121314151617181920
上面程序的输出如下:
CC++
JavaScript
GoRust123
内存优化
切片保留对底层数组的引用。只要切片存在于内存中,数组就不能被垃圾回收。这在内存管理方便可能是值得关注的。假设我们有一个非常大的数组,而我们只需要处理它的一小部分,为此我们创建这个数组的一个切片,并处理这个切片。这里要注意的事情是,数组仍然存在于内存中,因为切片正在引用它。
解决该问题的一个方法是使用copy函数funccopy(dst,src[]T)int
来创建该切片的一个拷贝。这样我们就可以使用这个新的切片,原来的数组可以被垃圾回收。
packagemain import( "fmt" ) funccountries()[]string{ countries:=[]string{"USA","Singapore","Germany","India","Australia"} neededCountries:=countries[:len(countries)-2] countriesCpy:=make([]string,len(neededCountries)) copy(countriesCpy,neededCountries)//copiesneededCountriestocountriesCpy returncountriesCpy } funcmain(){ countriesNeeded:=countries() fmt.Println(countriesNeeded) }1234567891011121314151617
在上面程序中,第9行neededCountries:=countries[:len(countries)-2]
创建一个底层数组为countries
并排除最后两个元素的切片。第11行将neededCountries
拷贝到countriesCpy
并在下一行返回countriesCpy
。现在数组countries
可以被垃圾回收,因为neededCountries
不再被引用。