Data Type

复合数据类型

本章节开始,我们来一起认识一下 Go 中的复合数据结构,步入正题本节目标:

  • 复合数据结构(数组、切片、哈希表)的定义、遍历以及应用

数组(array)

数组:一组连续的内存空间,来存储相同类型的数据,索引从 0 开始依次类推。

明确一点 Go 没有 PHP 中的关联数组,大白话就是数组的索引只能是数字,不能是字符串,值可以是数值、字符串或者自定义类型。

定义数组

比如:声明一个长度为 3 ,存储值为 int 类型数组,不赋值

var a [3]int
fmt.Println(a)  // [0 0 0]

当然我们再定义时,也可以一并赋值

var b = [3]int{1, 2, 3}
fmt.Println(b) // [1 2 3]

再比如,声明长度为 3 的 float32 类型数组,并只为前两个元素赋值为:1.1、2.1,第三个元素不赋值

var c = [3]float32{1.1, 2.1}
fmt.Println(c) // [1.1 2.1 0]

如果你觉得每次指定长度太麻烦了,还可以使用 ... 符号简写,比如定义如下存储值为 a 、b、c 字符串数组:

s := [...]string{"a", "b", "c"}
fmt.Println(s) // [a b c]

遍历数组

我们已知 Go 中,变量语句只有 for,那还等什么呢?看招吧:

招式一:使用 len 函数获取其数组长度,遍历

for i := 0; i < len(s); i++ {
    fmt.Println(s[i])
}
// output:
// a
// b
// c

招式二:使用 range 关键词,遍历完后自动终止遍历

s := [...]string{"a", "b", "c"}
for k, v := range s {
    fmt.Println(k, v)
}
// output:
// 0 a
// 1 b
// 2 c

如果不关心索引值,可以使用 _ 符号屏蔽,如下:

for _, v := range s {
    fmt.Println(v)
}
// output:
// a
// b
// c

再实际开发中,数组往往使用较少,更多的使用下面的切片。

数组的比较

相同类型的且相同长度的数组是可以进行比较的,如下:

a := [2]int{1, 2}
b := [...]int{1, 2}
c := [2]int{1, 3}
fmt.Println(a == b) // true
fmt.Println(a == c) // false
fmt.Println(b == c) // false

不同类型或不同长度的数组,不能比较,如下:

d := [2]string{"A", "B"}
fmt.Println(a == d) // invalid operation: a == d (mismatched types [2]int and [2]string)

e := [3]int{1, 2}
fmt.Println(a == e) // invalid operation: a == e (mismatched types [2]int and [3]int)

切片(slice)

Go 的数组的长度是一个定长,一旦确认不能改变,这在实际开发中使用起来很蹩脚,Go 中提供了一种灵活动态扩容的内置类型,那就是切片。

切片可变长度的存储相同类型的值,与数组相比,切片长度是不固定的,可以动态追加元素,使其容量增大。

切片的定义

知晓了数组后,切片也很容易搞懂了,切片不需要说明长度,[]表示是切片类型

我们定义一个一周的字符串切片,如下:

方式一:声明一个值为 nil 的 slice

var s1 []int
if s1 == nil {
    fmt.Println("s1 is empty slice")
}
fmt.Printf("%T %#v\n", s1, s1)
// output:
// s1 is empty slice
// []int []int(nil)

声明并赋值为空 string 的 slice 写法:

s2 := []string{}
fmt.Printf("%T %#v\n", s2, s2) // []string []string{}

两者不同的是,s1 是值为空(nil)切片,s2 是值为空串(非nil)的切片。

方式二:除了这种方式定义切片类型,我们还可以使用 make 关键字

如声明一个长度(len)为2,容量(cap)为 4 的字符串切片:

s3 := make([]string, 2, 4)
fmt.Printf("%T %#v\n", s3, s3) // []string []string{"", ""}
fmt.Printf("len=%d cap=%d \n", len(s3), cap(s3)) // len=2 cap=4

注解:

len() 源码中注解:Slice, or map: the number of elements in v; if v is nil, len(v) is zero. cap() 源码中注解:Slice: the maximum length the slice can reach when resliced;

  • len() 意思是说:slice 或 map 类型,返回其元素的数量,如果值是 nil 则长度是 0
  • cap() 意思是说:申请 slice 达到的最大长度

这么说你可能还是不能很好的理解,木关系,往下看一个示例带你搞清楚 slice 申请容量(cap)大小的原理,看不懂你打我😼

append 函数

在研究 cap 容量大小前,我们先来看俩函数 append() 和 copy()

第一个:append() 往一个切片中追加一个或多个元素,返回追加后的新切片值

// 空值切片 w
var w []string
fmt.Printf("len=%d cap=%d v=%v\n", len(w), cap(w), w)

// 追加空串到切片 w
w1 := append(w, "")
fmt.Printf("len=%d cap=%d v=%v\n", len(w1), cap(w1), w1)

// 追加字符串 A B 到切片 w1
w2 := append(w1, "A", "B")
fmt.Printf("len=%d cap=%d v=%v\n", len(w2), cap(w2), w2)

// output:
// len=0 cap=0 v=[]
// len=1 cap=1 v=[]
// len=3 cap=3 v=[ A B]

