上篇文章中详细介绍了 Go
的基础语言,指出了 Go
和其他主流的编程语言的差异性,比较侧重于语法细节,相信只要稍加记忆就能轻松从已有的编程语言切换到 Go
语言的编程习惯中,尽管这种切换可能并不是特别顺畅,但多加练习尤其是多多试错,总是可以慢慢感受 Go
语言之美!
在学习 Go
的内建容器前,同样的,我们先简单回顾一下 Go
的基本语言,温度而知新可以为师矣!
上节知识回顾
内建类型种类
bool
(u)int
,(u)int8
,(u)int16
,(u)int32
,(u)int64
,uintptr
byte(uint8)
,rune(int32)
,string
float32
,float64
,complex64
,complex128
内建类型特点
- 类型转换只有显示转换,不存在任何形式的隐式类型转换
- 虽然提供指针类型,但指针本身不能进行任何形式的计算.
- 变量声明后有默认初始化零值,变量零值视具体类型而定
基本运算符
- 算术运算符没有
++i
和--i
- 比较运算符
==
可以比较数组是否相等
- 位运算符新增按位清零运算符
&^
流程控制语句
if
条件表达式不需要小括号并支持变量赋值操作
if
条件表达式内定义的变量作用域仅限于当前语句块
switch
语句可以没有break
,除非使用了fallthrough
switch
条件表达式不限制为常数或整数
switch
条件表达式可以省略,分支逻辑转向case
语言实现.
- 省略
switch
条件表达式后,每个case
条件可以有多个条件,用逗号分隔.
for
循环的条件表达式也不需要小括号,且没有其他形式的循环.
for
循环的初始条件,终止条件和自增表达式都可以省略或者同时省略
函数和参数传递
- 函数声明按照函数名,入参,出参顺序定义,并支持多返回值
- 函数有多个返回值时可以给返回值命名,但对调用者而言没有差别
- 函数的入参没有必填参数,可选参数等复杂概念,只支持可变参数列表
- 函数参数传递只有值传递,没有引用传递,即全部需要重新拷贝变量
内建容器有哪些
复习了 Go
语言的基础语法后,开始继续学习变量类型的承载者也就是容器的相关知识.
承载一类变量最基础的底层容器就是数组了,大多数高级的容器底层都可以依靠数组进行封装,所以先来了解一下 Go
的数组有何不同?
数组和切片
- 数组的声明和初始化
数组的明显特点就是一组特定长度的连续存储空间,声明数组时必须指定数组的长度,声明的同时可以进行初始化,当然不指定数组长度时也可以使用 ...
语法让编译器帮我们确定数组的长度.
func TestArray(t *testing.T) {
var arr1 [3]int
arr2 := [5]int{1, 2, 3, 4, 5}
arr3 := [...]int{2, 4, 6, 8, 10}
// [0 0 0] [1 2 3 4 5] [2 4 6 8 10]
t.Log(arr1, arr2, arr3)
var grid [3][4]int
// [[0 0 0 0] [0 0 0 0] [0 0 0 0]]
t.Log(grid)
}
- 数组的遍历和元素访问
最常见的 for
循环进行遍历就是根据数组的索引进行访问,range arr
方式提供了简化遍历的便捷方法.
func TestArrayTraverse(t *testing.T) {
arr := [...]int{2, 4, 6, 8, 10}
for i := 0; i < len(arr); i++ {
t.Log(arr[i])
}
for i := range arr {
t.Log(arr[i])
}
for i, v := range arr {
t.Log(i, v)
}
for _, v := range arr {
t.Log(v)
}
}
- 数组是值类型可以进行比较
数组是值类型,这一点和其他主流的编程语言有所不同,因此相同纬度且相同元素个数的数组可以比较,关于这方面的内容前面也已经强调过,这里再次简单回顾一下.
func printArray(arr [5]int) {
arr[0] = 666
for i, v := range arr {
fmt.Println(i, v)
}
}
func TestPrintArray(t *testing.T) {
var arr1 [3]int
arr2 := [5]int{1, 2, 3, 4, 5}
arr3 := [...]int{2, 4, 6, 8, 10}
// [0 0 0] [1 2 3 4 5] [2 4 6 8 10]
t.Log(arr1, arr2, arr3)
// cannot use arr1 (type [3]int) as type [5]int in argument to printArray
//printArray(arr1)
fmt.Println("printArray(arr2)")
printArray(arr2)
fmt.Println("printArray(arr3)")
printArray(arr3)
// [1 2 3 4 5] [2 4 6 8 10]
t.Log(arr2, arr3)
}
想要在 printArrayByPointer
函数内部修改参数数组,可以通过数组指针的方式,如果有不熟悉的地方,可以翻看上一篇文章回顾查看.
func printArrayByPointer(arr *[5]int) {
arr[0] = 666
for i, v := range arr {
fmt.Println(i, v)
}
}
func TestPrintArrayByPointer(t *testing.T) {
var arr1 [3]int
arr2 := [5]int{1, 2, 3, 4, 5}
arr3 := [...]int{2, 4, 6, 8, 10}
// [0 0 0] [1 2 3 4 5] [2 4 6 8 10]
t.Log(arr1, arr2, arr3)
fmt.Println("printArrayByPointer(arr2)")
printArrayByPointer(&arr2)
fmt.Println("printArrayByPointer(arr3)")
printArrayByPointer(&arr3)
// [666 2 3 4 5] [666 4 6 8 10]
t.Log(arr2, arr3)
}
- 切片的声明和初始化
切片和数组非常类似,创建数组时如果没有指定数组的长度,那么最终创建的其实是切片并不是数组.
func TestSliceInit(t *testing.T) {
var s1 [5]int
// [0 0 0 0 0]
t.Log(s1)
var s2 []int
// []
t.Log(s2,len(s2))
}
同理,数组可以声明并初始化,切片也可以,并且语法也很类似,稍不注意还以为是数组呢!
func TestSliceInitValue(t *testing.T) {
var s1 = [5]int{1, 3, 5, 7, 9}
// [1 3 5 7 9]
t.Log(s1)
var s2 = []int{1, 3, 5, 7, 9}
// [1 3 5 7 9]
t.Log(s2)
}
数组和切片如此相像,让人不得不怀疑两者之间有什么见不得人的勾当?其实可以从数组中得到切片,下面举例说明:
func TestSliceFromArray(t *testing.T) {
arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// arr = [0 1 2 3 4 5 6 7 8 9]
t.Log("arr = ", arr)
// arr[2:6] = [2 3 4 5]
t.Log("arr[2:6] = ", arr[2:6])
// arr[:6] = [0 1 2 3 4 5]
t.Log("arr[:6] = ", arr[:6])
// arr[2:] = [2 3 4 5 6 7 8 9]
t.Log("arr[2:] = ", arr[2:])
// arr[:] = [0 1 2 3 4 5 6 7 8 9]
t.Log("arr[:] = ", arr[:])
}
和其他主流的编程语言一样,[start:end]
是一个左闭右开区间,切片的含义也非常明确:
忽略起始索引 start
时,arr[:end]
表示原数组从头开始直到终止索引 end
的前一位;
忽略终止索引 end
时,arr[ start:]
表示原数组从起始索引 start
开始直到最后一位;
既忽略起始索引又忽略终止索引的情况,虽然不常见但是含义上将应该就是原数组,但是记得类型是切片不是数组哟!
目前为止,我们知道切片和数组很相似,切片相对于数组只是没有大小,那么切片和数组的操作上是否一样呢?
func updateSlice(s []int) {
s[0] = 666
}
func TestUpdateSlice(t *testing.T) {
arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// arr = [0 1 2 3 4 5 6 7 8 9]
t.Log("arr = ", arr)
s1 := arr[2:6]
// s1 = [2 3 4 5]
t.Log("s1 = ", s1)
s2 := arr[:6]
// s2 = [0 1 2 3 4 5]
t.Log("s2 = ", s2)
updateSlice(s1)
// s1 = [666 3 4 5]
t.Log("s1 = ", s1)
// arr = [0 1 666 3 4 5 6 7 8 9]
t.Log("arr = ", arr)
updateSlice(s2)
// s2 = [666 1 666 3 4 5]
t.Log("s2 = ", s2)
// arr = [666 1 666 3 4 5 6 7 8 9]
t.Log("arr = ", arr)
}
切片和数组在参数传递的表现不同,具体表现为数组进行参数传递时无法修改数组,想要想改数组只有传递数组指针才行,而切片却实现了数组的改变!
由于参数传递只有值传递一种方式,因此推测切片内部肯定存在指针,参数传递时传递的是指针,所以函数内部的修改才能影响到到函数外部的变量.
正是由于切片这种内部实现,需要特性也好表现形式也罢才使得切换和数组有着千丝万缕的联系,其实这种数据结果就是对静态数组的扩展,本质上是一种动态数组而已,只不过 Go
语言叫做切片!
切片是动态数组,上述问题就很容易解释了,参数传递时传递的是内部指针,因而虽然是值传递拷贝了指针,但是指针指向的真正元素毕竟是一样的,所以切片可以修改外部参数的值.
数组可以在一定程度上进行比较,切片是动态数组,能不能进行比较呢?让接下来的测试方法来验证你的猜想吧!
- 切片的添加和删除
数组是静态结构,数组的大小不能扩容或缩容,这种数据结构并不能满足元素个数不确定场景,因而才出现动态数组这种切片,接下来重点看下切片怎么添加或删除元素.
func printSlice(s []int) {
fmt.Printf("s = %v, len(s) = %d, cap(s) = %d\n", s, len(s), cap(s))
}
func TestSliceAutoLonger(t *testing.T) {
var s []int
// []
t.Log(s)
for i := 0; i < 10; i++ {
s = append(s, i)
printSlice(s)
}
// [0 1 2 3 ...,98,99]
t.Log(s)
for i := 0; i < 10; i++ {
s = s[1:]
printSlice(s)
}
// [0 1 2 3 ...,98,99]
t.Log(s)
}
s = append(s, i)
向切片中添加元素并返回新切片,由于切片是动态数组,当切片内部的数组长度不够时会自动扩容以容纳新数组,扩容前后的内部数组会进行元素拷贝过程,所以 append
会返回新的地址,扩容后的地址并不是原来地址,所以需要用变量接收添加后的切片.
当不断进行切片重新截取时 s[1:]
,切片存储的元素开始缩减,个数递减,容量也递减.
其实除了基于数组创建切片和直接创建切片的方式外,还存在第三种创建切片的方式,也是使用比较多的方式,那就是 make
函数.
func TestMakeSlice(t *testing.T) {
s1 := make([]int,10)
// s1 = [0 0 0 0 0 0 0 0 0 0], len(s1) = 10, cap(s1) = 10
t.Logf("s1 = %v, len(s1) = %d, cap(s1) = %d", s1, len(s1), cap(s1))
s2 := make([]int, 10, 32)
// s2 = [0 0 0 0 0 0 0 0 0 0], len(s2) = 10, cap(s2) = 32
t.Logf("s2 = %v, len(s2) = %d, cap(s2) = %d", s2, len(s2), cap(s2))
}
通过 make
方式可以设置初始化长度和容量,这是字面量创建切片所不具备的能力,并且这种方式创建的切片还支持批量拷贝功能!
func TestCopySlice(t *testing.T) {
var s1 = []int{1, 3, 5, 7, 9}
var s2 = make([]int, 10, 32)
copy(s2, s1)
// s2 = [1 3 5 7 9 0 0 0 0 0], len(s2) = 10, cap(s2) = 32
t.Logf("s2 = %v, len(s2) = %d, cap(s2) = %d", s2, len(s2), cap(s2))
var s3 []int
copy(s3, s1)
// s3 = [], len(s3) = 0, cap(s3) = 0
t.Logf("s3 = %v, len(s3) = %d, cap(s3) = %d", s3, len(s3), cap(s3))
}
切片的底层结构是动态数组,如果切片是基于数组截取而成,那么此时的切片从效果上来看,切片就是原数组的一个视图,对切片的任何操作都会反映到原数组上,这也是很好理解的.
那如果对切片再次切片呢,或者说切片会不会越界,其实都比较简单了,还是稍微演示一下,重点就是动态数组的底层结构.
func TestSliceOutOfBound(t *testing.T) {
arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
s1 := arr[2:6]
// s1 = [2 3 4 5], len(s1) = 4, cap(s1) = 6
t.Logf("s1 = %v, len(s1) = %d, cap(s1) = %d", s1, len(s1), cap(s1))
s2 := s1[3:5]
// s2 = [5 6], len(s2) = 2, cap(s2) = 3
t.Logf("s2 = %v, len(s2) = %d, cap(s2) = %d", s2, len(s2), cap(s2))
}
我们知道切片 slice
的内部数据结构是基于动态数组,存在三个重要的变量,分别是指针 ptr
,个数 len
和容量 cap
,理解了这三个变量如何实现动态数组就不会掉进切片的坑了!
个数 len
是通过下标访问时的有效范围,超过 len
后会报越界错误,而容量 cap
是往后能看到的最大范围,动态数组的本质也是控制这两个变量实现有效数组的访问.
集合 map
集合是一种键值对组成的数据结构,其他的主流编程语言也有类似概念,相比之下,Go
语言的 map
能装载的数据类型更加多样化.
- 字面量创建
map
换行需保留逗号,
func TestMap(t *testing.T) {
m1 := map[string]string{
"author": "snowdreams1006",
"website": "snowdreams1006",
"language": "golang",
}
// map[name:snowdreams1006 site:https://snowdreams1006.github.io]
t.Log(m1)
}
make
创建的map
和字面量创建的map
默认初始化零值不同
func TestMapByMake(t *testing.T) {
// empty map
m1 := make(map[string]int)
// map[] false
t.Log(m1, m1 == nil)
// nil
var m2 map[string]int
// map[] true
t.Log(m2, m2 == nil)
}
range
遍历map
是无序的
func TestMapTraverse(t *testing.T) {
m := map[string]string{
"name": "snowdreams1006",
"site": "https://snowdreams1006.github.io",
}
// map[name:snowdreams1006 site:https://snowdreams1006.github.io]
t.Log(m)
for k, v := range m {
t.Log(k, v)
}
t.Log()
for k := range m {
t.Log(k)
}
t.Log()
for _, v := range m {
t.Log(v)
}
}
- 获取元素时需判断元素是否存在
func TestMapGetItem(t *testing.T) {
m := map[string]string{
"name": "snowdreams1006",
"site": "https://snowdreams1006.github.io",
}
// snowdreams1006
t.Log(m["name"])
// zero value is empty string
t.Log(m["author"])
// https://snowdreams1006.github.io
if site, ok := m["site"]; ok {
t.Log(site)
} else {
t.Log("key does not exist ")
}
}
- 删除键值对时用
delete
函数
func TestMapDeleteItem(t *testing.T) {
m := map[string]string{
"name": "snowdreams1006",
"site": "https://snowdreams1006.github.io",
}
// map[name:snowdreams1006 site:https://snowdreams1006.github.io]
t.Log(m)
delete(m, "name")
// map[site:https://snowdreams1006.github.io]
t.Log(m)
delete(m, "id")
// map[site:https://snowdreams1006.github.io]
t.Log(m)
}
- 除
slice
,map
,func
外,其余类型均可键
value
可以承载函数func
类型
func TestMapWithFunValue(t *testing.T) {
m := map[int]func(op int) int{}
m[1] = func(op int) int {
return op
}
m[2] = func(op int) int {
return op * op
}
m[3] = func(op int) int {
return op * op * op
}
// 1 4 27
t.Log(m[1](1), m[2](2), m[3](3))
}
没有 set
Go
的默认类型竟然没有 set
这种数据结构,这在主流的编程语言中算是特别的存在了!
正如 Go
的循环仅支持 for
循环一样,没有 while
循环一样可以玩出 while
循环的效果,靠的就是增强的 for
能力.
所以,即使没有 set
类型,基于现有的数据结构一样能实现 set
效果,当然直接用 map
就可以封装成 set
.
func TestMapForSet(t *testing.T) {
mySet := map[int]bool{}
mySet[1] = true
n := 3
if mySet[n] {
t.Log("update", mySet[n])
} else {
t.Log("add", mySet[n])
}
delete(mySet, 1)
}
知识点总结梳理
Go
语言是十分简洁的,不论是基础语法还是这一节的内建容器都很好的体现了这一点.
数组作为各个编程语言的基础数据结构,Go
语言和其他主流的编程语言相比没有什么不同,都是一片连续的存储空间,不同之处是数组是值类型,所以也是可以进行比较的.
这并不是新鲜知识,毕竟上一节内容已经详细阐述过该内容,这一节的重点是数组的衍生版切片 slice
.
因为数组本身是特定长度的连续空间,因为是不可变的,其他主流的编程语言中有相应的解决方案,其中就有不少数据结构的底层是基于数组实现的,Go
语言的 slice
也是如此,因此个人心底里更愿意称其为动态数组!
切片 slice
的设计思路非常简单,内部包括三个重要变量,包括数组指针 ptr
,可访问元素长度 len
以及已分配容量 cap
.
当新元素不断添加进切片时,总会达到已最大分配容量,此时切片就会自动扩容,反之则会缩容,从而实现了动态控制的能力!
- 指定元素个数的是数组,未指定个数的是切片
func TestArrayAndSlice(t *testing.T) {
// array
var arr1 [3]int
// slice
var arr2 []int
// [0 0 0] []
t.Log(arr1,arr2)
}
- 基于数组创建的切片是原始数组的视图
func TestArrayAndSliceByUpdate(t *testing.T) {
arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// arr = [0 1 2 3 4 5 6 7 8 9]
t.Log("arr = ", arr)
s := arr[2:6]
// before update s = [2 3 4 5], arr = [0 1 2 3 4 5 6 7 8 9]
t.Logf("before update s = %v, arr = %v", s, arr)
s[0] = 666
// after update s = [666 3 4 5], arr = [0 1 666 3 4 5 6 7 8 9]
t.Logf("after update s = %v, arr = %v", s, arr)
}
- 添加或删除切片元素都返回新切片
func TestArrayAndSliceIncreasing(t *testing.T) {
var s []int
fmt.Println("add new item to slice")
for i := 0; i < 10; i++ {
s = append(s, i)
fmt.Printf("s = %v, len(s) = %d, cap(s) = %d\n", s, len(s), cap(s))
}
fmt.Println("remove item from slice")
for i := 0; i < 10; i++ {
s = s[1:]
fmt.Printf("s = %v, len(s) = %d, cap(s) = %d\n", s, len(s), cap(s))
}
}
[index]
访问切片元素仅仅和切片的len
有关,[start:end]
创建新切片仅仅和原切片的cap
有关
func TestArrayAndSliceBound(t *testing.T) {
arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := arr[5:8]
// s1[0] = 5, s1[2] = 7
t.Logf("s1[0] = %d, s1[%d] = %d", s1[0], len(s1)-1, s1[len(s1)-1])
// s1 = [5 6 7], len(s1) = 3, cap(s1) = 5
t.Logf("s1 = %v, len(s1) = %d, cap(s1) = %d", s1, len(s1), cap(s1))
s2 := s1[3:5]
// s2[0] = 8, s2[1] = 9
t.Logf("s2[0] = %d, s2[%d] = %d", s2[0], len(s2)-1, s2[len(s2)-1])
// s2 = [8 9], len(s2) = 2, cap(s2) = 2
t.Logf("s2 = %v, len(s2) = %d, cap(s2) = %d", s2, len(s2), cap(s2))
}
- 只有
map
没有set
func TestMapAndSet(t *testing.T) {
m := map[string]string{
"name": "snowdreams1006",
"site": "https://snowdreams1006.github.io",
"lang": "go",
}
// https://snowdreams1006.github.io
if site, ok := m["site"]; ok {
t.Log(site)
} else {
t.Log("site does not exist ")
}
s := map[string]bool{
"name": true,
"site": true,
"lang": true,
}
// Pay attention to snowdreams1006
if _, ok := m["isFollower"]; ok {
t.Log("Have an eye on snowdreams1006")
} else {
s["isFollower"] = true
t.Log("Pay attention to snowdreams1006")
}
}
delete
函数删除集合map
键值对
func TestMapAndSetByDelete(t *testing.T) {
m := map[string]string{
"name": "snowdreams1006",
"site": "https://snowdreams1006.github.io",
"lang": "go",
}
delete(m, "lang")
// delete lang successfully
if _,ok := m["lang"];!ok{
t.Log("delete lang successfully")
}
}
关于 Go
语言中内建容器是不是都已经 Get
了呢?如果有表述不对的地方,还请指正哈,欢迎一起来公众号[雪之梦技术驿站]学习交流,每天进步一点点!