Go语言 数组与切片

导言

  • 原文链接: Part 11: Arrays and Slices
  • If translation is not allowed, please leave me in the comment area and I will delete it as soon as possible.

数组

数组是相同类型元素的集合。举个例子,[5, 8, 9, 79, 76]就组成了一个数组。在Go语言中,数组元素的类型必须是相同的。

声明

数组属于类型[n]Tn 表示数组元素的数量,T 表示元素类型。注意,n 也是该类型的一部分。(我们将在下面讨论)

我们有很多方式声明数组。接下来,我来一一展示。

package main

import (  
    "fmt"
)


func main() {  
    var a [3]int //int array with length 3
    fmt.Println(a)
}

var a [3]int 声明了一个长度为 3 的整型数组。数组中的每个元素,都会被自动分配为该类型的零值。在这个例子中,a 是一个整型数组,所以它的所有元素将被分配为 0 — 整型的零值。运行上面的程序将会输出 [0 0 0]

数组的索引范围为[0, length - 1]。接下来,我们为上面的数组分配一些值。

package main

import (  
    "fmt"
)


func main() {  
    var a [3]int //int array with length 3
    a[0] = 12 // array index starts at 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} // short hand declaration to create array
    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 。剩余的 2 个元素将被自动分配为 0。运行程序,将会输出[12 0 0]

在声明数组时,你甚至可以省略长度,使用 ... 来让编译器识别数组长度。下面这个程序就是这样做的。

package main

import (  
    "fmt"
)

func main() {  
    a := [...]int{12, 78, 50} // ... makes the compiler determine the length
    fmt.Println(a)
}

数组长度也是类型的一部分。因此,[5]int[25]int 是不同的类型。也因为这个原因,数组大小不能改变。不必担心,因为 切片 可以克服这个限制。

package main

func main() {  
    a := [3]int{5, 78, 8}
    var b [5]int
    b = a //not possible since [3]int and [5]int are distinct types
}

在上面这个程序,我们试图把 [3]int数组 分配给[5]int数组,但这是不允许的,编译器将会抛出一个异常:
cannot use a (type [3]int) as type [5]int in assignment.

数组是值类型

Go 中,数组是值类型,而不是引用类型。这意味着,当数组被分配给一个新的变量时,新变量的数组只是原数组的拷贝。任何在新数组上的修改,都不会影响原始数组。

package main

import "fmt"

func main() {  
    a := [...]string{"USA", "China", "India", "Germany", "France"}
    b := a // a copy of a is assigned to b
    b[0] = "Singapore"
    fmt.Println("a is ", a)
    fmt.Println("b is ", b) 
}

在上面程序中,a 的拷贝被分配给 b。随后,数组b 的第一个元素被修改为 Singapore,但这并不会影响 原始数组a。运行这个程序,输出如下:

a is [USA China India Germany France]  
b is [Singapore China India 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) //num is passed by value
    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

使用 range 遍历数组

for 循环可以遍历数组的元素。

package main

import "fmt"

func main() {  
    a := [...]float64{67.7, 89.8, 21, 78}
    for i := 0; i < len(a); i++ { //looping from 0 to the length of the array
        fmt.Printf("%d th element of a is %.2f\n", i, a[i])
    }
}

在上面的程序中,for 循环遍历了整个数组。程序输出如下:

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 returns both the index and value
        fmt.Printf("%d the element of a is %.2f\n", i, v)
        sum += v
    }
    fmt.Println("\nsum of all elements of a",sum)
}

在上面的程序中,for i, v := range a 就是 for 循环的 range 形式。运行这个程序,输出如下:

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 elements of a 256.5  

如果你只需要数值,不需要索引,那你可以使用 _空白标识符 去替换 索引。

for _, v := range a { //ignores index  
}

上面的循环忽略了索引。同样的,数值也可以被忽略。

多维数组

到目前为止,我们创建的都是一维数组。接下来,我们来创建一个多维数组。

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"}, //this comma is necessary. The compiler will complain if you omit this comma
    }
    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。第 20 行的逗号是必须的,这是因为如果没有逗号,词法分析器会自动的插入分号,从而导致错误。如果你有兴趣,可以访问这个 网站 进行深入理解。

在上面的代码中,我们还声明了一个 二维数组b ,并一对一的对其赋值。这种方法也可以初始化二维数组。