第二个:copy(det,src) 将 src 拷贝一份到 det,示例:

// 声明一个 nw 字符串切片,长度等于 w2 的长度
nw := make([]string, len(w2))
copy(nw, w2)
fmt.Printf("len=%d cap=%d v=%v\n", len(nw), cap(nw), nw)
// output: len=3 cap=3 v=[ A B]

扩容原理

明白了 append() 函数,我们接下来继续弄明白切片容量(cap)是如何申请大小的,看个示例:

var x, y []int
for i := 0; i < 5; i++ {
    y = append(x, i)
    fmt.Printf("%d cap=%d\t%v\n", i, cap(y), y)
    x = y
}

结果如下:

0 cap=1	[0]
1 cap=2	[0 1]
2 cap=4	[0 1 2]
3 cap=4	[0 1 2 3]
4 cap=8	[0 1 2 3 4]

由上结果可得出结论:切片 x 装不下时,每次申请容量(cap)大小会是原容量大小的 2倍。

切片比较

切片唯一合法的比较是和 nil 比较,如:

var s1 []int
if s1 == nil {
    fmt.Println("s is nil")
}
// output: s is nil

切片理解

切片可以理解为是 array 的视图(view),我们来看个栗子:

定义一周的 string 切片变量,week

week := []string{"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}
fmt.Printf("%v\n", week)
// output: [Sun Mon Tue Wed Thu Fri Sat]

从 week 切片中,切出工作日切片,变量为 workday

workday := week[1:6]
fmt.Println(workday)
// output: [Mon Tue Wed Thu Fri]

修改 workday 切片的第一个元素值为字符串 “Monday”

workday[0] = "Monday"
fmt.Println(workday)

见证奇迹的时刻到了,请问切片变量 week,是 “Mon” 还是 “Monday”? 请自行思考一分钟。

假装一分钟到了
fmt.Println(week)
// output: [Sun Monday Tue Wed Thu Fri Sat]

有没有被结果惊呆?其实也很好理解,切片的底层就是数组,对数组做了变长包装,当一个切片的元素超出了容量(cap)大小后,便重新创建一个新的切片是原容量的 2 倍,然后将原数据拷贝过去。

每一次对 slice 做一次切割(如:week[1:6]),其实都是在原数据上的一次 view,当修改了切割出来新切片变量,原始数据也会被修改。

哈希表(map)

Map 是一种无序的键值对集合,特点是通过键 key 快速检索数据,理论上O(1)时间复杂度内检索、更新或删除对应的值。

Go 中 map 就是一个哈希表(Hash)的引用

定义

方式一:使用 make 函数,创建 map

ages := make(map[string]int)
ages["braem"] = 29
ages["bing"] = 18
fmt.Printf("%v\n", ages) // map[bing:18 braem:29]

方式二:也可以直接创建,使用指定 key/value

ages := map[string]int{"braem": 29, "bing": 18}
fmt.Printf("%v", ages) // map[bing:18 braem:29]

两种写法上不同,但是表达的意思是一样的。

操作

可以通过 key 获取下标对应的 value,写法:

fmt.Println(ages["bing"]) // 18

即使,key 不存在,程序也不会报错,如下:

fmt.Println(ages["bob"]) // 0

但是有时候我们需要知道,某个元素是否真的存在 map 中(而不是取不到时得到一个零值),可以这么做:

age, ok := ages["bob"]
if !ok {
    fmt.Println("is not a key in map")
}

最佳实践,常见的写法是将其两个语句结合起来:

if age, ok := ages["bob"]; !ok {
    fmt.Println("is not key bob")
}

还可以使用内置函数 delete 删除元素,如下:

delete(ages, "bing")
fmt.Println(ages) // map[braem:29]

遍历

map 的遍历是随机打乱的(作者故意的),避免开发陷阱,示例如下:

rank := map[int]string{1: "A", 2: "B", 3: "C", 4: "D"}
for key, value := range rank {
    fmt.Println(key, value)
}
// output:
// 3 C
// 4 D
// 1 A
// 2 B

如果需要排序,可以借助 sort 包中的,排序函数

查看排序代码
  rank := map[int]string{1: "A", 2: "B", 3: "C", 4: "D"}
  var nums []int
  // 取得 map 中数值下标 key,追加到 nums 切片
  for num := range rank {
      nums = append(nums, num)
  }
  // 排序 nums 切片
  sort.Ints(nums)
  // 遍历有序 nums,通过其 key 获取 map 元素值
  for _, num := range nums {
      fmt.Printf("%d\t%s\n", num, rank[num])
  }

比较

map 和 slice 一样,map 与 map 不能比较,唯一例外是只能和 nil 进行比较,

num1 := make(map[int]int)
var num2 map[string]int
num3 := map[string]int{}

//fmt.Println(num1 == num2) // invalid operation: num1 == num2 (map can only be compared to nil)
fmt.Println(num1 == nil) // false
fmt.Println(num2 == nil) // true
fmt.Println(num3 == nil) // false

小结

  • 本章节学习了 Go 中复合数据类型,数组(array)、切片(slice)、哈希表(map)的使用
  • 针对这三种不同的数据类型,结合你的日常开发,能否想到其应用场景,欢迎评论留言