[Journey with golang] 2. Function

golang的函数作为“第一公民”,表现在:

  • 函数是一种类型,函数类型变量可以像其他类型变量一样使用,可以作为其他函数的参数或返回值,也可以直接调用执行
  • 支持多返回值返回
  • 支持闭包
  • 支持可变函数

与其他语言一样,函数声明包括关键字func、函数名、参数列表、返回列表和函数体。函数名遵循标识符的命名规则,首字母大小写决定了该函数在其他包的可见性:大写时其他包可见,小写时只有相同的包可以访问。

函数可以没有返回值,这样默认会返回0。

函数支持有名的返回值,参数名相当于函数体内最外层的局部变量,命名返回值变量会被初始化为类型零值,最后的return可以不带参数名直接返回。

函数不支持默认参数,不支持重载,也不支持命名函数的嵌套定义(匿名函数可以)。

函数支持多值返回,一个习惯用法是:如果多值返回时有error类型,把它作为最后一个返回值。

golang函数实参到形参的传递永远是值拷贝,有时函数调用后实参指向的值发生了变化,那是因为参数传递的是指针值的拷贝(那不就是c++的传指针?),实参是一个指针变量,传递给形参的是这个指针变量的副本,二者指向同一地址,本质上参数传递仍然是值拷贝。

不定参数声明使用 param ...type 这样的语法格式。所有的不定参数类型必须相同,且不定参数必须是函数的最后一个参数。不定参数名在函数体内相当于切片,对切片的操作同样适合对不定参数的操作。切片可以作为参数传递给不定参数,切片名后面要加上...。形参为不定参数的函数和形参为切片的函数类型不相同。

函数类型又叫函数签名,一个函数的类型就是函数定义首行去掉函数名、参数名和{,可以使用fmt.Printf的%T格式化参数来打印函数类型。两个函数类型相同的条件是:拥有相同的形参列表和返回值列表(列表元素次序、个数和类型都相同),形参名可以不同。

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

函数类型和map、slice、chan一样,实际函数类型变量和函数名都可以当做指针变量,该指针指向函数代码的开始位置。通常说函数类型变量是一种引用类型,未初始化的函数类型的变量默认值为nil。

有名函数的函数名可以看做函数类型的常量,可以直接使用函数名调用函数,也可以直接赋值给函数类型变量。

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

golang提供defer关键字,可以注册多个延迟调用。这些调用以先进后出(FIFO)的顺序在函数返回前被执行。defer常用语保证一些资源最终被回收或释放。defer后面必须是函数或方法的调用,不能是语句,否则会报expression in defer must be function call错误。

defer函数的实参在注册时通过值拷贝传递进去。defer语句必须先注册才能执行,如果在defer位于return之后就不会被执行。当主动调用os.Exit(int)退出进程时,defer也不再被执行。

defer语句的位置不当有可能会导致panic,一般defer语句放在错误检查语句之后。

defer也有明显的副作用:defer会推迟资源释放,defer相对于普通的函数调用需要间接数据结构的支持,与普通的函数调用相比,有一定性能损耗。

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

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

如果函数返回的闭包引用了该函数的局部变量:

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

如果一个函数调用返回的闭包引用修改了全局变量,则每次调用都会影响全局变量。使用闭包是为了减少全局变量,所以闭包引用全局变量不是好的编程方式。

闭包最初的目的是减少全局变量,在函数调用的过程中隐式传递共享变量,有其有用的一面:但是这种隐秘的共享变量的方式带来的坏处是不够直接,不够清晰,除非是非常有价值的地方,一般不建议使用闭包。

对象是附有行为的数据,而闭包是附有数据的行为。

golang提供了两种处理错误的方式,一种是借助panic和recover的抛出捕获机制,另一种是使用error错误类型。

panic用于主动抛出错误,而recover用来捕获panic抛出的错误。引发panic有两种情况,一种是程序主动调用panic函数,另一种是程序产生运行时错误,由运行时检测并抛出。

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

panic不但可以在函数正常流程中抛出,在defer逻辑里也可以再次调用panic或抛出panic,defer里面的panic能够被后续执行的defer捕获。

recover用来捕获panic,组织panic继续向上传递。recover和defer一起使用,但是recover只有defer后面的函数体内被直接调用才能捕获panic终止异常,否则返回nil,异常继续向外传递。可以有连续多个panic被抛出,但只有最后一次panic能被捕获。

广义上的错误:发生非期望的行为。狭义的错误:发生非期望的已知(指错误的类型是预料并定义好的)行为。异常:发生非期望的未知(与已知相反)行为。Go是一门类型安全的语言,其运行时不会出现这种编译器和运行时都无法捕获的错误,也就是说,不会出现untrapped error,所以从这个角度来说go不存在所谓异常,出现的“异常”全是错误。

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

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

所以对错误的处理也有两种办法,一种是通过返回一个错误类型值来处理错误,另一种是直接调用panic抛出错误,退出程序。

实际编程中,error和panic的使用应遵循如下三条规则:

  1. 程序局部代码的执行结果不符合预期,但此种行为不是运行时错误范围内预定义的错误,此种非期望的行为不会导致程序无法提供服务,此类场景应该使用函数返回error类型变量进行错误处理。
  2. 程序执行过程中发生错误,且该种错误是运行时错误范围内预定义的错误,此时golang默认的隐式处理动作就是调用panic,如果此种panic发生在程序的分支流程不影响主要功能,则可以在发生panic的程序分支上游处使用recover进行捕获,避免引发整个程序的崩溃
  3. 程序局部代码执行结果不符合预期,此种行为虽然不是运行时错误范围内预定义的错误,但此种非期望的行为会导致程序无法继续提供服务,此类场景在代码中应该主动调用panic终止程序的执行。

go函数使用的是caller-save的模式,即由调用者负责保存寄存器,所以在函数汇编代码头尾不会出现push ebp; mov esp ebp这样的代码,相反是在主调函数调用被调函数的前后有一个保存现场和恢复现场的动作。

go编译器产生的汇编代码是一种中间抽象态,它不是对机器码的映射,而是和平台无关的一个中间态汇编描述所以汇编代码中有些寄存器是真实的,有些是抽象的,比如:

  • SB(static base pointer,静态基址寄存器),它和全局符号一起表示全局变量的地址。
  • FP(frame pointer,栈帧寄存器),该寄存器指向当前函数调用栈帧的栈底位置。
  • PC(program counter,程序计数器),存放下一条指令的执行地址,很少直接操作该寄存器,一般是CALL,RET等指令隐式的操作。
  • SP(stack pointer,栈顶寄存器),一般在函数调用前由主调函数设置SP的值对占空间进行分配或回收。

go内嵌汇编和反汇编产生的代码并不是一一对应的,汇编编译器对内嵌汇编程序自动作出了调整,主要差别是增加了保护现场,以及函数调用前保持PC、SP偏移地址重定位等逻辑。反汇编代码更能反应程序的真实执行逻辑。

猜你喜欢

转载自www.cnblogs.com/JHSeng/p/12141958.html