0

如何快速学习Go的切片和数组数据类型

 1 year ago
source link: https://blog.51cto.com/u_10992108/5745552
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本文已收录​ ​如何快速学习Go的struct数据类型​​。涵盖PHP、JavaScript、Linux、Golang、MySQL、Redis和开源工具等等相关内容。

什么是数组

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

数组属于类型 。 表示数组中的元素数,并表示每个元素的类型。元素的数量也是类型的一部分(我们稍后将对此进行更详细的讨论。​​[n]TnTn​

有不同的方法来声明数组。让我们一个接一个地看一下。

package main

import (
"fmt"
)


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

var a [3]int 声明一个长度为 3 的整数数组。数组中的所有元素都将自动分配数组类型的零值。在这种情况下是一个整数数组,因此的所有元素都赋给 ,int 的零值。运行上述程序将打印​​a 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() {
// short hand declaration to create array
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)
}

在上面的程序中,第 8 行声明了一个长度为 3 的数组,但只提供了一个值 。其余 2 个元素将自动指定。此程序将打印​​a := [3]int{12} 12 0​

[12 0 0]

您甚至可以忽略声明中数组的长度,并将其替换为,并让编译器为您找到长度。这是在以下程序中完成的。​​...​

package main

import (
"fmt"
)

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

数组的大小是类型的一部分。因此 和 是不同的类型。因此,无法调整数组的大小。

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
}

在上面程序的第 6 行中,我们尝试将类型的变量分配给不允许的类型变量,因此编译器将打印以下错误:

./prog.go:6:7: 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 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)
}

在上面的第 13 行程序中,数组实际上是按值传递给函数的,因此不会因为函数调用而更改。该程序将打印:

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++ { //looping from 0 to the length of the array
fmt.Printf("%d th element of a is %.2f\n", i, a[i])
}
}

上面的程序使用循环来迭代数组的元素,从 index 到 。该程序有效并将打印:

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 提供了一种更好、更简洁的方式,通过使用循环的范围形式来迭代数组。 返回索引和该索引处的值。让我们使用范围重写上面的代码。我们还将找到数组中所有元素的总和。

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)
}

上述程序的第 8 行是 for 循环的范围形式。它将返回索引和该索引处的值。我们打印值,并计算数组中所有元素的总和。该程序的输出是:

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
}

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

到目前为止,我们创建的数组都是单维的。可以创建多维数组。

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)
}

在上面的程序的第 17 行中,使用短手语法声明了一个二维字符串数组。第 20 行末尾的逗号是必需的。这是因为词法分析器根据简单规则自动插入分号。如果您有兴趣了解有关为什么需要分号的更多信息,请阅读​ ​参考文章​​。另一个 2d 数组在第 23 行中声明,并为每个索引逐个添加字符串。这是初始化 2d 数组的另一种方法。

第 7 行中的函数使用两个 for 范围循环来打印 2d 数组的内容。以上程序将打印:

lion tiger
cat dog
pigeon peacock

apple samsung
microsoft google
AT&T T-Mobile

数组就是这样。尽管数组似乎足够灵活,但它们具有固定长度的限制。不能增加数组的长度。这就是切片进入画面的地方。事实上,在 Go 中,切片比传统数组更常见。

切片是数组顶部的方便、灵活且功能强大的包装器。切片本身不拥有任何数据。它们只是对现有数组的引用。

具有 T 类型元素的切片由下式表示​​[]T​​。

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)
}

该语法从一个数组开始从一个索引到另一个索引创建一个切片。因此,在上面程序的第 9 行中,从索引 1 到 3 创建数组的切片表示形式。使用该表达式:​​a[start:end]​​。

package main

import (
"fmt"
)

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

在上面的函数的第 9 行中,创建一个包含 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)
}

在上面程序的第 9 行中,我们从数组的索引 2、3、4 创建。for 循环将这些索引中的值递增 1。当我们在for循环之后打印数组时,我们可以看到对切片的更改反映在数组中。程序的输出是:

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)
}

在第 9 行中,缺少开始值和结束值。“开始”和“结束”的默认值分别为 和 。两个切片并共享同一个数组。程序的输出是:

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
}

在上面的程序中,是从 的索引 1 和 2 创建的。因此,的长度为 2。

切片可以重新切片到其容量。超出此值的任何内容都将导致程序引发运行时错误。

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))
}

在上述程序的第11行中,被重新切片到其容量。以上程序输出:

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

使用make创建切片

生成切片可以使用make([]T, len, cap)来创建切片。[]T表示数据类型、len表示切片长度、cap表示切片容量。make 函数创建一个数组并返回对它的切片引用。

package main

import (
"fmt"
)

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

默认情况下,使用 make 创建切片时,这些值将清零。上述程序将输出 。​​[0 0 0 0 0]​

正如我们已经知道的那样,数组被限制为固定长度,并且它们的长度不能增加。切片是动态的,可以使用函数将新元素追加到切片中。追加函数的定义是 。​​append([]T, x...):[]T​​。返回的是一个新的切片。 x ...函数定义中的 T 表示函数接受参数 x 的可变数量的参数。这些类型的函数称为​ ​可变参数函数​​。

不过,有一个问题可能会困扰您。如果切片由数组支持,并且数组本身具有固定长度,那么切片如何具有动态长度。在引擎盖下发生的事情是,当新元素追加到切片时,将创建一个新数组。现有数组的元素将复制到此新数组,并返回此新数组的新切片引用。新切片的容量现在是旧切片的两倍。以下程序将使事情变得清晰。

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
}

在上面的程序中,容量最初为 3。我们将新元素附加到第 10 行中的汽车,并将 返回的切片再次分配给汽车。现在,汽车的容量翻了一番,变成了6辆。上述程序的输出是:

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

切片类型的零值为 。切片的长度和容量为 0。可以使用追加函数将值追加到切片。

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)
}
}

在上面的程序中为 nil,我们已将 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)
}

在上述程序的第 10 行中,通过附加到切片中。程序的输出是:

fruitsveggiesfood: [potatoes tomatoes brinjal oranges apples]

切片作为函数参数

可以将切片视为由结构类型在内部表示。

type slice struct {
Length int
Capacity int
ZerothElement *byte
}

切片包含长度、容量和指向数组第零个元素的指针。将切片传递给函数时,即使它按值传递,指针变量也将引用相同的基础数组。因此,当切片作为参数传递给函数时,在函数内部所做的更改在函数外部也是可见的。让我们编写一个程序来检查一下。

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
}

上述程序第 17 行中的函数调用将切片的每个元素递减 2。在函数调用后打印切片时,这些更改是可见的。如果您还记得,这与数组不同,在数组中,对函数内部的数组所做的更改在函数外部不可见。上述程序的输出是

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(dst, src []T):int​​。

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)
}

在上面程序的第 9 行中,创建一个禁止最后 2 个元素的切片。上述程序的第 11 行复制到下一行中的函数,并从中返回它。现在数组可以被垃圾回收,因为它不再被引用。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK