[奔跑的 Go] 教程七、深入学习 Go 语言中的函数

Golang

如同 JavaScript,函数是 Go 语言中的一等公民。它们可以赋值给变量,作为参数传递,立即执行或者延后上次执行。

☛ 函数的定义

通常,函数是指基于某些输入值执行特定任务的代码块。为了可以随时随地执行这个代码块, 可以将其创建为一个函数。输入值可以为函数提供额外的信息,是可选的。函数可以有返回值,也可以没有返回值。

在 Go 中,用 func 关键字定义函数。

func dosomething() {
    fmt.Println("Hello World!")
}

可以在程序中函数体内的任何地方调用函数,如 dosomething()

https://play.golang.org/p/THBF9b1nOr-

☛ 函数的命名规则

Go 建议用单个单词或小驼峰的形式命名函数。尽管带 下划线 的函数名也是合法的,但是不符合 Go 的命名规范。

☛ 函数的参数

正如前面所提到的,函数在执行时可以接收额外的输入值,我们把它们称为参数。一个函数可以接收一个或多个参数。

例子 1. 返回问候信息

https://play.golang.org/p/E_7DdS_Hw4f

例子 2. 对两个整数进行加和

https://play.golang.org/p/QUNdCtGO4sA

如果所有参数都是相同的数据类型,则可以使用简短参数表示法。

https://play.golang.org/p/j8yvuTv10KK

☛ 返回值

一个函数可以返回一个值,这个值可以被打印出来或者赋值给另外一个变量。

https://play.golang.org/p/b0V3589pbVj

一个函数的返回值,必须在函数的参数的括号后面指定返回值的具体类型。 在上面的程序中,我们通过将结果类型(int)转换为int64来确保返回值与函数的返回类型匹配。

☛ 多个返回值

与其他编程语言不同的是,Go 函数可以有多个返回值。当函数有多个返回值的时候,返回值的数据类型需要用括号括起来,放在参数后面。

https://play.golang.org/p/TSSbha-8g9f

获取函数的多个返回值,需要同时声明多个变量,并用逗号分隔。

如果只需要多个返回值中的某一个值,可以将不需要的返回值赋值给 _ (空白标识符)。这种做法还是很有必要的,因为如果定义了一个变量但没有使用,编译器就会报错。

https://play.golang.org/p/eqLywlCdAAW

☛ 给返回值命名

在定义函数时给返回值命名,有利于在函数中明确地提及每个返回值变量。系统会自动为这些变量创建空间,并可以在函数体内直接可用。可以在函数内更改这些变量的值。需要在函数末尾用 return 语句返回这些命名返回值。当程序执行到 return语句时,就会自动返回这些变量。

https://play.golang.org/p/7mjltVetYS8

当返回值数据类型相同时,可以使用如下的简写模式

https://play.golang.org/p/UBtP7nnBD8_c

☛ 递归函数

直接或间接调用函数本身,则该函数称为递归函数。递归函数的基本语法格式如下

func r() {
    r()
}

当运行上面的函数 r 时,它将无限循环。因此,在递归函数中,我们通常使用诸如 if-else 之类的条件语句来避免无限循环。

一个简单的递归函数的例子是 n 的阶乘。“n” 的阶乘的基本递归公式是 “n *(n-1)”( 要求 “n> 0”)。

// n! = n×(n-1)! 要求 n >0
func getFactorial(num int) int {
    if num > 1 {
        return num * getFactorial(num-1)
    } else {
        return 1 // 1! == 1
    }
}

上面的 getFactorial 函数就是一个递归函数,因为是在 getFactorial 函数内部调用继续调用 getFactorial 函数。

当调用函数 getFactorial 并传递整型参数 num 时,如果 num 等于 1,函数返回 1,否则执行 num * getFactorial(num-1)。再次调用 getFactorial 函数,返回后继续往复上面的步骤,直到 num 最终等于 1。这时开始倒着将所有的函数调用逐一执行,直到第一次 getFactorial 函数调用为止。

https://play.golang.org/p/xg1_zTFUsd5

☛ defer 关键字

defer 是 Go 中的一个关键字,它的作用是使函数延迟执行, 直到父函数执行结束或父函数命中 return 语句时再执行。

通过下面这个例子,可以对其有更直观的了解。

https://play.golang.org/p/x4JfXO3DEng

main 函数执行时,首先打印 main started,当执行到 sayDone 函数时,因为有 defer 关键字,这个函数会被放到等待队列中。继续向下执行,打印 main finished,然后 main 函数退出,sayDone() 函数开始执行。

还可以给被延迟执行的函数传递参数,但是这里有个坑。下面我们来写一个简单的带参数的函数。

https://play.golang.org/p/bxUskp0MCvo

