4

兼容并蓄广纳百川,Go lang1.18入门精炼教程,由白丁入鸿儒,go lang复合容器类型的声...

 2 years ago
source link: https://v3u.cn/a_id_227
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.
neoserver,ios ssh client

兼容并蓄广纳百川,Go lang1.18入门精炼教程,由白丁入鸿儒,go lang复合容器类型的声明和使用EP04

兼容并蓄广纳百川,Go lang1.18入门精炼教程,由白丁入鸿儒,go lang复合容器类型的声明和使用EP04

    书接上回,容器数据类型是指一种数据结构、或者抽象数据类型,其实例为其他类的对象。 或者说得更具体一点,它是以一种遵循特定访问规则的方法来存储对象。 容器的大小取决于其包含的基础数据对象(或数据元素)的个数。Go lang中常用的容器数据有数组、切片和集合。

    数组是一个由长度固定的特定类型元素组成的序列,一个数组可以由零个或多个元素组成,它是一种线性的数据结构,同时内部元素的内存地址是相连的,没错,Python中的元祖(tuple)和Go lang中的数组就是一类东西,由于定长的特性,所以在系统资源占用层面具备一定的优势。

    我们可以使用 [n]Type 来声明一个数组。其中 n 表示数组中元素的数量, Type 表示每个元素的类型:

package main

import "fmt"

func main() {
// 声明时没有指定数组元素的值, 默认为零值
var arr [5]int
fmt.Println(arr)

arr[0] = 1
arr[1] = 2
arr[2] = 3
fmt.Println(arr)
}

    程序返回:

[0 0 0 0 0]
[1 2 3 0 0]

    除此之外,也可通过海象操作符等方式进行声明:

package main

import "fmt"

func main() {
// 直接在声明时对数组进行初始化
var arr1 = [5]int{15, 20, 25, 30, 35}
fmt.Println(arr1)

// 使用短声明
arr2 := [5]int{15, 20, 25, 30, 35}
fmt.Println(arr2)

// 部分初始化, 未初始化的为零值
arr3 := [5]int{15, 20} // [15 20 0 0 0]
fmt.Println(arr3)

// 可以通过指定索引,方便地对数组某几个元素赋值
arr4 := [5]int{1: 100, 4: 200}
fmt.Println(arr4) // [0 100 0 0 200]

// 直接使用 ... 让编译器为我们计算该数组的长度
arr5 := [...]int{15, 20, 25, 30, 35, 40}
fmt.Println(arr5)

// 定义多维数组
arr := [3][2]string{
{"1", "10"},
{"2", "3"},
{"3", "4"}}
fmt.Println(arr) // [[15 20] [25 22] [25 22]]

//数组取值

fmt.Println(arr[0][0])

}

    同时数组支持嵌套,也就是多维数组结构,最后通过数组的下标进行取值操作。

    通过将数组作为参数传递给len函数,可以获得数组的长度:

package main

import "fmt"

func main() {
a := [...]float64{67.7, 89.8, 21, 78}
fmt.Println("length of a is",len(a))

}

    数组是值类型 ,而不是引用类型。这意味着当它们被分配给一个新变量时,将把原始数组的副本分配给新变量。如果对新变量进行了更改,则不会影响原对象,这有点像Python中的可变以及不可变数据类型,原理都是一样的:

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

    和Python中元祖不同的是,数组元素的值可以改变,但是元素成员不能增减,最后数组可以使用for关键字或者range关键字进行遍历操作:

package main

import "fmt"

func test() {

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)

}

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

test()
}

    程序返回:

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

    使用内置的 len 方法将返回数组中元素的个数,即数组的长度。

func arrLength() {
arr := [...]string{"Go语言极简一本通", "Go语言微服务架构核心22讲", "从0到Go语言微服务架构师"}
fmt.Println("数组的长度是:", len(arr)) //数组的长度是: 3
}

    可以这么理解,go lang的数组是元素值可以改变但长度不变的元祖(tuple)。

    切片(Slice)

    切片是对数组的一个连续片段的引用,所以切片是一个引用类型。切片本身不拥有任何数据,它们只是对现有数组的引用,每个切片值都会将数组作为其底层的数据结构。slice 的语法和数组很像,只是没有固定长度而已。

    使用 []Type 可以创建一个带有 Type 类型元素的切片:

// 声明整型切片
var numList []int

// 声明一个空切片
var numListEmpty = []int{}

    如果愿意,也可以使用 make 方法构造一个切片,格式为 make([]Type, size, cap) :

numList := make([]int, 3, 5)

    切片之所以称之为切片,是因为我们可以对数组进行裁切,从而创建一个切片:

package main

import "fmt"

func main() {

arr := [5]int{1, 2, 3, 4, 5}
var s1 = arr[1:4]
fmt.Println(arr)
fmt.Println(s1)

}

    程序返回:

[1 2 3 4 5]
[2 3 4]

    一个切片由三个部分构成:指针 、长度 和 容量 。指针指向第一个切片元素对应的底层数组元素的地址,要注意的是切片的第一个元素并不一定就是数组的第一个元素。长度对应切片中元素的数目;长度不能超过容量,容量一般是从切片的开始位置到底层数据的结尾位置。简单地理解,容量就是从创建切片索引开始的底层数组中的元素个数,而长度是切片中的元素个数:

package main

import "fmt"

func main() {

s := make([]string, 3, 5)
fmt.Println(len(s)) // 3
fmt.Println(cap(s)) // 5

}

    由上可知,切片内置的 len 方法和 cap 方法分别返回切片 的长度和容量。

    如果切片操作超出上限将导致一个 panic 异常,有点像Python中列表的下标越界异常:

s := make([]int, 3, 5)
fmt.Println(s[10]) //panic: runtime error: index out of range [10] with length 3

    切片是引用类型,所以声明之后不对它进行赋值的话,它的默认值是 nil

var numList []int
fmt.Println(numList == nil) // true

    切片之间不能比较,因此我们不能使用恒等(== )操作符来判断两个切片是否含有全部相等元素。特别注意,如果你需要测试一个切片是否是空的,使用 len(s) == 0 来判断,而不应该用 s == nil 来判断:

    切片自己不拥有任何数据元素。它只是底层数组的一种表示。对切片所做的任何修改都会反映在底层数组中:

package main

import "fmt"

func main() {

var arr = [...]int{1, 2, 3}
s := arr[:]
fmt.Println(arr)
fmt.Println(s)

s[0] = 4
fmt.Println(arr)
fmt.Println(s)

}

    程序返回:

[1 2 3]
[1 2 3]
[4 2 3]
[4 2 3]

    上面的 arr[:] 没有填入起始值和结束值,默认就是 0 和 len(arr) 。

    使用 append方法可以将新元素追加到切片上,这和Python中的列表方法如出一辙,append方法的定义是 func append(slice []Type, elems ...Type) []Type 。其中 elems ...Type 在函数定义中表示该函数接受参数 elems 的个数是可变的。这些类型的函数被称为可变函数:

package main

import "fmt"

func main() {

s := []int{1}
fmt.Println(s)
fmt.Println(cap(s))

s = append(s, 2)
fmt.Println(s)
fmt.Println(cap(s))

s = append(s, 3, 4)
fmt.Println(s)
fmt.Println(cap(s))

s = append(s, []int{5, 6}...)
fmt.Println(s)
fmt.Println(cap(s))

}

    程序返回:

[1]
1
[1 2]
2
[1 2 3 4]
4
[1 2 3 4 5 6]
8

    当新的元素被添加到切片时,如果容量不足,会创建一个新的数组。现有数组的元素被复制到这个新数组中,并返回新的引用。现在新切片的容量是旧切片的两倍。

    切片也可以有多个维度,也就是嵌套的形式:

package main

import "fmt"

func main() {

numList := [][]string{
{"1", "1"},
{"2", "2"},
{"3", "3"},
}
fmt.Println(numList)

}

    集合(Map)

    在 Go lang中,集合是散列表(哈希表)的引用。它是一个拥有键值对元素的无序集合,在这个集合中,键是唯一的,可以通过键来获取、更新或移除操作。无论这个散列表有多大,这些操作基本上是通过常量时间完成的。所有可比较的类型,如整型 ,字符串等,都可以作为 key 。

    使用 make方法传入键和值的类型,可以创建集合 。具体语法为 make(map[KeyType]ValueType) 。