printarray 函数使用了两个嵌套的 for 循环输出二维数组的内容。运行上面的程序,输出如下:

lion tiger  
cat dog  
pigeon peacock 

apple samsung  
microsoft google  
AT&T T-Mobile  

这就是数组。虽然数组看起来已经足够灵活,但数组长度是固定的,这是它的缺点。我们不能增长数组的长度。于是,切片 出现了。事实上,在 Go语言 中,切片数组 更常用。

切片

切片是数组之上的封装,它方便、灵活、功能强大。切片并不会拥有原始数据,它们只是引用了数组。

package main

import (  
    "fmt"
)

func main() {  
    a := [5]int{76, 77, 78, 79, 80}
    var b []int = a[1:4] //creates a slice from a[1] to a[3]
    fmt.Println(b)
}

句式 a[start:end] 表示:在 数组a 的基础上,创建了一个切片,索引区间为 [0, end - 1]。所以,a[1:4] 创建了一个切片,这个切片引用了数组区间 [1, 3] 的元素。因此,切片b 的值是 [77 78 79]

我们还有另外一种方式创建切片。

package main

import (  
    "fmt"
)

func main() {  
    c := []int{6, 7, 8} //creates and array and returns a slice reference
    fmt.Println(c)
}

在上面的程序中,c := []int{6, 7, 8} 创建了一个长度为 3 的数组、返回一个切片引用、并将该引用传递给 切片c

切片的修改

切片并不会拥有任何数据。它只是引用了底层数组。因此,任何对切片的修改,最终都会反映在底层数组。

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 ,引用了数组索引 2, 3, 4for循环 使切片的元素都增加 1。运行这个程序,我们将会发现: 对切片的修改作用到了底层数组。输出如下:

array before [57 89 90 82 100 78 67 69 59]  
array after [57 89 91 83 101 78 67 69 59]  

当大量的切片共享底层数组,此时,对这些切片的修改都会影响底层数组。

package main

import (  
    "fmt"
)

func main() {  
    numa := [3]int{78, 79 ,80}
    nums1 := numa[:] //creates a slice which contains all elements of the array
    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)
}

在上面的程序中,num[:] 其实是 num[0:len(num)] 的简写形式。由于 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]  

切片的长度与容量

切片的长度表示切片拥有的元素数量。而切片的容量,表示的是切片指向的底层数组拥有的元素数量。

我们使用代码来解释吧~

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 引用了 fruitarray1、2 索引,因此 fruitslice 长度为 2

fruitarray的长度是 7fruitslice 引用了 fruitarray1、2 索引,因此它的容量为 6 — 因为它引用的开始索引是 1,如果从 0 开始引用,那么它的容量就是 7

运行上面的程序,输出将是:
length of slice 2 capacity 6

对于一个切片,它能访问的最大索引为 容量 - 1 超过最大索引的访问将会引发运行时错误。

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 is 2 and capacity is 6
    fruitslice = fruitslice[:cap(fruitslice)] //re-slicing furitslice till its capacity
    fmt.Println("After re-slicing length is",len(fruitslice), "and capacity is",cap(fruitslice))
}

运行上面的程序,输出如下:

length of slice 2 capacity 6  
After re-slicing length is 6 and capacity is 6  

使用 make 创建切片

func make([]T, len, cap) []T 函数 传递切片的类型、长度和容量,可以创建一个切片。cap 参数是可选的,默认等于 lenmake 函数将会创建一个数组,并返回一个引用该数组的切片。

package main

import (  
    "fmt"
)

func main() {  
    i := make([]int, 5, 5)
    fmt.Println(i)
}

使用 make 创建切片时,切片元素的初始值是对应类型的零值。上面的程序输出为 [0 0 0 0 0]

为切片追加元素

我们已经知道了,数组的长度是固定的。但,切片是动态的,使用 append函数 ,我们可以为其追加元素。append函数 定义为 func append(s []T, x ...T) []T

在函数定义中,x ...T 表示 x 是一个接收可变长度的参数。相应的,拥有可变长度参数的函数,被称为 可变函数(variadic functions)

现在,你可能会疑惑: 切片的底层是一个数组,而数组长度是固定的,那么切片长度为何可变呢?解释是: 当我们为切片追加元素的时候,一个新数组将会被创建,随后,原数组的元素将被拷贝给新数组,并返回一个对新数组的引用。新切片的容量将是旧切片的两倍。