在上面的程序中,我们让 endTime 函数 “延迟” 执行,也就是说它将在 main 函数结束时再执行,但是由于在 main 函数结束时,time ===“2 PM”,我们想要的是 Program ended at 2 PM 的消息。由于 endTime 函数被延迟到 main 函数结束时才执行,然而它的参数 time 在其被放入 stack 时已经被赋值为 '1 PM'。

你可能会问,这里的 stack 是指什么。你可以把 stack 想像成为一个笔记本,Go 编译器会在其中记录下要被延迟执行的函数。遵循 先进后出 ( LIFO ) 的执行顺序。

让我们来编写多个延期任务,阐释一下我的意思。

https://play.golang.org/p/NmeNjRPEmTK

defer 的一个比较实用的地方就是:当一个函数中含有很多条件判断,无论是if-elsecase 语句,在每个情况结束时你都要做一些类似 关闭一个文件 或者发送 http 响应 的事情。我们可以使用 defer 来节省时间,而不是写一堆繁复的函数调用。

以下就是一个反面教材 的程序范例。

if cond1 {
    ...
    fs.Close(file)
} else if cond2 {
    ...
    fs.Close(file)
} else if cond3 {
    ...
    fs.Close(file)

} else {
    ...
    fs.Close(file)
}

接下来就是一个 教科书 程序范例。

defer fs.Close(file)

if cond1 {
    ...
} else if cond2 {
    ...
} else if cond3 {
    ...
} else {
    ...
}

☛ 把函数看作类型

在 Go 中,函数也是一种类型。如果两个函数接收同样类型的参数,并返回同样类型的值,那么就说这两个函数的类型相同。

比如,addsubstract 都接收两个 int 类型的参数,并且都返回一个 int 类型的值,那么它们就是同样的类型。

前面我们已经看过一些函数的定义,例如内置函数 append 是这样定义的

func append(slice []Type, elems ...Type) []Type

因此任何这样定义的函数,比如,prepend 函数在切片( 可扩展的数组 )的开头添加元素

func prepend(slice []Type, elems ...Type) []Type

那么,就可以说  append 和 prepend 具有相同的类型

func (slice []Type, elems ...Type) []Type

在 Go 中,函数体与函数类型没有任何关系。但是这种类型的作用是什么呢?

如果要将函数作为参数传递给另一个函数,或者当函数的返回值是另一个函数时,就需要在函数定义时指定返回类型。

下面来定义两个函数 addsubtract,看看他们如何比较。

https://play.golang.org/p/LxOPPRvq4Ta

我们可以看到 addsubtract 函数具有相同的类型 func(int, int) int

再来定义一个函数,它接收两个整型和一个函数作为参数,这个函数需要对两个数字进行数学运算。我们将使用 addsubtract 函数作为该函数的第三个参数。

https://play.golang.org/p/z3lwQPAhNLJ

在上面的程序中,我们已经定义了函数 calc,它接受 int型参数 a 和 b 以及第三个参数类型为func(int, int) int的参数f。然后我们就可以调用以 ab作为参数的f 函数。

我们可以创建一个类型别名使代码可读性更好 。我们可以这么改写上面的程序

https://play.golang.org/p/RvayW75C8yy

我们也可以用其他的方式编写上面的程序calc函数,而非让第三个参数作为函数接受如addsubstract这样的string命令,并返回一个基于命令的匿名函数,然后再执行。

☛ 把函数当作变量的值(匿名函数)

在 go 语言中,函数也可以被看作值类型。这就意味着你可以把一个函数赋值给变量。

https://play.golang.org/p/UoorB5zCGb2

在上面的程序中,我们创建了一个全局变量 add 并且给它赋值了一个新创建的函数。 我们使用 Go 的类型推断来获取匿名函数的类型(因为我们没有提到* add *的类型)。 在这种情况下,add 是一个匿名函数,因为它是从一个没有名称的函数创建的。

☛ 立即调用函数

如果你用过 JavaScript,你应该会知道什么是立即调用函数, 不过不知道也没关系。在 Go 中,我们可以创建 定义并同时执行 的匿名函数。

我们再开看下前面这个函数定义的例子

add 是一个匿名函数。有些人可能说它不是真正的匿名函数,因为我们仍然可以在 main 函数中的任何地方用 add 来指代这个函数(在其他情况下,可能是从程序中的任何地方)。而不是前面所说的立即调用或执行。所以,我们需要修改一下。

https://play.golang.org/p/yxRJr51OxzY

我们来看一下这个函数的定义。第一部分从 func} 是在定义函数,然后 (3,5) 是在执行这个函数。因此,sum 是这个函数执行的返回值。因此,以上程序返回的结果如下

5+3 = 8

立即调用函数也可以在 main 函数之外全局使用。当你需要用 函数 执行的返回值创建全局变量时,就可以使用立即调用函数。

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://medium.com/rungo/the-anatomy-of-...

译文地址:https://learnku.com/go/t/28083

本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
讨论数量: 1

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!