Go切片,函数简单总结 |青训营笔记

104 阅读8分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第3天

slice

切片是一种简化版的动态数组。因为动态数组的长度不固定,切片的长度自然也就不能是类型的组成部分了。

切片高效操作的要点是要降低内存分配的次数,尽量保证append操作(在后续的插入和删除操作中都涉及到这个函数)不会超出cap的容量,降低触发内存分配的次数和每次分配内存大小。

slice定义

 type SliceHeader struct {
     Data uintptr   // 指向底层的的数组指针
     Len  int       // 切片长度
     Cap  int       // 切片最大长度
 }

和数组一样,内置的len函数返回切片中有效元素的长度,内置的cap函数返回切片容量大小,容量必须大于或等于切片的长度。

切片可以和nil进行比较,只有当切片底层数据指针为空时切片本身为nil,这时候切片的长度和容量信息将是无效的。如果有切片的底层数据指针为空,但是长度和容量不为0的情况,那么说明切片本身已经被损坏了

只要是切片的底层数据指针、长度和容量没有发生变化的话,对切片的遍历、元素的读取和修改都和数组是一样的。在对切片本身赋值或参数传递时,和数组指针的操作方式类似,只是复制切片头信息(reflect.SliceHeader),并不会复制底层的数据。对于类型,和数组的最大不同是,切片的类型和长度信息无关,只要是相同类型元素构成的切片均对应相同的切片类型。

当我们想定义声明一个切片时可以如下:

在对切片本身赋值或参数传递时,和数组指针的操作方式类似,只是复制切片头信息·(reflect.SliceHeader),并不会复制底层的数据。对于类型,和数组的最大不同是,切片的类型和长度信息无关,只要是相同类型元素构成的切片均对应相同的切片类型。

添加元素

append() :内置的泛型函数,可以向切片中增加元素。

 在切片尾部追加N个元素
 var a []int
 a = append(a, 1)               // 追加1个元素
 a = append(a, 1, 2, 3)         // 追加多个元素, 手写解包方式
 a = append(a, []int{1,2,3}...) // 追加一个切片, 切片需要解包
 ​
 注意:尾部添加在容量不足的条件下需要重新分配内存,可能导致巨大的内存分配和复制数据代价。即使容量足够,依然需要用append函数的返回值来更新切片本身,因为新切片的长度已经发生了变化。
 ​
 切片开头位置添加元素
 var a = []int{1,2,3}
 a = append([]int{0}, a...)        // 在开头位置添加1个元素
 a = append([]int{-3,-2,-1}, a...) // 在开头添加1个切片
 ​
 注意:在开头一般都会导致内存的重新分配,而且会导致已有的元素全部复制1次。因此,从切片的开头添加元素的性能一般要比从尾部追加元素的性能差很多。
 ​
 append链式操作
 var a []int
 a = append(a[:i], append([]int{x}, a[i:]...)...)     // 在第i个位置插入x
 a = append(a[:i], append([]int{1,2,3}, a[i:]...)...) // 在第i个位置插入切片
 ​
 每个添加操作中的第二个append调用都会创建一个临时切片,并将a[i:]的内容复制到新创建的切片中,然后将临时创建的切片再追加到a[:i]。
 ​
 append和copy组合
 a = append(a, 0)     // 切片扩展1个空间
 copy(a[i+1:], a[i:]) // a[i:]向后移动1个位置
 a[i] = x             // 设置新添加的元素
 第三个操作中会创建一个临时对象,我们可以借用copy函数避免这个操作,这种方式操作语句虽然冗长了一点,但是相比前面的方法,可以减少中间创建的临时切片。

删除元素

 从开头位置删除;
 直接移动数据指针,代码如下:
 a = []int{1, 2, 3, ...}
 a = a[1:]                       // 删除开头1个元素
 a = a[N:]                       // 删除开头N个元素
 ​
 将后面的数据向开头移动,使用append原地完成(所谓原地完成是指在原有的切片数据对应的内存区间内完成,不会导致内存空间结构的变化)
 a = []int{1, 2, 3, ...}
 a = append(a[:0], a[1:]...) // 删除开头1个元素
 a = append(a[:0], a[N:]...) // 删除开头N个元素
 ​
 使用copy将后续数据向前移动,代码如下:
 a = []int{1, 2, 3}
 a = a[:copy(a, a[1:])] // 删除开头1个元素
 a = a[:copy(a, a[N:])] // 删除开头N个元素
 ​
 从中间位置删除;
 对于删除中间的元素,需要对剩余的元素进行一次整体挪动,同样可以用append或copy原地完成:
 append删除操作如下:
 a = []int{1, 2, 3, ...}
 a = append(a[:i], a[i+1], ...)
 a = append(a[:i], a[i+N:], ...)
 ​
 copy删除操作如下:
 a = []int{1, 2, 3}
 a = a[:copy(a[:i], a[i+1:])] // 删除中间1个元素
 a = a[:copy(a[:i], a[i+N:])] // 删除中间N个元素
 ​
 从尾部删除。
 a = []int{1, 2, 3, ...}
 a = a[:len(a)-1]   // 删除尾部1个元素
 a = a[:len(a)-N]   // 删除尾部N个元素
 ​
 删除切片尾部的元素是最快的