package main

import "fmt"

func main() {

// 创建一个键类型为 string 值类型为 int 名为 scores 的 map
scores := make(map[string]int)
steps := make(map[string]string)

fmt.Println(scores)
fmt.Println(steps)

}

    也可以用集合字面值的语法创建集合,同时还可以指定一些最初的 key/value :

package main

import "fmt"

func main() {

var steps2 map[string]string = map[string]string{
"1": "1",
"2": "2",
"3": "3",
}
fmt.Println(steps2)

}

    亦或者使用海象操作符:

package main

import "fmt"

func main() {

steps3 := map[string]string{
"1": "1",
"2": "2",
"3": "3",
}
fmt.Println(steps3)

}

    动态地添加新元素:

// 可以使用 `map[key] = value` 向 map 添加元素。
steps3["4"] = "4"

    修改元素:

// 若 key 已存在,使用 map[key] = value 可以直接更新对应 key 的 value 值。
steps3["4"] = "第四步"

// 直接使用 map[key] 即可获取对应 key 的 value 值,如果 key不存在,会返回其 value 类型的零值。
fmt.Println(steps3["4"] )

    删除某个key:

//使用 delete(map, key)可以删除 map 中的对应 key 键值对,如果 key 不存在,delete 函数会静默处理,不会报错。
delete(steps3, "4")

    判断 key 是否存在:

// 如果我们想知道 map 中的某个 key 是否存在,可以使用下面的语法:value, ok := map[key]
v3, ok := steps3["3"]
fmt.Println(ok)
fmt.Println(v3)

v4, ok := steps3["4"]
fmt.Println(ok)
fmt.Println(v4)

    这个逻辑说明集合的下标读取可以返回两个值,第一个值为当前 key 的 value 值,第二个值表示对应的 key 是否存在,若存在 ok 为 true ,若不存在,则 ok 为 false 。

    集合也可以进行遍历操作:

// 遍历 map 中所有的元素需要用 for range 循环。
for key, value := range steps3 {
fmt.Printf("key: %s, value: %d\n", key, value)
}

    同样使用len方法来获取集合的长度:

// 使用 len 函数可以获取 map 长度
func createMap() {
//...
fmt.Println(len(steps3)) // 4
}

    当集合被赋值为一个新变量的时候,它们指向同一个内部数据结构。因此,改变其中一个变量,就会影响到另一变量。所以,集合是引用数据类型:

package main

import "fmt"

func main() {

steps4 := map[string]string{
"1": "1",
"2": "2",
"3": "3",
}
fmt.Println("steps4: ", steps4)

newSteps4 := steps4
newSteps4["1"] = "1.1-222"
newSteps4["2"] = "2.2-222"
newSteps4["3"] = "3.3-222"
fmt.Println("steps4: ", steps4)

fmt.Println("newSteps4: ", newSteps4)

}

    程序返回:

steps4:  map[1:1 2:2 3:3]
steps4: map[1:1.1-222 2:2.2-222 3:3.3-222]
newSteps4: map[1:1.1-222 2:2.2-222 3:3.3-222]

    所以,当需要保留原对象数据做其他操作时,最好不要使用赋值,而是提前声明新容器。同理,当集合作为参数传递到方法内时,方法对其做了修改操作,也会影响原集合对象。

     在业务代码的编写上,我们经常会查询来自数据库的源数据,再把它们插入到对应的复合数据类型结构中去,再进行下一步的业务聚合、裁剪、封装、处理,然后返回到前端,进行渲染操作。大体上,我们会选择数组、切片还有集合,一般情况下最外部是切片或者是数组,然后内嵌集合的数据集,集合内key作为字段,value作为字段的值。在操作上,需要注意值类型(数组)和引用类型(切片、集合)的区别:值类型的特点是:变量直接存储值,内存通常在栈中分配;引用类型的特点是:变量存储的是一个地址,这个地址对应的空间里才是真正存储的值,内存通常在堆中分配,说白了就是值类型赋值后修改不会影响原对象,而引用类型反之,有点像Python中的可变和不可变数据类型,由此可见,天下武功,同归殊途,万法归宗,万变不离其宗。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK