函数

函数包含连续的执行语句,可以再代码中通过调用函数来执行它们。函数能够将一个复杂的工作切分成多个更小的模块,使得多人协作变得更容易。函数对它的使用者隐藏了实现细节。这几方面的特性使函数成为多数编程语言的重要特性之一。

一、函数声明

每个函数声明都包含一个名字、一个形参列表、一个可选的返回列表以及函数体:

func name(parameter-list)(result-list){
    body
}

形参列表指定了一组变量的参数名和参数类型,这些局部变量都由调用者提供的实参传递而来。返回列表指定了函数返回值的类型。当函数返回一个未命名的返回值或者没有返回值时,返回列表的括号可以省略。返回值也可以像形参一样命名,这时每个命名的返回值会声明为一个局部变量,并根据变量类型初始化为相应的零值。

当函数存在返回列表时,必须显式地以return语句结束,除非函数明确不会走完整个执行流程,如无限for循环或函数中抛出宕机异常。

函数的类型称作函数签名。当两个函数拥有相同的形参列表和返回列表时,认为这两个函数的类型或签名是相同的,而形参和返回值的名字不会影响到函数类型,采用简写同样也不会影响到函数的类型。

每一次调用函数都需要提供实参来对应函数的每一个形参,包括参数调用顺序也必须一致。形参变量都是函数的局部变量,初始值由调用者提供的实参传递。函数形参以及命名返回值同属于函数最外层作用域的局部变量。

实参是按值传递的,所以函数接收到的是每个实参的副本。修改函数的形参变量并不会影响到调用者提供的实参。但如果提供的实参包含引用类型,那么函数使用形参变量时就有可能会间接地修改实参变量。

二、递归

函数可以递归调用,意味着函数可以直接或间接地调用自己。

许多编程语言适用固定长度的函数调用栈,大小在64KB到2MB之间。递归的深度会受限于固定长度的栈大小,所以当进行深度递归调用时必须谨防栈溢出。相比固定长度的栈,Go语言的实现使用了可变长度的栈,栈的大小会随着使用而增长,可达到1GB左右的上限。

三、多返回值

一个函数能够返回不止一个结果。标准包内的许多函数都返回两个值:一个期望得到的计算结果和一个错误值,或者表示函数是否调用正确的布尔值。

调用一个涉及多值计算的函数会返回一组值,如果调用者要使用这些值,则必须显式地将返回值赋给变量。返回一个多值结果可以是调用另一个多值返回的函数。一个多值调用可以作为单独的实参传递给拥有多个形参的函数中。一个函数如果有命名的返回值,可以省略return语句的操作室,称为裸返回,裸返回是将每个命名返回结果按照顺序返回的快捷方法,当在函数中存在多个返回语句且有多个返回结果,裸返回还可以消除重复代码,但并不能使代码更易理解。

func a(s string)(s []string, err error){
    ...
    return
}
ss, err := a(s)
func b(s string)([]string, error){
    return a(s)
}

四、错误

有一些函数总是成功返回的,一些函数只要符合其前置条件就能够成功返回。但对于一些函数,即使在高质量的代码中,也不能保证一定能成功返回,很多可靠的操作都可能会毫无征兆的发生错误。

如果当函数调用发生错误时返回一个附加的结果作为错误值,则可以通过这个结果查看错误信息。习惯上将错误值作为最后一个结果返回。如果错误只有一种情况,结果通常设置为布尔类型。

很多时候,错误的原因可能多种多样,这时则需要一些详细信息,错误的结果类型往往是error。error是内置的结构类型。

一个错误可能是空值或非空值,空值意味着成功而非空值意味着失败,非空的错误类型有一个错误消息字符串,可以通过调用它的Error方法或直接输出错误信息。一般当一个函数返回一个非空错误时,它的其他结果都是未定义的且应该忽略。但有一些函数在调用出错的情况下回返回部分有用的结果。

1.错误处理策略

当一个函数调用返回一个错误时,调用者应当负责检查错误并采取合适的处理应对。

一、将错误传递下去

fmt.Errorf使用fmt.Sprintf函数格式化一条错误信息并且返回一个新的错误值。

设计一个错误信息时应当慎重,确保每一条信息的描述都是有意义的,包含充足的相关信息,并且保持一致性,无论被同一个函数或同一个包下的一组函数返回时,这样的错误都可以保持统一的形式和错误处理方式。

二、对于不固定或不可预测的错误,在短暂间隔后对操作进行重试,超出一定重试次数和限定的时间后再报错退出

三、如果依旧不能顺利进行下去,调用者能够输出错误并优雅地停止程序

可以使用log包中含的函数,如log.Fatalf等。

四、一些错误情况下,之巨鹿错误信息后程序继续运行

五、在某些罕见情况下可以直接安全地忽略掉整个日志

Go语言的错误处理有特定的规律。进行错误检查之后,检测到失败的情况往往都在成功之前,如果检测到的失败导致函数返回,成功的逻辑一般不会放在else块中而是在外层的作用域中。函数会有一种通常的形式,就是在开头使用一连串检查来返回错误,之后跟着实际的函数体一直到最后。

2.文件结束标志

通常,用户会对函数返回的多种错误感兴趣,而不是中间涉及的程序逻辑。一个程序必须针对不同种类的错误采取不同的措施。

考虑如果要从一个文件中读取n个字节的数据,如果n是文件本身的长度,任何错误都代表操作失败。另一方面,如果调用者反复尝试读取固定大小的块指导文件耗尽,调用者必须把读取到文件末尾的情况区别于遇到其他错误的操作。

因此,io包保证任何有文件结束引起的读取错误,始终都会得到一个与众不同的错误—io.EOF。这个错误可以作为文件结束的标志。

五、函数变量

函数在Go语言中是头等重要的值,就像其他值一样,函数变量也有类型,而且可以赋给变量或者传递或者从其他函数中返回。函数变量可以像其他函数一样调用。

函数类型的零值是nil,调用一个空的函数变量将导致宕机。函数变量可以和空值进行比较,但本身不可比较,所以不能互相比较或者作为键值出现在map中。

var f func(int) int
f(3) //宕机
if f != nil{
    f(3)
}

六、匿名函数

命名函数只能在包级别的作用域进行声明,但我们能够使用函数字面量在任何表达式内指定函数变量。函数字面量就像函数声明,但在func关键字后面没有函数的名称。它是一个表达式,它的值称作匿名函数。以这种方式定义的函数能够获取到整个词法换将,因此里层的函数可以使用外层函数中的变量。

func squares() func() int{
    var x int
    return func() int{
        x++
        return x * x
    }
}
func main(){
    f := squares()
    fmt.Println(f()) //1
    fmt.Println(f()) //4
    fmt.Println(f()) //9
    fmt.Println(f()) //16
}
//函数squares返回了另一个函数,类型是func() int。调用squares创建了一个局部变量x并返回了一个匿名函数,每次调用squares都会递增x的值,然后返回x的平方。第二次调用squares函数将创建第二个变量x,然后返回一个递增x值的新匿名函数。
//这个例子中变量的生命周期不是由它的作用域所决定的,x在main函数返回squares函数后依旧存在(x这时隐藏在函数变量f中)。

里层的匿名函数能够获取和更新外层squares函数的局部变量。这些隐藏的变量引用导致我们把函数归类为引用类型且函数变量无法进行比较的原因。

当一个匿名函数需要进行递归,必须先声明一个变量然后将匿名函数赋给这个变量。如果将两个步骤合并为一个声明,函数字面量将不能存在于函数变量的作用域中,这样就不能递归调用自己了。

七、变长函数

变长函数吧被调用时可以有可变的参数个数,如fmt.Printf与其变种。

在参数列表最后的类型名称之前使用省略号…表示一个变长函数,调用这个函数时可以传递该类型任意数目的参数。

func sum(vals ...int)int{
    total := 0
    for _, val := range vals{
        total += val
    }
    return total
}
func main(){
    fmt.Println(sum()) //0
    fmt.Println(sum(3)) //3
    fmt.Println(sum(1, 2, 3, 4)) //10
    
    values := []int{1, 2, 3, 4}
    fmr.Printlnn(sum(values...)) //10,同上面的10,作用相同
}
//sum函数返回零个或多个int参数。函数体内,vals是一个int类型的slice,调用sum时任意数量的参数都将提供给vals参数。

调用者显式地申请一个数组,将实参复制给这个数组,并把一个数组slice传递给函数。当实参已经存在于一个slice中时,如果要调用一个变长函数,在最后一个参数后面加上省略号…即可。

尽管…int参数就像函数体内的slice,但变长函数的类型和一个带有普通slice参数的类型不相同。

func f(...int){}
func g([]int){}
fmt.Printf("%T\n", f) //func(...int)
fmt.Printf("%T\n", g) //func([]int)

