[奔跑的 Go] 教程七、深入学习 Go 语言中的函数
如同 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-else
或 case
语句,在每个情况结束时你都要做一些类似 关闭一个文件
或者发送 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 中,函数也是一种类型。如果两个函数接收同样类型的参数,并返回同样类型的值,那么就说这两个函数的类型相同。
比如,add
和 substract
都接收两个 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 中,函数体与函数类型没有任何关系。但是这种类型的作用是什么呢?
如果要将函数作为参数传递给另一个函数,或者当函数的返回值是另一个函数时,就需要在函数定义时指定返回类型。
下面来定义两个函数 add
和 subtract
,看看他们如何比较。
https://play.golang.org/p/LxOPPRvq4Ta
我们可以看到 add
和 subtract
函数具有相同的类型 func(int, int) int
。
再来定义一个函数,它接收两个整型和一个函数作为参数,这个函数需要对两个数字进行数学运算。我们将使用 add
和 subtract
函数作为该函数的第三个参数。
https://play.golang.org/p/z3lwQPAhNLJ
在上面的程序中,我们已经定义了函数 calc
,它接受 int
型参数 a
和 b
以及第三个参数类型为func(int, int) int
的参数f
。然后我们就可以调用以 a
和b
作为参数的f
函数。
我们可以创建一个类型别名使代码可读性更好 。我们可以这么改写上面的程序
https://play.golang.org/p/RvayW75C8yy
我们也可以用其他的方式编写上面的程序calc
函数,而非让第三个参数作为函数接受如add
或substract
这样的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 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
推荐文章: