Go第十课-函数

95 阅读7分钟
  1. 函数是唯一一种基于特定输入,实现特定任务并可返回任务执行结果的代码块(Go 语言中的方法本质上也是函数)

  2. Go 程序就是一组函数的集合,Go 程序的执行流本质上就是在函数调用栈中上下流动,从一个函数到另一个函数。

  3. 在同一个 Go 包中,函数名应该是唯一的。

  4. 参数列表与返回值列表的组合也被称为函数签名,它是决定两个函数类型是否相同的决定因素.

  5. 每个函数声明所定义的函数,仅仅是对应的函数类型的一个实例。

  6. Go 语言中,函数参数传递采用是值传递的方式。所谓“值传递”,就是将实际参数在内存中的表示逐位拷贝(Bitwise Copy)到形式参数中。对于像整型、数组、结构体这类类型,它们的内存表示就是它们自身的数据内容,因此当这些类型作为实参类型时,值传递拷贝的就是它们自身,传递的开销也与它们自身的大小成正比。

  7. 但是像 string、切片、map 这些类型就不是了,它们的内存表示对应的是它们数据内容的“描述符”。当这些类型作为实参类型时,值传递拷贝的也是它们数据内容的“描述符”,不包括数据内容本身,所以这些类型传递的开销是固定的,与数据内容大小无关。这种只拷贝“描述符”,不拷贝实际数据内容的拷贝过程,也被称为“浅拷贝”。

  8. 对于类型为接口类型的形参,Go 编译器会把传递的实参赋值给对应的接口类型形参;对于为变长参数的形参,Go 编译器会将零个或多个实参按一定形式转换为对应的变长形参。

  9. 在 Go 中,变长参数实际上是通过切片来实现的。

  10. Go 语言的函数作为“一等公民”

    1. 特征一:Go 函数可以存储在变量中
    2. 特征二:支持在函数内创建并通过返回值返回。
    3. 特征三:作为参数传入函数。
    4. 特征四:拥有自己的类型。

与其他主要编程语⾔的差异

  1. 可变参数

    func sum(ops ...int) int {
    	res := 0
    	for _, v := range ops {
    		res += v
    	}
    	return res
    }
    
    func TestSum(t *testing.T) {
    	t.Log(sum(1, 2, 3, 4))    // 10
    	t.Log(sum(1, 2, 3, 4, 5)) // 15
    }
    
  2. 可以有多个返回值

    func returnMulti() (int, int) {
    	rand.Seed(time.Now().UnixNano())
    	return rand.Intn(10), rand.Intn(20)
    }
    
    func TestReturnMulti(t *testing.T) {
    	t.Log(returnMulti())
    }
    
  3. 函数可以作为变量的值,可以作为参数和返回值

匿名函数与闭包

  1. 匿名函数是没有函数名的函数,保存到某个变量或者作为立即执行函数,多用于实现回调函数和闭包。

    func showfunc() {
    	// 将匿名函数保存到变量
    	add := func(x, y int) {
    		fmt.Println(x + y)
    	}
    	add(10, 20) // 通过变量调用匿名函数
    	// 自执行函数:匿名函数定义完加()直接执行
    	func(x, y int) {
    		fmt.Println(x + y)
    	}(10, 40)
    }
    
  2. 闭包=函数+应用环境。

  3. Go 闭包是在函数内部创建的匿名函数,这个匿名函数可以访问创建它的函数的参数与局部变量。

    func adder() func(int) int {
    	var x int
    	return func(y int) int {
    		x += y
    		return x
    	}
    }
    func useAdder() {
    	f := adder()
    	fmt.Println(f(10)) // 10
    	fmt.Println(f(20)) // 30
    	fmt.Println(f(30)) // 60
    	f2 := adder()
    	fmt.Println(f2(20)) // 20
    	fmt.Println(f2(40)) // 60
    }
    
  4. 闭包本质上就是一个匿名函数或叫函数字面值,它们可以引用它的包裹函数,也就是创建它们的函数中定义的变量。然后,这些变量在包裹函数和匿名函数之间共享,只要闭包可以被访问,这些共享的变量就会继续存在。

