第十一篇 数组和切片
欢迎来到Golang教程系列的第十一篇
Array 数组
array数组是同类型元素的集合。例如,整数的集合5,8,9,79,76组成一个数组。要是混合不同类型的值,例如,一个数组同时包含字符串和整数在Go中是不允许的。
声明
一个数组的类型是[n]T,n是数组元素的个数,T是代表每个元素的类型。元素的个数n同样也是类型的一部分。(我们很快会讨论这个部分)
声明数组的方式有很多种,让我们一个一个地看。
package main
import (
"fmt"
)
func main() {
var a [3]int //长度为3的int数组
fmt.Println(a)
}
var a [3]int声明了一个长度为3的int类型数组,数组类型的所有元素在数组中会自动赋值为零值,在上面的例子中,a是一个整型数组,因此a的所有元素都赋值为int的零值0,运作上面的程序会打印出。
[0 0 0]
注:string类型的零值为"",number类型的零值为0,bool类型的零值为false
数组的索引(下标、index)是从0开始到 length-1,让我们给上面的数组赋值。
package main
import (
"fmt"
)
func main() {
var a [3]int //长度为3的int数组
a[0] = 12 // 数组索引是从0开始
a[1] = 78
a[2] = 50
fmt.Println(a)
}
a[0]是给数组的第一个元素赋值,上面程序会打印
[12 78 50]
下面我们使用简短声明创建一个相同的数组
package main
import (
"fmt"
)
func main() {
a := [3]int{12, 78, 50} //简短声明创建数组
fmt.Println(a)
}
上面的程序打印同样的输出。
[12 78 50]
在简短声明中,并不是所有的元素都需要赋值的。
package main
import (
"fmt"
)
func main() {
a := [3]int{12} //简短声明创建数组
fmt.Println(a)
}
在上面的程序中,a := [3]int{12} 声明了一个长度为3的数组,但是只提供了一个值12,剩下的两个元素会自动赋值为0,上面的程序输出。
[12 0 0]
你甚至可以忽略声明中数组的长度,通过使用...代替,编译器会为你找到适合的长度。下面的程序就是这么做的。
package main
import (
"fmt"
)
func main() {
a := [...]int{12,78,50} //编译器会判断长度
fmt.Println(a)
}
数组的大小也是类型的一部分,因此,[5]int和[25]int是不同的类型,也因如此,数组是不可以调整大小。不用担心这个限制,因为后面的slices会解决这个问题。
package main
func main() {
a := [3]int{5, 78, 8}
var b [5]int
b = a //这样是不可以的,因为[3]int和[5]int是不同的类型
}
在上面程序中,b = a,我们尝试给一个[5]int类型的变量赋值[3]int类型的变量是不允许的,因此,编译器会打印下面的错误。
./arr.go:11:7: cannot use a (type [3]int) as type [5]int in assignment
数组是值类型(value types)
Go的数组是值类型,并不是引用类型。这意味着当它们赋值给一个新的变量时,会复制原数组到新的变量。如果修改了新的变量,就不会影响到原数组。
package main
import (
"fmt"
)
func main() {
a := [...]string{"China","USA","Germany","France"}
b := a // a的副本赋值给b
b[0] = "Singapore"
fmt.Println("a is", a)
fmt.Println("b is", b)
}
在上面的程序中,b := a,把a的副本赋值给b,b[0] = "Singapore",修改b的第一个元素为Singapore,这不会影响原数组a,上面的程序会输出
a is [China USA Germany France]
b is [Singapore USA Germany France]
类似的,当你把数组传输为函数参数时,只会传输值,不会改变原数组。
package main
import (
"fmt"
)
func changeLocal(num [5]int) {
num[0] = 55
fmt.Println("inside function ", num)
}
func main() {
num := [...]int{5,6,7,8,8}
fmt.Println("before passing to function ", num)
changeLocal(num)
fmt.Println("after passing to function ", num)
}
上面程序中,数组num实际只是传了值给changeLocal函数,所以不会因为函数调用而改变。下面的程序的输出。
before passing to function [5 6 7 8 8]
inside function [55 6 7 8 8]
after passing to function [5 6 7 8 8]
数组的长度
通过将数组作为参数传到len函数可以获取数组的长度
package main
import (
"fmt"
)
func main() {
a := [...]float64{67.7, 89.8, 21, 78}
fmt.Println("length of a is ", len(a))
}
上面的程序输出
length of a is 4
迭代数组的使用范围
for循环可以用来遍历访问数组的元素
package main
import (
"fmt"
)
func main() {
a := [...]float64{67.7, 89.8, 21, 78}
for i := 0; i < len(a); i++ {
fmt.Printf("%d th element of a is %.2f\n", i, a[i])
}
}
上面的程序中,使用for循环迭代数组的元素,从0到length of array - 1,程序将会输出
0 th element of a is 67.70
1 th element of a is 89.80
2 th element of a is 21.00
3 th element of a is 78.00
Go有提供了一个更好更简单的方式来遍历访问,就是使用for循环的range,range返回索引和索引的值,让我们用range重写上面的程序,而且我们新增了获取数组所有元素的和。
package main
import (
"fmt"
)
func main() {
a := [...]float64{67.7, 89.8, 21, 78}
sum := float64(0)
for i,v := range a { // range返回索引和值
fmt.Printf("%d the element of a is %.2f\n", i, v)
sum += v
}
fmt.Println("\nsum of all element of a", sum)
}
上面程序中for i, v := range a,for循环中的range,会返回当前索引的索引和值。我们打印值并且计算a数组元素的和,上面程序会输出。
0 the element of a is 67.70
1 the element of a is 89.80
2 the element of a is 21.00
3 the element of a is 78.00
sum of all element of a 256.5
如果你只想要值而不需要索引,你可以用空白标识符_代替索引。
for _, v := range a { //忽略索引
}
多维数组
到目前为止我们创建的数组都是一维的,下面我们创建多维数组。
package main
import (
"fmt"
)
func printarray(a [3][2]string) {
for _, v1 := range a {
for _, v2 := range v1 {
fmt.Printf("%s ", v2)
}
fmt.Printf("\n")
}
}
func main() {
a := [3][2]string {
{"lion", "tiger"},
{"cat", "dog"},
{"pigeon", "peacock"}, // 这后面的逗号是必须的,不然会报错
}
printarray(a)
var b[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)
}
在上面程序中,二维数组a使用了简短声明,注意结尾的逗号是必须的,这是因为语法分析程序会根据简单规则在符号(标识符、数字、字符串常量等等,不是标点符号)后面自动插入分号,一句话来说,如果一个符号是语句的结尾,并且后面接着新的一行,那么语法分析器会在符号的后面加上分号(semicolon)。如果你对此感兴趣的话,可以阅读有关信息https://golang.org/doc/effective_go.html#semicolons .
二维数组b则是通过一个个索引来赋值,这是另外一个初始化二维数组的方法。
printarray函数使用了两个for rang循环打印二维数组的内容。上面的程序会打印出
lion tiger
cat dog
pigeon peacock
apple samsung
microsoft google
At&T T-Mobile
上面就是二维数组的内容,尽管数组看起来足够灵活了,但是它们有固定长度的限制,不可以新增一个数组的长度。然后就是slices出场了,实际上,在Go开发中,slices比arrays更常用。
Slices
Slices是一个方便、灵活和强大的位于数组之上的包装器,Slices本身并不拥有数据,它们只是引用已存在的数据。
创建slice
带有元素类型T的slice用[]T来表示
package main
import "fmt"
func main() {
a := [5]int {76, 77,78, 79,80}
var b []int = a[1:4] // 创建一个a的切片,从a[1]到a[3]
fmt.Println(b)
}
语法a[start:end]就是创建一个数组a的切片,是从索引start到索引end-1,所以上面程序中a[1:4]就是创建一个切片,它表示数组a从索引1到3的数据。因此,sliceb的值为[77, 78, 79]
让我们看另外一种创建slice的方式
package main
import "fmt"
func main() {
c := []int {6, 7, 8} // 创建一个数组并且返回slice引用
fmt.Println(c)
}
在上面的程序中,c := []int{6, 7, 8}创建了一个三个整数的数组,并且返回了slice引用保存在c。
修改slice
slice切片自身并不拥有任何数据,它只是一个表示潜在的数组。任何对于slice的修改,都会反射到潜在的数组中去。
package main
import "fmt"
func main() {
darr := [...]int{57, 89, 90, 82, 100, 78, 67, 69, 59}
dslice := darr[2:5]
fmt.Println("array before", darr)
for i := range dslice {
dslice[i]++
}
fmt.Println("array after", darr)
}
在上面程序中,dslice := darr[2:5]创建了数组darr索引2、3、4的切片dslice。for循环给这些索引的值都加1,当我们在循环之后打印数组,我们可以看到slice的改变也会影响数组。下面是程序的输出
array before [57 89 90 82 100 78 67 69 59]
array after [57 89 91 83 101 78 67 69 59]
当若干个slices共享一个数组时,每个切片的修改都会反射到数组上。
package main
import "fmt"
func main() {
numa := [3]int {78, 79, 80}
nums1 := numa[:] //创建一个全数组元素的切片
nums2 := numa[:]
fmt.Println("array before change 1", numa)
nums1[0] = 100
fmt.Println("array after modification to slice nums1", numa)
nums2[1] = 101
fmt.Println("array after modification to slice nums2", numa)
}
在上面程序中,numa[:]省略了开始和结束,而开始和结束的默认值分别是0和len(numa)。切片nums1和nums2共享同一个数组,下面是程序的输出。
array before change 1 [78 79 80]
array after modification to slice nums1 [100 79 80]
array after modification to slice nums2 [100 101 80]
根据上面输出,我们很明显看到切片共享同一个数组,切片的修改都会反射到数组中去。
slice的长度和容量
slice切片的长度就是slice里面元素的个数,切片的容量是从创建切片的索引开始的基础数组中的元素数
让我们通过写代码来理解。
package main
import "fmt"
func main() {
fruitarray := [...]string{"apple", "orange", "grape", "mango", "water melon", "pine apple", "chikoo"}
fruitslice := fruitarray[1:3]
fmt.Printf("length of slice %d capacity %d", len(fruitslice), cap(fruitslice)) // length of fruitslice is 2 and capacity is 6
}
上面程序中,fruitslice是通过fruitarray索引1,2创建。所以,fruitslice的长度是2.
fruitarray的长度是7, fruitslice是从数组的索引1开始创建,因此,fruitslice的容量是从fruitarray索引1开始的元素个数。例如,从orange开始,元素的个数是6,因此,fruitslie的容量是6.上面的程序输出length of slice 2 capacity 6
切片可以重新划分它的容量,超出这个范围将导致程序抛出运行时错误。
package main
import "fmt"
func main() {
fruitarray := [...]string{"apple", "orange", "grape", "mango", "water melon", "pine apple", "chikoo"}
fruitslice := fruitarray[1:3]
fmt.Printf("length of slice %d capacity %d\n", len(fruitslice), cap(fruitslice)) // length of fruitslice is 2 and capacity is 6
fruitslice = fruitslice[:cap(fruitslice)] // 重新切分到它的容量大小
fmt.Printf("After re-slicing length of slice %d capacity %d\n", len(fruitslice), cap(fruitslice))
}
在上面程序中,fruitslice重新切分它的容量。上面的程序输出。
length of slice 2 capacity 6
After re-slicing length of slice 6 capacity 6
用make创建slice切片
func make([]T, len, cap)[]T可以用来创建切片,传入参数type, length, capacity。capacity参数是可选的,默认数组长度。make函数创建一个数组,并且返回引用这个数组的切片。
package main
import "fmt"
func main() {
i := make([]int, 5, 5)
fmt.Println(i)
}
用make创建的切片的默认值为零值。上面的程序输出[0 0 0 0 0]
Appending to a slice
我们知道数组是有限制固定大小而且它们的长度是不可以增加的。而Slice是动态的,可以通过使用append函数给slice添加新元素。append函数的定义是append(s []T, x ...T) []T。
x...T在函数定义中表示的是函数接受形参x的参数数目可变的。这种函数叫可变函数(variadic functions),我们下一篇会讲到。
有个问题可能会让你疑惑,如果slice是通过array创建的,但是array的长度又是固定的,那么slice的动态长度是怎么整的呢?实际上,在底层下发生的是,当在slice后面新增一个元素时,就会创建一个新的数组。原来数组的元素会复制到新的数组,然后新的slice就会引用这个新的数组并且返回。新的slice的容量就是旧的slice的容量的两倍,是不是很酷呀。老规矩,代码出真知。
package main
import "fmt"
func main() {
cars := []string{"Ferrari", "Honda", "Ford"}
fmt.Println("cars:", cars, "has old length", len(cars),"and capacity", cap(cars)) //capacity of cars is 3
cars = append(cars, "Toyota")
fmt.Println("cars:", cars, "has old length", len(cars),"and capacity", cap(cars)) //capacity of cars is doubled to 6
}
在上面程序中,cars的初始容量是3,cars = append(cars, "Toyota")然后我们给cars新增了一个元素并且重新返回了cars的值。现在cars的容量加倍为6了。上面的程序输出。
cars: [Ferrari Honda Ford] has old length 3 and capacity 3
cars: [Ferrari Honda Ford Toyota] has old length 4 and capacity 6
切片的零值是nil,nil切片的长度和容量都是0,我们可以通过使用append函数给nil切片新增元素。
package main
import "fmt"
func main() {
var names []string // slice的零值是nil
if names == nil {
fmt.Println("slice is nil going to append")
names = append(names, "John", "Sebastian", "Vinay")
fmt.Println("names contents:", names)
}
}
上面程序中,names是nil,然后我们给names新增了3个字符串,上面程序输出。
slice is nil going to append
names contents: [John Sebastian Vinay]
也可以使用...操作符添加一个slice给另外一个slice。关于这个操作符,下一篇我们会详细讲解。
package main
import "fmt"
func main() {
veggies := []string{"potatoes", "tomatoes", "brinjal"}
fruits := []string{"oranges", "apples"}
food := append(veggies, fruits...)
fmt.Println("food:", food)
}
上面程序中,food是通过appendfruits,veggies创建的,程序输出:food: [potatoes tomatoes brinjal oranges apples]
给函数传切片
slice可以用结构体表示它的内部,它是长这样
type slice struct {
Length int
Capacity int
ZerothElement *byte
}
一个切片包含长度、容量和指向数组第0个元素的指针,当把切片传输函数时,即使它传的是值,指针变量依旧会引用引用同样的底层数组,因此,当一个切片传递给函数做参数时,在函数里面改变切片也会使得函数外面改变。让我们写个程序看看。
package main
import "fmt"
func subtactOne(numbers []int) {
for i := range numbers {
numbers[i] -= 2
}
}
func main() {
nos := []int{8, 7, 6}
fmt.Println("slice before function call", nos)
subtactOne(nos) // 函数改变了切片
fmt.Println("slice after function call", nos) //修改会反射到函数外面
}
在上面程序中,subtactOne(nos)函数调用把slice切片的每个元素都减2,我们在函数调用后再打印切片,可以看到切片改变了。如果您还记得的话,这与数组不同,在数组中,对函数内部的数组所做的更改在函数之外是不可见的。上面程序的输出如下。
slice before function call [8 7 6]
slice after function call [6 5 4]
多维切片
跟数组相似,切片slice也可以有多重维度。
package main
import "fmt"
func main() {
pls := [][]string {
{"C", "C++"},
{"JavaScript"},
{"Go","Rust"},
}
for _, v1 := range pls {
for _, v2 := range v1 {
fmt.Printf("%s ", v2)
}
fmt.Printf("\n")
}
}
上面程序输出
C C++
JavaScript
Go Rust
内存优化
切片保持底层数组的引用,只要切片在数组中,数组就不会被垃圾回收,这让我们在内存管理时会有些忧虑。我们假设我们有一个非常大的数组,但是我们只对于执行其中的一小部分感兴趣。从今以后,我们可以从数组中创建一个切片,并执行操作这个切片。重要的需要注意的是因为切片还在引用,数组仍在内存中。
解决这个问题的一个方法是使用copy函数func copy(dst, src []T) int创建切片的副本。这个方法我们可以使用新的切片,而源数组就可以被垃圾回收了。
package main
import "fmt"
func contries() []string {
contries := []string{"USA", "Singapore", "Germany", "India", "Australia"}
neededContries := contries[:len(contries)-2]
contriesCpy := make([]string, len(neededContries))
copy(contriesCpy, neededContries) // 复制neededContries到contriesCpy
return contriesCpy
}
func main() {
contriesNeeded := contries()
fmt.Println(contriesNeeded)
}
在上面程序中,neededContries := contries[:len(contries)-2]创建了一个切片除了contries的最后两个元素,copy(contriesCpy, neededContries)就是复制neededContries到contriesCpy,然后在函数下一行返回它。现在,contries可以垃圾回收了,因为neededContries不再引用它啦。
以上就是本篇的全部,感谢阅读,这是我第一次翻译,难免会有翻译不当的地方,如果有什么反馈和评论,欢迎提出来!
浙公网安备 33010602011771号