变长函数通常用于格式化字符串。函数的后缀f是广泛使用的命名习惯,用于可变长Printf风格的字符串格式化输出函数。

interface{}类型意味着这个函数的最后一个参数可以接收任意类型的值。

八、延迟函数调用

如在网络连接中,使用完成之后需要关闭网络连接,包括发生错误的情况,但随着有关网络的函数变得越来越复杂,并且需要更多的错误情况,关闭网络这种重复的工作会给之后的维护带来困扰。Go语言的defer机制可以让这些工作变得更简单。

语法上,一个defer语句就是普通的函数或方法调用,在调用之前加上关键字defer。函数和参数表达式会在语句执行时求值,但无论是正常情况下(执行return语句或函数执行完毕),或是不正常情况下(宕机),实际调用推迟到包含defer语句的函数结束后才执行。defer语句没有限制使用次数,调用时以调用defer语句顺序的倒序进行。

defer语句经常使用于成对的操作,比如打开关闭、连接断开、加锁解锁,即使是再复杂的控制流,资源也能在任何情况下正常释放。正确使用defer语句的地方是在成功获得资源之后。

defer语句也可以用来调试一个复杂的函数,即在函数的”入口“和”出口“处设置调试行为。

延迟执行的函数在return语句之后执行,并且可以更新函数的结果变量。因为匿名函数可以得到其外层函数作用域内的变量(包括命名的结果),所以延迟执行的匿名函数可以观察到函数的返回结果。

func double(x int)(result int){
    defer func(){fmt.Printf("double(%d) = %d\n", x, result)}()
    return x + x
}
_ = double(4)
//输出:double(4) = 8

延迟执行的匿名函数能够改变外层函数返回给调用者的结果:

func triple(x int)(result int){
    defer func(){result += x}()
    return double(x)
}
fmt.Println(triple(4)) //12

延迟的函数不到函数最后一刻是不会执行的,要注意循环里defer语句的使用。

九、宕机

Go语言的类型系统会捕获许多编译时的错误,但有些其他错误(比如数组越界访问或者解引用空指针)都需要在运行时进行检查。当Go语言运行时检测到这些错误,就会发生宕机。

并不是所有宕机都是在运行时发生的。可以直接调用内置的宕机函数,内置的当即函数可以接受任何值作为参数。

switch s := suit(drawCard()); s{
    case "Spades":
    case "Hearts":
    case "Diamonds":
    case "Clubs":
    default:
    panic(fmt.Sprintf("invalid suit %q", s))
}

设置函数的断言是一个良好的习惯,但这也会带来多余的检查。除非能够提供有效的错误信息或者能够很快地检测出结果,否则在运行时检测断言条件毫无意义。

func Reset(x *Buffer){
    if x == nil{
        panic("x is nil") //没必要
    } 
    x.elements = nil
}

尽管Go语言的宕机机制和其他语言的异常很类似,但宕机的使用场景不尽相同。由于宕机会引起程序异常退出,因此只有在发生严重错误时参会使用宕机。

当宕机发生时,所有的延迟函数以倒序执行,从栈最上面的函数开始一直返回至main函数:

func main(){
    f(3)
}
func f(x int){
    fmt.Printf("f(%d)\n", x+0/x) //panics if x == 0则发生宕机
    defer fmt.Printf("defer %d\n", x)
    f(x - 1)
}
//输出
//f(3)
//f(2)
//f(1)
//defer 1
//defer 2
//defer 3
//当调用f(0)时会发生宕机,执行三个延迟的fmt.Printf调用
//之后运行时终止了这个程序,会输出宕机信息和一个栈转储信息到标准错误流(省略)

十、恢复

退出程序通常是正确处理宕机的方式,但也有例外。在一定情况下可以进行恢复,至少有时可以在退出前清理当前混乱的情况。

如果内置的recover函数在延迟函数的内部调用,而且这个包含defer语句的函数发生宕机,recover会终止当前的宕机状态并返回宕机的值。函数不会从之前宕机的地方继续运行而是正常返回。如果recover在其他任何情况下运行则没有任何效果且返回nil。

func Parse(input string)(s *Syntax, err error){
    defer func(){
        if p := recover(); p != nil{
            err = fmt.Errorf("internal error: %v", p)
        }
    }()
}
//Parse函数中的延迟函数会从宕机状态恢复,并使用宕机值组成一条错误信息。

有些情况下是没有恢复动作的。比如内存耗尽使得Go运行时发生严重错误而直接终止进程。

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