错误处理策略

  1. 如何进行错误处理的?

    1. Go 函数增加了多返回值机制,来支持错误状态与返回信息的分离
  2. 使用 error 类型,而不是传统意义上的整型或其他类型作为错误类型,有什么好处呢?

    1. 第一点:统一错误类型。
    2. 第二点:错误是值。
    3. 第三点:易扩展,支持自定义错误上下文。
  3. 具体策略:

    1. 策略一:透明错误处理策略。这样构造出的错误值代表的上下文信息,对错误处理方是透明的,因此这种策略称为“透明错误处理策略”。
    2. 策略二:“哨兵”错误处理策略:我建议你尽量使用errors.Is方法去检视某个错误值是否就是某个预期错误值,或者包装了某个特定的“哨兵”错误值。
    3. 策略三:错误值类型检视策略:请尽量使用errors.As方法去检视某个错误值是否是某自定义错误类型的实例。errors.As函数会沿着该包装错误所在错误链,与链上所有被包装的错误的类型进行比较,直至找到一个匹配的错误类型。
    4. 策略四:错误行为特征检视策略:将某个包中的错误类型归类,统一提取出一些公共的错误行为特征,并将这些错误行为特征放入一个公开的接口类型中。
  4. 健壮性的“三不要”原则

    1. 不要相信任何外部输入的参数。为了保证函数的健壮性,函数需要对所有输入的参数进行合法性的检查。
    2. 不要忽略任何一个错误。我们不能假定它一定会成功,我们一定要显式地检查这些调用返回的错误值。一旦发现错误,要及时终止函数执行,防止错误继续传播。
    3. 不要假定异常不会发生。

Go 语言中的异常:panic

  1. 一类是来自 Go 运行时,另一类则是 Go 开发人员通过 panic 函数主动触发的。

  2. 无论在哪个 Goroutine 中发生未被恢复的 panic,整个程序都将崩溃退出。

  3. 不过,函数 F 中已进行求值的 deferred 函数都会得到正常执行,执行完这些 deferred 函数后,函数 F 才会把控制权返还给其调用者。

  4. recover 是 Go 内置的专门用于恢复 panic 的函数,它必须被放在一个 defer 函数中才能生效。如果 recover 捕捉到 panic,它就会返回以 panic 的具体内容为错误上下文信息的错误值。如果没有 panic 发生,那么 recover 将返回 nil。而且,如果 panic 被 recover 捕捉到,panic 引发的 panicking 过程就会停止。

    func funcA() {
    	fmt.Println("func A")
    }
    
    func funcB() {
    	defer func() {
    		err := recover()
    		//如果程序出出现了panic错误,可以通过recover恢复过来
    		if err != nil {
    			fmt.Println("recover in B")
    		}
    	}()
    	panic("panic in B")
    }
    
    func funcC() {
    	fmt.Println("func C")
    }
    func mainRecover() {
    	funcA()
    	funcB()
    	funcC()
    	// func A
    	// recover in B
    	// func C
    }
    
  5. 如何应对 panic?

    1. 第一点:评估程序对 panic 的忍受度。

    2. 第二点:提示潜在 bug。在 Go 标准库中,大多数 panic 的使用都是充当类似断言的作用的。

    3. 第三点:不要混淆异常与错误。

      1. Java 的checked exception用于一些可预见的、常会发生的错误场景,Java 中对checked exception处理的本质是错误处理,虽然它的名字用了带有“异常”的字样。
      2. Go 中的 panic 更接近于 Java 的RuntimeException+Error,而不是checked exception。

defer 函数

  1. defer 函数。在Go语言的函数中return语句在底层并不是原子操作,它分为给返回值赋值和RET指令两步。而defer语句执行的时机就在返回值赋值操作后,RET指令执行前。

    func Clear() {
    	fmt.Println("clear string")
    }
    
    func TestDefer(t *testing.T) {
    	defer Clear()
    	fmt.Println("continue")
    	panic("err") // defer 仍会执行
    }
    
  2. 只有在函数(和方法)内部才能使用 defer。

  3. 这些函数被称为 deferred 函数。defer 将它们注册到其所在 Goroutine 中,用于存放 deferred 函数的栈数据结构中,这些 deferred 函数将在执行 defer 的函数退出前,按后进先出(LIFO)的顺序被程序调度执行。

  4. defer注册要延迟执行的函数时,该函数所有的参数都需要确定其值。

  5. 注意事项

    1. 第一点:明确哪些函数可以作为 deferred 函数
    2. 第二点:注意 defer 关键字后面表达式的求值时机
    3. 第三点:知晓 defer 带来的性能损耗
    4. defer 关键字后面的表达式,是在将 deferred 函数注册到 deferred 函数栈的时候进行求值的。