上篇文章中详细介绍了 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 学习笔记之数组还是切片都没什么不一样-LMLPHP

正是由于切片这种内部实现,需要特性也好表现形式也罢才使得切换和数组有着千丝万缕的联系,其实这种数据结果就是对静态数组的扩展,本质上是一种动态数组而已,只不过 Go 语言叫做切片!

切片是动态数组,上述问题就很容易解释了,参数传递时传递的是内部指针,因而虽然是值传递拷贝了指针,但是指针指向的真正元素毕竟是一样的,所以切片可以修改外部参数的值.

数组可以在一定程度上进行比较,切片是动态数组,能不能进行比较呢?让接下来的测试方法来验证你的猜想吧!

go 学习笔记之数组还是切片都没什么不一样-LMLPHP

  • 切片的添加和删除

数组是静态结构,数组的大小不能扩容或缩容,这种数据结构并不能满足元素个数不确定场景,因而才出现动态数组这种切片,接下来重点看下切片怎么添加或删除元素.

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:] ,切片存储的元素开始缩减,个数递减,容量也递减.

go 学习笔记之数组还是切片都没什么不一样-LMLPHP

其实除了基于数组创建切片和直接创建切片的方式外,还存在第三种创建切片的方式,也是使用比较多的方式,那就是 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 是往后能看到的最大范围,动态数组的本质也是控制这两个变量实现有效数组的访问.

go 学习笔记之数组还是切片都没什么不一样-LMLPHP

go 学习笔记之数组还是切片都没什么不一样-LMLPHP

集合 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 了呢?如果有表述不对的地方,还请指正哈,欢迎一起来公众号[雪之梦技术驿站]学习交流,每天进步一点点!

go 学习笔记之数组还是切片都没什么不一样-LMLPHP

08-19 14:36