第十一篇 数组和切片

欢迎来到Golang教程系列的第十一篇

Array 数组

array数组是同类型元素的集合。例如,整数的集合5,8,9,79,76组成一个数组。要是混合不同类型的值,例如,一个数组同时包含字符串和整数在Go中是不允许的。

声明

一个数组的类型是[n]Tn是数组元素的个数,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循环迭代数组的元素,从0length 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循环的rangerange返回索引和索引的值,让我们用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[:]省略了开始和结束,而开始和结束的默认值分别是0len(numa)。切片nums1nums2共享同一个数组,下面是程序的输出。

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

切片的零值是nilnil切片的长度和容量都是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)就是复制neededContriescontriesCpy,然后在函数下一行返回它。现在,contries可以垃圾回收了,因为neededContries不再引用它啦。

以上就是本篇的全部,感谢阅读,这是我第一次翻译,难免会有翻译不当的地方,如果有什么反馈和评论,欢迎提出来!

原文地址: https://golangbot.com/arrays-and-slices/