Go(2)-- 函数

1. 基本概念

1.1 函数定义

      一个函数的定义包括如下几个部分:函数声明关键字 func、函数名、参数列表、返回列表和函数体。函数名首字母大小写决定了该函数在其他包的可见性:大写时其他包可见,小写时只有相同的包可以访问;函数返回值需要使用()包裹,如果只有一个返回值,而且使用的是非命名的函数,则返回的参数的()可以省略。函数体使用{}包裹,并且{必须位于函数返回值同行的行尾。

func 函数名(形参列表)(返回值列表){
    
    执行语句 ...
    return 返回值列表
}

      举例:

在这里插入图片描述
      函数的特点:

  1. 多个相邻的相同类型的参数可以使用简写模式
func getSum(a, b int)int{ // a int, b int 简写为 a, b int
	return a + b
}
  1. 支持有名的返回值,参数名就相当于函数体内最外层的局部变量,命名返回值变量会被初始化为类型零值,最后的 return 可以不带参数名直接返回。
func getSumAndSub(a, b int)(sum , sub int){
	sum = a + b
	sum = a - b
	return // return 是 return sum , sub 的简写
}
  1. 不支持类似py中的默认值参数
  2. 不支持函数重载
  3. 不支持类似py中的函数嵌套,严格说不支持命名函数的嵌套定义,但支持嵌套匿名函数
func add(a, b int)(sum int){
	anoymous := func(x, y int) int{
		return x + y
	}
	sum = anoymous(a, b)
	return
}

1.2 多值返回

      Go 函数支持多汁返回,定义多值返回的返回参数列表时要使用()包裹,支持命名参数的返回。

func getSumAndSub(a, b int)(sum , sub int){
	sum = a + b
	sum = a - b
	return // return 是 return sum , sub 的简写
}

      如果多值返回值有错误类型,则习惯用法是将错误类型作为最后一个返回值。

      多值返回的实质是:主调函数预先分配好空间来存放返回值,被调函数执行时将返回值复制到该返回位置来实现的

1.3 实参到形参的传递

      Go 函数实参到形参的传递永远是值拷贝,有时我们可以更改实参的值是传入的是指针的拷贝。

1.4 可变参数

      Go 支持数目不确定的形式参数,使用 param ... type 的语法个数。

      可变参数的特点:

  1. 所有不定参数类型必须相同
  2. 可变参数必须是函数的最后一个参数

在这里插入图片描述

  1. 可变参数在函数体内相当于切片,对切片的操作同样适合对可变参数的操作。例如:
func sum(arr ... int)(sum int){

	for _, v := range arr{
		sum += v
	}

	return
}
  1. 切片可以作为参数传递给不定参数,切片名后要加上 ...

在这里插入图片描述

  1. 形参为可变参数的函数和形参为切片的函数类型并不一致
func sumA(arr ... int){
	
}

func sumB(arr  []int){
	
}

func main(){

	a := sumA
	b := sumB
	fmt.Printf("a type = %T\n", a)
	fmt.Printf("b type = %T\n", b)

}

在这里插入图片描述

  1. 在 Go 中,函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量了。通过该变量可以对函数调用。

在这里插入图片描述

  1. 函数既然是一种数据类型,因此在 Go 中,函数可以作为形参,并且调用

在这里插入图片描述

2. 函数签名和匿名函数

2.1 函数签名

      函数类型又叫函数签名,一个函数的类型就是函数定义首行去掉函数名、参数名和 { ,可以使用 fmt.Printf%T 格式化参数函数的类型。

package main

import (
	"fmt"
)

func add(a, b int) int{
	return a + b
}

func main(){
	fmt.Printf("%T\n", add)
}

在这里插入图片描述

      两个函数类型相同的条件是:拥有相同的形参列表和返回值列表(列表元素的次序、个数和类型都相同),形参名可以不同。

      可以使用 type 定义函数类型,函数类型变量可以作为函数的参数或返回值。

            基本语法:type 自定义数据类型名 数据类型

            案例:type myint int 这时,myint 等价 int 来使用了

            案例:type mySum func(int, int) int //这时,mySum 等价一个函数类型 func(int ,int) int

在这里插入图片描述
在这里插入图片描述

package main

import (
	"fmt"
)

func add(a, b int) int{
	return a + b
}

func sub(a, b int) int{
	return a - b
}

type Op func(int, int) int 	// 定义一个函数类型,输入的是两个 int 类型,
						   	// 返回值是一个 int 类型

func do(f Op, a, b int) int {// 定义一个函数,第一个参数是函数类型 Op
	return f(a, b)			// 函数类型兵力可以直接用来进行函数调用
}


func main(){

	a := do(add, 1, 2) // 函数名 add 可以当作相同函数类型形参,不需要强制类型转换
	fmt.Println(a)

	b := do(sub, 1, 2) 
	fmt.Println(b)
}

2.2 匿名函数

      匿名函数可以看作函数字面量,所有直接使用函数类型变量的地方都可以由匿名函数代替。匿名函数可以直接赋值给函数变量,可以当作实参,也可以当作返回值,还可以直接调用。

在这里插入图片描述

3. defer

      defer 用于注册多个延迟调用,这些调用以先进后出的顺序在函数退出前被执行。类似于 Java 中的 finally,defer 常用于保证一些资源最终一定能够得到回收和释放

在这里插入图片描述

       defer 后面必须是函数或方法的调用,不能是语句。

       defer 函数的实参在注册时通过值拷贝传递进去。

在这里插入图片描述

       defer 语句必须先注册后才能执行,如果 defer 位于 return 之后,则 defer 因为没有注册,不会执行。
       主动调用 os.Exit(int)退出进程时, defer 将不再被执行,即使 defer 已经提前注册。

在这里插入图片描述
       defer 最主要的价值是在 ,当函数执行完毕后,可以及时的释放函数创建的资源。

func test(){

    file = openFile("文件名")
    // 关闭文件资源
    // 这样可以避免忘记资源没有被释放
    defer file.close()
    
    // 其他代码
}

4. 闭包

4.1 概念

       闭包是由函数及其相关引用环境组合而成的实体,一般通过匿名函数在引用外部函数的局部变量或包全局变量构成。

      闭包 = 函数 + 引用环境

       闭包对闭包外的环境引入是直接引用,编译器检测到闭包,会将闭包引用的外部变量分配到堆上

       如果函数返回的闭包引用了该函数的局部变量(参数或函数内部变量):

  1. 多次调用该函数,返回的多个闭包所引用的外部变量是多个副本,原因是每次调用函数都会为局部变量分配内存。
  2. 用一个闭包函数多次,如果该闭包修改了其引用的外部变量,则每一次调用该闭包对该外部变量都有影响,因为闭包函数共享外部引用
func fa(a int) func(i int) int{

	return func(i int) int{
		fmt.Println(&a, a)
		a = a + i
		return a
	}
	
}

      可以看到,fa 函数内部的匿名函数引用到了 fa 的参数 a,根据闭包的定义,符合闭包的定义,所以构成了一个闭包。

func main()  {

	f := fa(1)			// f 引用的外部的闭包环境包括本次函数调用的形参 a 的值是 1
	g := fa(1)			// g 引用的外部的闭包环境包括本次函数调用的形参 a 的值是 1

	// 此时 f、g 引用的外部的闭包环境中的 a 的值并不是同一个,而是两次函数调用产生的副本

	fmt.Println(f(1))
	// 多次调用 f 引用的是同一个副本 a
	fmt.Println(f(1))

	// g 中的 a 的值仍然是 1
	fmt.Println(g(1))
	fmt.Println(g(1))

}

在这里插入图片描述

      如果一个函数调用返回的闭包引用修改了全局变量,则每次调用都会影响全局变量。

package main

import (
	"fmt"
)

var (
	a = 0
)

func fa() func(i int) int{

	return func(i int) int{
		fmt.Println(&a, a)
		a = a + i
		return a
	}

}

func main()  {

	f := fa()			// f 引用的外部的闭包环境包括全局变量 a 
	g := fa()			// g 引用的外部的闭包环境包括全局变量 a 

	// 此时 f、g 引用的外部的闭包环境中的 a 的值是同一个

	fmt.Println(f(1))
	fmt.Println(f(1))
	fmt.Println(g(1))
	fmt.Println(g(1))

}

在这里插入图片描述

      同一个函数返回的多个闭包共享该函数的局部变量。

package main

import (
	"fmt"
)

func fa(base int) (func(int) int, func(int) int){

	fmt.Println(&base, base)
	
	add := func(i int) int{
		base += i
		fmt.Println(&base, base)
		return base
	}

	sub := func(i int) int{
		base -= i
		fmt.Println(&base, base)
		return base
	}

	return add, sub
}

func main()  {

	// f、g 闭包引用的 base 是同一个,是 fa 函数调用传递过来的实参值
	f, g := fa(0)	// base 的地址是 0xc04204a058
	fmt.Println(f(1), g(2))

	// s、k 闭包引用的 base 是同一个,是 fa 函数调用传递过来的实参值
	s, k := fa(0) 	// base 的地址是 0xc04204a0c0
	fmt.Println(s(1), k(2))

	// f、g 和 s、k 引用不同的闭包变量,这是由于 fa 每次调用都要重新分配形参
}

在这里插入图片描述

4.2 闭包的价值

      闭包最初的目的是减少全局变量,在函数调用的过程中隐式地传递共享变量。但是这种隐秘地共享变量方式不够直观、不易理解。一般不建议使用闭包。

5. panic 和 recover

5.1 基本概念

      panicrecover 是用来处理运行时错误的内置函数。panic 用来主动抛出错误,recover 用来捕获 panic 抛出的错误。

      发生 panic后,程序会从调用 panic 的函数位置或者发生panic的地方立即返回,逐行向上执行函数的defer语句,然后逐层打印函数调用堆栈,直到被 recover 捕获或运行到最外层函数而退出。

       panic 的函数签名如下:

panic(i interface{})

       panic的参数是一个空接口类型interface{},所以任意类型的变量都可以传递给 panic。调用 panic的方法:panic(xxx)

package main

import "fmt"

func test()  {
	fmt.Println("panic before")

	panic("this is a panic")

	fmt.Println("panic after")
}

func main()  {
	
	test()

}

在这里插入图片描述
      recover()用来捕获 panic,阻止 panic 继续向上传递。recover()defer 一起使用,但是 recover() 只有在 defer后面的函数体内被直接调用才能捕获 panic 终止异常,否则返回 nil,异常继续向外传递。

// 这个会捕获失败
defer recover()

// 这个会捕获失败
defer fmt.Println(recover())

// 嵌套的两层也会捕获失败
defer func(){
	func () {
		fmt.Println("defer inner")
		recover() // 无效
	}()
}


// 如下场景会捕获成功

defer func(){
	fmt.Println("defer inner")
	recover()
}

func except()  {
	recover()
}

func test()  {
	defer except()
	panic("test panic")
}

      可以有连续多个 panic 被抛出,但是只有最后一次 panic 能被捕获。

package main

import "fmt"

func main()  {
	
	defer func ()  {
		if err := recover(); err != nil{
			fmt.Println(err)
		}
	}()

	defer func(){
		panic("first defer panic")
	}()

	defer func(){
		panic("second defer panic")
	}()

	panic("main body panic")

}

在这里插入图片描述

      包中 init 函数引发的 panic 只能在 init 函数中被捕获,因为 init 函数先于 main 函数执行。

      函数并不能捕获内部新启动协程goroutine所抛出的 panic

package main

import (
	"fmt"
	"time"
)

func main()  {
	
	defer func ()  {
		if err := recover(); err != nil{
			fmt.Println("捕获到 panic")
			fmt.Println(err)
		}
	}()

	go da() // 开启一个协程,执行 da 函数
	go db() // 开启一个协程,执行 db 函数

	time.Sleep(3 * time.Second) // 这里 Sleep 的原因是因为如果 main 函数先执行完,则并不能够等待协程的执行完。
}

func da()  {
	panic("panic da")
	for i := 0; i < 10; i++{
		fmt.Println(i)
	}
}

func db()  {
	for i := 0; i < 10; i++{
		fmt.Println(i)
	}
}

在这里插入图片描述

5.2 使用场景

       panic主动调用场景:

  1. 程序遇到了无法正常执行下去的错误,主动调用 panic 函数结束程序运行。
  2. 在调试程序时,通过主动调用 panic实现快速退出,panic打印出的堆栈能够更快地定位错误。

      为保证程序地健壮性,需要主动在程序的分支流程使用 recover() 拦截运行时错误。

6. 错误处理

      在 Java 中,我们通过 try ... catch ... finally 的方式处理程序中可能出现的错误。Go 语言追求简洁优雅,所以,Go 语言并不支持 try ... catch ... finally 这种处理。Go 中引入的处理方式为: defer, pacic, recover

6.1 error

      Go 语言内置错误接口类型 error。任何类型只要实现 Error() string 方法,便可以传递 error 接口类型变量。Go 语言典序错误处理方式是将 error 作为函数最后一个返回值,在调用函数时,通过检测其返回的 error 值是否为 nil 进行错误处理。

type error interface{
	Error() string
}

      在 Go 中,也支持自定义错误,使用 errors.Newpanic 内置函数。

  1. errors.New("错误说明"),会返回一个 error 类型的值,表示一个错误
  2. panic 内置函数,接收一个 interface() 类型的值(即任何值)作为参数,可以接收一个 error 类型的变量,输出错误信息,并退出程序。

在这里插入图片描述

6.2 错误和异常

      广义上的错误:发生非期望的行为。

      狭义上的错误:发生非期望的已知行为,这里的已知是指错误类型是预料并定义好的。

      异常:发生非期望的未知行为。这里的未知是指错误的类型不在预先定义的范围内。异常又被称为未捕获的错误(untrapped error)。程序在执行时发生未预先定义的错误,程序编译器和运行时都没有及时将其捕获处理。而是由操作系统进行异常处理。

在这里插入图片描述
      Go 在运行时不会出现编译器和运行时都无法捕获的异常,即不会出现 untrapped error。即 Go 语言不存在所谓的异常。

      Go 程序需要处理的错误分为两类:

  • 运行时错误,此类错误在运行时能够被捕获。
  • 程序逻辑错误:程序执行结果不符合预期,但不会引发运行时错误。

      Go 对于错误提供了两种处理机制:

  • 通过函数返回错误类型的值来处理错误。
  • 通过 panic打印程序调用栈,终止程序执行来处理错误。

      编程中,errorpanic 的使用应遵循如下原则:

  • 程序发生的错误但是程序不能容错继续执行,此时程序应主动调用 panic 或由运行时抛出 panic
  • 程序虽然发生错误,但是程序能够容错继续执行,此时应该使用错误返回值的方式处理错误。或者在可能发生运行时错误的非关键分支上使用 recover 捕获 panic
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值