go语言学习--数组、切片、map初始化操作和区别

Go语言中,数组是一种具有相同类型固定大小的一种数据结构,数组定义会定义长度

数组的声明语法如下

var 数组变量名 [元素数量]Type
a:= [3]int{}

//我们可以通过如下方式给数组赋值

a[0] = 1

a[1] = 2

a[2] = 3

//数组遍历

for i, v := range a {
fmt.Printf("%d %d\n", i, v)
}

//数组比较

如果两个数组类型相同(包括数组的长度,数组中元素的类型)的情况下,我们可以直接
过较运算符(
==!=)来判断两个数组是否相等,只有当两个数组的所有元素都是相等的时候数组才是相等的,不能比较两个类型不同的数组,否则程序将无法完成编译。

一.在Go中,数组属于基本类型,他们之间的赋值,传递是属于值拷贝,同样的,如果将数组作为参数在函数间传递,也是属于值拷贝
例子:

Array1:=[3] int{}

Array2:=[2] int{1,2}

Array3:=[2] int{4,5}

Array3[0]=6

fmt.Println(Array1, Array2, Array3)

结果:0,0,0;4,5;6,5

二.只有长度相同和类型相同的才能赋值

q := [3]int{1, 2, 3}

q = [4]int{1, 2, 3, 4} // 编译错误:无法将 [4]int 赋给 [3]int

三.数组的长度是固定的,没办法动态增加数组的长度。而切片却没有这个限制,实际上在 Go 中,切片比数组更为常见。

切片

切片(slice)是建立在数组之上的更方便,更灵活,更强大的数据结构。切片并不存储任何元素而只是对现有数组的引用。

创建切片

一:元素类型为 T 的切片表示为: []T。通过数组创建一个切片

    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[start] 到 a[end -1] 的切片。在上面的程序中,第 9 行 a[1:4] 创建了一个从 a[1] 到 a[3] 的切片。因此 b 的值为:[77 78 79]。

二:创建切片的另一种方式:

    c := []int{6, 7, 8} //creates and array and returns a slice reference

    fmt.Println(c)

在上面的程序中,第 9 行 c := []int{6, 7, 8} 创建了一个长度为 3 的 int 数组,并返回一个切片给 c。

三:用 make 创建切片

内置函数 func make([]T, len, cap) []T 可以用来创建切片,该函数接受长度和容量作为参数,返回切片。容量是可选的,默认与长度相同。使用 make 函数将会创建一个数组并返回它的切片。

    i := make([]int, 5, 5)

    fmt.Println(i)

用 make 创建的切片的元素值默认为 0 值。上面的程序输出为:[0 0 0 0 0]。

切片操作:

修改切片

切片本身不包含任何数据。它仅仅是底层数组的一个上层表示。对切片进行的任何修改都将反映在底层数组中。   

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 行,我们创建了一个从 darr[2] 到 darr[5] 的切片 dslice。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]

当若干个切片共享同一个底层数组时,对每一个切片的修改都会反映在底层数组中。 

    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 行, 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] 

从输出结果可以看出,当多个切片共享同一个数组时,对每一个切片的修改都将会反映到这个数组中。

切片的长度和容量

切片的长度是指切片中元素的个数。切片的容量是指从切片的起始元素开始到其底层数组中的最后一个元素的个数。

译者注:使用内置函数 cap 返回切片的容量。

让我们写一些代码来更好地理解这一点。

    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 is 2 and capacity is 6

在上面的程序中,创建了一个以 fruitarray 为底层数组,索引从 1 到 3 的切片 fruitslice。因此 fruitslice 长度为2。

fruitarray 的长度是 7。fruiteslice 是从 fruitarray 的索引 1 开始的。因此 fruiteslice 的容量是从 fruitarray 的第 1 个元素开始算起的数组中的元素个数,这个值是 6。因此 fruitslice 的容量是 6。程序的输出为:length of slice 2 capacity 6。

切片的长度可以动态的改变(最大为其容量)。任何超出最大容量的操作都会发生运行时错误。   

 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 行修改 fruitslice 的长度为它的容量。上面的程序输出如下:

length of slice 2 capacity 6 

After re-slicing length is 6 and capacity is 6

追加元素到切片

我们已经知道数组是固定长度的,它们的长度不能动态增加。而切片是动态的,可以使用内置函数 append 添加元素到切片。append 的函数原型为:append(s []T, x ...T) []T。

x …T 表示 append 函数可以接受的参数个数是可变的。这种函数叫做变参函数

你可能会问一个问题:如果切片是建立在数组之上的,而数组本身不能改变长度,那么切片是如何动态改变长度的呢?实际发生的情况是,当新元素通过调用 append 函数追加到切片末尾时,如果超出了容量,append 内部会创建一个新的数组。并将原有数组的元素被拷贝给这个新的数组,最后返回建立在这个新数组上的切片。这个新切片的容量是旧切片的二倍(译者注:当超出切片的容量时,append 将会在其内部创建新的数组,该数组的大小是原切片容量的 2 倍。最后 append 返回这个数组的全切片,即从 0 到 length - 1 的切片)。下面的程序使事情变得明朗:   

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

在上面的程序中,cars 的容量开始时为 3。在第 10 行我们追加了一个新的元素给 cars,并将 append(cars, "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 

切片的 0 值为 nil。一个 nil 切片的长度和容量都为 0。可以利用 append 函数给一个 nil 切片追加值。   

 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)

在上面的程序中 names 为 nil,并且我们把 3 个字符串追加给 names。程序的输出为:

slice is nil going to append 

names contents: [John Sebastian Vinay]