这里有点谬误,因为有时并不会创建一个新数组,只有当原数组装不下所有追加后的数据时,此时才会创建一个新数组去容纳。
新切片的容量也不一定总是旧切片的两倍,当旧切片的长度小于 1024 时,新切片的容量才是旧切片的两倍

来看个代码。

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 new length", len(cars), "and capacity", cap(cars)) //capacity of cars is doubled to 6
}

在上面的程序中,cars 的容量最初是 3。当我们为其分配一个新的元素 Toyota、并将返回的切片重新传递给 cars 后,此时 cars 的容量将变为 6。程序输出如下:

cars: [Ferrari Honda Ford] has old length 3 and capacity 3  
cars: [Ferrari Honda Ford Toyota] has new length 4 and capacity 6  

切片的零值是 nilnil 切片的长度和容量都为 0。但是,我们依旧可以使用 append函数 为其追加元素。

package main

import (  
    "fmt"
)

func main() {  
    var names []string //zero value of a slice is nil
    if names == nil {
        fmt.Println("slice is nil going to append")
        names = append(names, "John", "Sebastian", "Vinay")
        fmt.Println("names contents:",names)
    }
}

在上面程序中,namesnil,我们为其追加 3 个元素。程序输出如下:

slice is nil going to append  
names contents: [John Sebastian Vinay]  

我们也可以使用 ... 操作符为一个切片,追加另外一个切片。

package main

import (  
    "fmt"
)

func main() {  
    veggies := []string{"potatoes","tomatoes","brinjal"}
    fruits := []string{"oranges","apples"}
    food := append(veggies, fruits...)
    fmt.Println("food:",food)
}

在上面的程序中,我们通过 append函数,为 fruits切片 追加一个切片 veggies,并把返回的引用传递给 food。运行上面的程序,输出如下:

food: [potatoes tomatoes brinjal oranges apples]

将切片传递给函数

切片可以用以下结构体进行表示:

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)                               //function modifies the slice
    fmt.Println("slice after function call", nos) //modifications are visible outside
}

这个函数的作用就是:将切片的每个值都增加 2。在函数调用结束后,输出 slice,我们会发现,这些改变影响了原切片。回忆一下,当我们在一个函数内部修改数组时,这些改变并不会作用于原数组。运行上面的程序,输出如下:

slice before function call [8 7 6]  
slice after function call [6 5 4]  

多维切片

和数组类似,切片也可以有多个维度。

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函数。copy函数 定义为 func copy(dst, src []T) int,它将生成一个原切片的深拷贝。于是,我们可以使用 copy 来产生新切片,从而使底层的数组能被垃圾收集。(可能有点迷惑,看下面)

假如原数组长度是 1000a 切片引用了它的 3 个元素,这使得原数组的内存不能被回收。于是我们可以使用 copy 函数,copy(b, a) 会在内存中生成一个长度为 3 的底层数组,并把 a 的数组元素传递给该底层数组,之后让 b 指向这个底层数组。于是,当 a 对原数组的引用被移除,长度为 1000 的原数组就可以被垃圾回收,而我们还能使用原数组的数据。

来看段代码:

package main

import (  
    "fmt"
)

func countries() []string {  
    countries := []string{"USA", "Singapore", "Germany", "India", "Australia"}
    neededCountries := countries[:len(countries)-2]
    countriesCpy := make([]string, len(neededCountries))
    copy(countriesCpy, neededCountries) //copies neededCountries to countriesCpy
    return countriesCpy
}
func main() {  
    countriesNeeded := countries()
    fmt.Println(countriesNeeded)
}

在上面程序中,neededCountries := countries[:len(countries)-2]countries数组 之上创建了一个切片。copy(countriesCpy, neededCountries)neededCountries 深拷贝给 countriesCpy,随后返回 countriesCpy。此时 countries数组 就可以被垃圾回收,因为 neededCountries 不再引用它了。

原作者留言

我已经把上面的概念整合到了一个程序,你可以在 github 下载。

优质内容来之不易,您可以通过该 链接 为我捐赠。

最后

感谢原作者的优质内容。

这是我的第一次翻译,欢迎指出文中的任何错误。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值