函数

为完成某一功能的程序指令(语句)的集合,称为函数。

函数分类 在Go语言中,函数是第一类对象,我们可以将函数保持到变量中。函数主要有具名和匿名之分,包级函数一般都是具名函数,具名函数是匿名函数的一种特例,当匿名函数引用了外部作用域中的变量时就成了闭包函数,闭包函数是函数式编程语言的核心。

 具名函数:就和c语言中的普通函数意义相同,具有函数名、返回值以及函数参数的函数。
 func Add(a, b int) int {
     return a+b
 }
 ​
 匿名函数:指不需要定义函数名的一种函数实现方式,它由一个不带函数名的函数声明和函数体组成。
 var Add = func(a, b int) int {
     return a+b
 }
  1. 闭包函数:返回为函数对象,不仅仅是一个函数对象,在该函数外还包裹了一层作用域,这使得,该函数无论在何处调用,优先使用自己外层包裹的作用域。
  2. 一级对象:支持闭包的多数语言都将函数作为第一级对象,就是说函数可以存储到变量中作为参数传递给其他函数,最重要的是能够被函数动态创建和返回。
  3. 包:go的每一个文件都是属于一个包的,也就是说go是以包的形式来管理文件和项目目录结构的。

函数声明和定义

 func fuction_name([parameter list])[return types]{
     函数体
 }
  1. func 函数由func开始声明
  2. function_name 函数名称
  3. parameter list 参数列表
  4. return_types 返回类型
  5. 函数体 函数定义的代码集合

函数传参

Go语言中的函数可以有多个参数和多个返回值,参数和返回值都是以传值的方式和被调用者交换数据。在语法上,函数还支持可变数量的参数,可变数量的参数必须是最后出现的参数,可变数量的参数其实是一个切片类型的参数。

当可变参数是一个空接口类型时,调用者是否解包可变参数会导致不同的结果

 func main(){
     var a = []int{1, 2, 3}
     Print(a...)   // 解包
     Print(a)      // 未解包
 }
 ​
 func Print(a ...int{}) {
     fmt.Println(a...)
 }

以上当传入参数为a...时即是对切片a进行了解包,此时其实相当于直接调用Print(1,2,3)。当传入参数直接为 a时等价于直接调用Print([]int{}{1,2,3})

函数返回值

不仅函数的参数可以有名字,也可以给函数的返回值命名。

 举例代码如下:
 func Find(m map[int]int, key int)(value int, ok bool) {
     value,ok = m[key]
     return
 }
 ​
 如果返回值命名了,可以通过名字来修改返回值,也可以通过defer语句在return语句之后修改返回值,举例代码如下:
 func mian() {
     for i := 0 ; i<3; i++ {
         defer func() { println(i) }
     }
 }
 ​
 // 该函数最终的输出为:
 // 3
 // 3
 // 3

以上代码中如果没有defer其实返回值就是0,1,2,但defer语句会在函数return之后才会执行,也就是或只有以上函数在执行结束return之后才会执行defer语句,而该函数return时的i值将会达到3,所以最终的defer语句执行printlin的输出都是3。

defer语句延迟执行的其实是一个匿名函数,因为这个匿名函数捕获了外部函数的局部变量v,这种函数我们一般叫闭包。闭包对捕获的外部变量并不是传值方式访问,而是以引用的方式访问。

递归调用

Go语言中,函数还可以直接或间接地调用自己,也就是支持递归调用。Go语言函数的递归调用深度逻辑上没有限制,函数调用的栈是不会出现溢出错误的,因为Go语言运行时会根据需要动态地调整函数栈的大小。这部分的知识将会涉及goroutint和动态栈的相关知识,我们将会在之后的博文中向大家解释。

它的语法和c很相似,格式如下:

 func recursion() {
    recursion() /* 函数调用自身 */
 }
 ​
 func main() {
    recursion()
 }