可以使用 ... 操作符将一个切片追加到另一个切片末尾:  

 veggies := []string{"potatoes","tomatoes","brinjal"}

    fruits := []string{"oranges","apples"}

    food := append(veggies, fruits...)

    fmt.Println("food:",food)

上面的程序中,在第10行将 fruits 追加到 veggies 并赋值给 food。...操作符用来展开切片。程序的输出为:food: [potatoes tomatoes brinjal oranges apples]。

切片作为函数参数

可以认为切片在内部表示为如下的结构体:

type slice struct

    Length        int

    Capacity      int

    ZerothElement *byte

}

可以看到切片包含长度、容量、以及一个指向首元素的指针。当将一个切片作为参数传递给一个函数时,虽然是值传递,但是指针始终指向同一个数组。因此将切片作为参数传给函数时,函数对该切片的修改在函数外部也可以看到。让我们写一个程序来验证这一点。

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。在函数调用之后打印切片的的内容,发现切片内容发生了改变。你可以回想一下,这不同于一个数组,对函数内部的数组所做的更改在函数外不可见。上面的程序输出如下:

array before function call [8 7 6] 

array after function call [6 5 4] 

  • 1
  • 2

多维切片

同数组一样,切片也可以有多个维度。

  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 
 

Map集合:

Map集合声明:

/* 声明变量,默认 map 是 nil */

var map变量名 map[key_data_type]value_data_type
在声明的时候不需要知道 map 的长度,因为 map 是可以动态增长的,未初始化的 map 的值是 nil,使用函数 len() 可以获取 map 中 pair 的数目,cap()获取容量

注意:数组是固定长度容量,不能自动扩充,切片扩充是翻倍增加,map 可以根据新增的 key-value 动态的伸缩,因此它不存在固定长度或者最大限制,但是也可以选择标明 map 的初始容量 capacity

可以使用make来定义:

make(map[keytype]valuetype, cap)

例子:map2 := make(map[string]float, 10)当容量到达上限10会自动加1

如果一个 key 要对应多个值怎么办?

mp1 := make(map[int][]int) 把value 定义成切片[] int
map的循环和数组、切片一致都是用
for _, v := range mapName

map的删除操作:

delete(map, 键)

scene := make(map[string]int)

scene["route"] = 66

scene["brazil"] = 4

scene["china"] = 960

delete(scene, "brazil")

for k, v := range scene {

    fmt.Println(k, v)

}

Go语言中并没有为 map 提供任何清空所有元素的函数、方法,清空 map 的唯一办法就是重新 make 一个新的 map
Go语言sync.Map
在高并发下大量读写map类型会有问题

例子:

// 创建一个int到int的映射

m := make(map[int]int)

// 开启一段并发代码

go func() {

    // 不停地对map进行写入

    for {

        m[1] = 1

    }

}()

// 开启一段并发代码

go func() {

    // 不停地对map进行读取

    for {

        _ = m[1]

    }

}()


错误信息显示,并发的 map 读和 map 写,也就是说使用了两个并发函数不断地对 map 进行读和写而发生了竞态问题,map 内部会对这种并发操作进行检查并提前发现.

Go语言在 1.9 版本中提供了一种效率较高的并发安全的 sync.Map,sync.Map 和 map 不同,不是以语言原生形态提供,而是在 sync 包下的特殊结构。

sync.Map 有以下特性:无须初始化,直接声明即可。
sync.Map 不能使用 map 的方式进行取值和设置等操作,而是使用 sync.Map 的方法进行调用,Store 表示存储,Load 表示获取,Delete 表示删除。
使用 Range 配合一个回调函数进行遍历操作,通过回调函数返回内部遍历出来的值,Range 参数中回调函数的返回值在需要继续迭代遍历时,返回 true,终止迭代遍历时,返回 false。

package main
import (
 "fmt"
 "sync"
)

func main() {
var syncMap sync.Map
// 将键值对保存到sync.Map
syncMap.Store("greece",1)
syncMap.Store("london", 2)
syncMap.Store("egypt", 3)
// 从sync.Map中根据键取值
fmt.Println(syncMap.Load("london"))
// 根据键删除对应的键值对
syncMap.Delete("london")
// 遍历 所有sync.Map中的键值对
syncMap.Range(func(k, v interface{}) bool {
 fmt.Println("iterate:", k, v)
   retun true
 })
}

代码输出如下:
2 true
iterate: egypt 3
iterate: greece 1


sync.Map 没有提供获取 map 数量的方法,替代方法是在获取 sync.Map 时遍历自行计算数量,sync.Map 为了保证并发安全有一些性能损失,因此在非并发情况下,使用 map 相比使用 sync.Map 会有更好的性能。

内存优化

切片保留对底层数组的引用。只要切片存在于内存中,数组就不能被垃圾回收。这在内存管理方便可能是值得关注的。假设我们有一个非常大的数组,而我们只需要处理它的一小部分,为此我们创建这个数组的一个切片,并处理这个切片。这里要注意的事情是,数组仍然存在于内存中,因为切片正在引用它。

解决该问题的一个方法是使用 copy 函数 func copy(dst, src []T) int 来创建该切片的一个拷贝。这样我们就可以使用这个新的切片,原来的数组可以被垃圾回收。

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 行 neededCountries := countries[:len(countries)-2] 创建一个底层数组为 countries 并排除最后两个元素的切片。第 11 行将 neededCountries 拷贝到 countriesCpy 并在下一行返回 countriesCpy。现在数组countries 可以被垃圾回收,因为 neededCountries 不再被引用。

你可能感兴趣的:(go)