函数进阶(Go语言)

函数

定义

函数是结构话编程的最小模板单元。它将复杂的算法过车分解为若干较小任务,隐藏相关细节,使得程序结构更加清晰,易于维护。函数设计成相对对立,通过接收输入参数完成一段算法指令,输出或存储相关结果。函数是代码复用和测试的基本单元。

关键字func用于定义函数。Go中的函数有些不太方便的限制,但也借鉴了动态语言的某些优点。

  • 无须前置声明
  • 不支持命名嵌套
  • 不支持同名函数重载
  • 不支持默认参数
  • 支持不定长变参
  • 支持多返回值
  • 支持命名返回值
  • 支持匿名函数和闭包

函数属于第一类对象,具备相同签名(参数及返回值列表)的视作同一类型。

**第一类对象(first-class object)指可在运行期创建,可用作函数参数或返回值,可存入变量的实体。**最常用的就是匿名函数。

函数只能判定其是否为nil,不支持其他比较操作。

从函数返回局部变量指针是安全的,编译器会通过逃逸分析来决定是否在堆上分配内存。

函数内联(inline)对内存分配有一定影响。

当前编译器并未实现尾递归优化(tail-call optimization).尽管Go执行栈的上限是GB,轻易不会出现堆栈溢出(stack overflow)错误,但依然需要注意拷贝栈的复制成本。

参数

Go对参数的处理偏向保守,不支持有默认值的可选参数,不知命名实参;调用时,必须按签名顺序传递指定类型和数量的实参,就算以“

—”命名的参数也不能忽略。

在参数列表中,相邻的同类型参数可合并。

func test(x , y int ,s string ,_ bool) *int{
		return nil;
}

参数可视作函数局部变量,因此不能在相同层次定义同名变量。

形参是指函数定义中的参数,实参则是函数调用时所传递的参数。形参类似函数局部变量,而实参则是函数外部对象,可以是常量,变量,表达式或函数等。

不管是指针,引用类型,还是其他类型参数,都是值拷贝传递(pass-by-value).区别无非是拷贝目标对象,还是拷贝指针。在函数调用前,会为形参和返回值分配内存空间,并将实参拷贝到形参内存。

如果函数参数过多,建议将其重构为一个复合结构类型。

type zxt struct {
	name string
	sex  string
	age int
}

func newOption *zxt{
	return &zxt{
		name: "zxt"
		sex: "nan"
		age: 18
	}
}

变参

变参本质上就是一个切片。只能接收一到多个类型参数,且必须放在参数列表尾部。

将切片作为变参是,须进行展开操作。如果是数组,先将其转换为切片。

func test(a ...int){
	fmt.Println(a)
}

func mian(){
	a := [3]int{1,2,3}
	test(a[:]...)
}

既然变参是切片,那么参数复制的仅是切片自身,并不包括底层数组,也因此可修改原数据,如果需要,可用内置函数copy复制底层数据。

返回值

有返回值的函数,必须有明确的return终止语句。

除非有paint,或者无break的死循环,则无须return终止语句。

借鉴动态语言的多返回值模式,函数得以返回更多状态,尤其是error模式。

不方便的是没有元组类型,也不能用数组,切片接收,但可以“—”忽略掉不想要的返回值。多返回值可用作其他函数调用实参,或当作结果直接返回。

func div(x , y int) (int ,error){
	if y = 0 {
		return 0,errors.New("0")
	}
  return x/y ,nil
}

func log(x int , e error){
  fmt.Println(x,error)
}

func test()(int ,error){
  return div(5,0)
}

func mian(){
  log(test())
}

命名返回值

对返回值命名和简短变量定义一样,优缺点共存。

命名返回值让函数声明更加清晰,可以改善帮助文档和代码编译器提示。

命名返回值和参数一样,可当作函数局部变量使用,最后有return隐式返回。

func div (x ,y int ) (z int,err error){
	if y =0 {
		err =errors.New("0")
		return
	}
	z = x/y
	return
}

这些特殊的“局部变量”会被不同层级的同名变量遮蔽。但是编译器能检查到此类状况,只要改为显示调用即可。

func add (x , y int)(z int){
	{
		z := x /y  // 新定义的同名局部变量;同名遮蔽
		return //错误 改成 return z 
	}
	return 
}

除遮蔽外,还要对全部返回值命名,否则编译器会搞不懂清楚情况。

如果返回值类型能明确表明其含义,就尽量不要对其命名。

匿名函数

匿名函数是指没有定义名字符号的函数。

除了没有名字外,匿名函数和普通函数完全相同。最大区别是,可以在函数内部定义匿名函数,形成类似嵌套效果。匿名函数可直接调用,保存到变量,作为参数或返回值。

直接执行

func main(){
	func(s string){
		println(s)
	}("zxt hello")
}

赋值给变量

func main(){
	add := func(x,y int)int {
		return x+y
	}
	println(add(1,2))
}

作为参数

func test( f func() ) {
	
}

func mian() {
	test(func(){
		println("zzz")
	})
}

作为返回值

func test() func(int , int) int {
	return func(x,y int){
	retrun x+y
	}
}

func mian(){
	add := test()
	println(add(1,2))
}

将匿名函数赋值给变量,与为普通函数提供名字标识符有着根本区别。

普通函数和匿名函数都可作为结构体字段,或经通道传递。

不曾使用的匿名函数会被编译器当作错误。

除闭包因素外,匿名函数也是一种常见重构手段,可将大函数分解成多个相对独立的匿名函数块,然后用相对简洁的调用完成逻辑流程,已实现框架和细节。

相比语句块,匿名函数的作用域被隔离(不使用闭包),不会引用外部污染,更加灵活。没有定义顺序限制,必要时可抽离,便于实现干净,清晰的代码层次。

闭包

闭包(closure)是在其词法上下文中引用了自由变量的函数,或者说是函数和其引用的环境的组合体。

闭包是函数和引用环境的组合体。

闭包通过指针引用环境变量,可能导致生命周期延长,甚至被分配到堆内存。

每次用不同的环境变量或传参复制,让各自闭包环境不同。

多个匿名函数引用同一环境变量,也会让事情变得更加复杂。任何的修改行为都会影响其他函数取值,在并发模式下可能需要作同步处理。

闭包可以不用参数就可以读取或修改环境状态。

延迟调用

语句defer向当前函数注册稍后执行的函数调用。这些调用被称作延迟调用,因为它们直到当前函数执行结束前才被执行,常用于资源释放,解除锁定,以及错误处理等操作。

注意:延迟调用注册的是调用,必须提供执行所需参数(哪怕为空)。参数值在注册时被复制并缓存起来。

多个延迟注册按照FILO次序执行。(栈)

编译器通过插入额外指令来实现延迟调用,而return和panic语句度会终止当前函数流程,引发延迟调用。另外,return语句不是ret汇编指令,会先更新返回值。

误用 延迟调用在函数结束时才被执行。不合理的使用方式会浪费更多资源,甚至造成逻辑错误。

错误处理

error

官方推荐的标准做法是返回error状态。

func zxt(a ...interface{})(n int , e error){
	
}

标准库将error定义为接口类型,以便实现自定义错误类型。

type error interface {
	Error() string
}

error总是最后一个返回参数。标准库提供了相关创建函数,可以方便地创建包含简单错误文本的error对象。

err := errors.New("zzz")

应通过错误变量,而非文本内容来判定错误类别。

错误变量通常以er r作为前缀,且字符串内容全部小写,没有结束标点,以便于嵌入到其他格式化字符串中输出。

大量函数和方法 返回error,使得调用代码变得很难看,一堆堆的检查语句充斥在代码执行积案。解决思路有:

  • 使用专门的检查函数处理错误逻辑(如记录日志),简化检查代码。
  • 在不影响逻辑的情况下,使用defer延后处理错误状态(err退化赋值)。
  • 在不中断逻辑的情况下,将错误作为内部状态保存,等最终“提交”时在处理。

panic,recover

与error相比,panic/recover在使用方法上更类似于try/catch结构化异常。

func panic(v interface{})
func recoer() interface{}

panic和recover是内置函数并非语句。panic会立即中断当前函数流程,执行延迟调用。而在延迟调用函数中,recover可捕获并返回panic提交的错误对象。

func main(){
	defer func(){
		if err :=recover(); err != nil { //捕获错误
			log.fatalln(err)
		}
	}()
	panic("中断") //引发错误
	
	println("exit") // 永远不会被执行
}

因为panic参数是空接口类型,因此可使用任何对象作为错误状态。而recover返回结果同样要做转型才能获取到具体信息。

无论是否执行recover,所有延迟调用都会被执行。但中断性错误会沿调用堆栈向外传递,要么被外层捕获,要么导致进程崩溃。

func test(){
	defer println("test 1") // 遵循 栈的顺序 先进后出
	defer println("test 2") 
	
	panic("中断") //中断当前函数 触发延迟调用
}

func main(){
	 defer func(){
	 		log.Println(recover())
	 }()
	 
	 test()  //先执行test 函数 
}
// 输出结果   
// test 2
// test 1
// 中断 

连续调用panic仅最后一个会被recover捕获。

func main() {
	defer func() {
		for {
			if err := recover(); err != nil {
				log.Panicln(err)
			} else {
				log.Fatalln("fatal")
			}
		}
	}()
	defer func() {
		panic("抛出异常1")
	}()
	panic("抛出异常2")

}

在延迟函数中Panic,不会影响后续延迟调用执行,而recover之后panic,可被在此捕获。

除非是不可恢复性导致系统无法正常工作的错误,否则不建议使用panic.
参考资料
<Go语言学习笔记> 雨痕

©️2020 CSDN 皮肤主题: 像素格子 设计师:CSDN官方博客 返回首页