在学习函数前,我们已经多次使用了函数,比如 main 函数、Print 函数。本章会详细介绍 Go 函数的使用。

6.1 什么是函数

在介绍变量的时候,说到“变量就是给数据起个名字”。同样可以理解为,函数就是给代码块起了一个名字,以便我们复用某些代码块。

一个完整的函数声明包括 func 关键字、函数名、形参列表、返回值类型列表和函数体。使用 func 关键字声明函数,函数无需参数或无返回值时,形参列表和返回值类型列表可以省略。形参列表需要描述参数名及参数类型,所有形参均为函数块局部变量,也就是说作用域仅在函数块内。返回值类型列表需要定义函数返回的数据类型。Go 函数格式定义示例:

func FuncName(/*形参列表*/) (/*返回值类型列表*/) {
    // 函数体
    ...
    return v1, err //返回多个值
}

我们用之前接触过的 addition 函数作为示例:两个 int 类型的数字相加,返回一个 int 类型的结果:

func addition(x int, y int) int {
    return x + y
}

可能你已经发现了,在 Go 中,不论定义函数还是定义变量,都是名字在前,类型在后。这种格式,比较符合人类的思维模式。就像我们登记个人信息,总是个人姓名在前,然后是性别等其他属性。

从声明 addition 函数的这段代码可以看出,使用 func 关键字定义函数,是函数的开端。 addition 是函数名,用于在其他地方调用该函数。x,y是函数的预期变量,也就是形参,类型是 int。最后返回一个 int 类型的值。{ 表示函数体的开始,} 表示函数的结束。如果定义函数时声明了返回值,则函数体必须以 return 语句结尾。

6.2 无参无返回值

在定义函数的时候,既没有声明参数列表,也没有返回值类型列表的函数,称为无参无返回值函数。main 函数就是这样的函数。

func main() {
  ...
}

main 函数是整个程序的入口,通常代码会从 main 函数体的第一行开始执行,一直到最后一行。

6.3 有参无返回值

在定义函数时形参列表不为空:

func HelloWorld(h string) {
    fmt.Println(h)
}

定义函数时,形参列表里的参数被称为形参;在调用函数时,传递的参数叫实参。我们在调用函数时,通过实参把数据传递给函数的形参。

使用函数名加括号的形式调用函数,如果函数定义了形参,则在括号里传入相同数据类型、相同数量的实参,我们演示一下对 HelloWorld 函数的调用:

package main

import "fmt"

func HelloWorld(h string)  {
    fmt.Println(h)
}
func main()  {
    HelloWorld("Hello World")
}

函数的多形参定义:

func Players(c string, j string, k string) {
    fmt.Println(c, j, k)
} 
// or
func Players(c, j, k string) {
    fmt.Println(c, j, k)
}

func main()  {
    Players("Curry", "James", "Kobe")
}

6.4 单个返回值

基本上函数都需要形参和返回值,例如,判断数字是奇数还是偶数。在编写函数前,先考虑一下函数的功能、输入的内容和返回的值,这有助于更好的设计函数,也利于测试。编程语言通常提供多种解决问题的方法,因此,从函数的设计角度看,明确函数的输入和输出会使函数的实现过程更简单。

为了实现判断奇偶的函数,可以进行以下假设:

  • 接收单个参数,且参数是整数类型;

  • 返回一个布尔值,如果是偶数,返回 true,否则返回 false。

在了解函数预期的输入和输出类型后,再花点时间考虑下函数的结构,现在可以编写函数了。

func isEven(i int) bool {
    return
}

isEven 函数接受整数输入,并返回一个布尔值。该函数用来判断整数值是否为偶数,是就返回 true。实现此功能的方法之一是使用模运算 %。运算符左边的整数,除以运算符右边的整数,然后返回余数。现在可以编写函数:

func isEven(i int) bool {
    return i%2 == 0
}

在设计函数时,程序员仅受语言和想象力的限制。但在编写更复杂的软件或进行团队协作时,需要注意以下重要事项:

  • 一个函数做好一件事情。这有利于软件更改,也方便进行功能测试和软件整体测试。

  • 可维护。如果你和团队成员协同工作,你认为这个函数易于维护和理解吗?如果不是这样,则函数可能过于复杂或需要一些文档。请记住,你可能在一年后的某个深夜调用此函数!

  • 性能。有时函数的性能也至关重要。函数定义清晰,方便针对函数的性能做客观的基准测试。

调用 isEven 函数:

package main

import "fmt"

func isEven(x int) bool {
 return x%2 == 0
}
func main() {
 fmt.Printf("%v\n", isEven(3))
 fmt.Printf("%v\n", isEven(4))
}

使用函数名调用函数,并传入所需的参数。在上面的示例中,isEven 函数被调用了两次,结果会输出到终端上。调用函数的次数没有限制。

6.5 多个返回值

Go 函数在定义时,可以声明返回多个值。来看以下示例,声明一个不接收任何值,并返回 string 和 int 类型的两个值:

func player() (string, int) {
    s := "Curry"
    i := 30
    return s, i
}

在调用 player 函数时,可以把返回的值直接分配给变量:

func main() {
    name, number := player()
    fmt.Printf("%v %v\n", name, number)
}

6.6 可变参函数

可变参函数就是接收的实参数量可以是0个或多个。在 Go 中,可以传递与函数定义时指定的数据类型相同的不定数量的参数,语法是三个点(…)。定义接收类型为 int 的任意数量参数的函数语法如下:

func SumNumbers(numbers ...int) int {

}

SumNumbers 函数接收一个或多个 int,并返回一个 int,可用于对任意数量的整数求和。实参在传递时会被转换成切片。SumNumbers 对任意数量的整数求和的方法如下:

func SumNumber(numbers ...int) int {
 total := 0
 for _, number := range numbers {
  total += number
 }
 return total
}

计算1,2,3,4,5,6,7的和:

package main

import "fmt"

func sumNumber(numbers ...int) int {
 total := 0
 for _, number := range numbers {
  total += number
 }
 return total
}
func main() {
 fmt.Println(sumNumber(1, 2, 3, 4, 5, 6, 7))
}

需要注意的是,在定义形参时,可变形参必须在形参列表的最后。

6.7 命名返回值

在声明函数时,可以给函数返回值定义名称:

func sayHello() (x, y string) {
 x = "Hello"
 y = "World"
 return
}

6.8 递归函数

递归是指函数可以直接或间接调用自身,直到满足特定条件。虽然递归是一个简单的概念,但递归函数是重要的编程构造。递归函数通常有相同的结构:一个特定条件和一个递归体。特定条件就是根据传入的参数判断是否需要停止递归,而递归体则是函数自身所做的一些处理。要使用递归函数,函数须将自身作为终止语句的结果值进行调用。

6.8.1 普通函数的执行流程

小明一顿饭可以吃5个肉包子,我们通过普通函数来满足他的胃口:

package main

import "fmt"

func Feed5(meatBun int) {
 fmt.Printf("Xiao Ming ate %d steamed buns!\n", meatBun)
}

func Feed4(meatBun int) {
 Feed5(meatBun - 1)
 fmt.Printf("Xiao Ming ate %d steamed buns!\n", meatBun)
}

func Feed3(meatBun int) {
 Feed4(meatBun - 1)
 fmt.Printf("Xiao Ming ate %d steamed buns!\n", meatBun)
}

func Feed2(meatBun int) {
 Feed3(meatBun - 1)
 fmt.Printf("Xiao Ming ate %d steamed buns!\n", meatBun)
}

func Feed1(meatBun int) {
 Feed2(meatBun - 1)
 fmt.Printf("Xiao Ming ate %d steamed buns!\n", meatBun)
}

func main() {
 Feed1(5)
 fmt.Println("Xiao Ming is full!")
}

执行这段代码,返回如下内容:

>go run meatbun.go
Xiao Ming ate 1 steamed buns!
Xiao Ming ate 2 steamed buns!
Xiao Ming ate 3 steamed buns!
Xiao Ming ate 4 steamed buns!
Xiao Ming ate 5 steamed buns!
Xiao Ming is full!

我们分析一下这段代码的执行流程:

  1. main 函数是程序的入口,首先会执行 main 函数中的第一条语句,这里调用了 Feed1 函数,传给 Feed1 函数的实参是5;

  2. Feed1 函数被执行,Feed1 函数的第一行代码调用了 Feed2 函数,传给 Feed2 函数的实参是5-1,也就是4;

  3. Feed2 函数被执行,Feed2 函数的第一行代码调用了 Feed3 函数,传给 Feed3 函数的实参是4-1,也就是3;

  4. Feed3 函数被执行,Feed3 函数的第一行代码调用了 Feed4 函数,传给 Feed4 函数的实参是3-1,也就是2;

  5. Feed4 函数被执行,Feed4 函数的第一行代码调用了 Feed5 函数,传给 Feed5 函数的实参是2-1,也就是1;

  6. Feed5 函数被执行,打印:Xiao Ming ate 1 steamed buns!;

  7. Feed4 函数的第一行代码执行结束,开始执行第二行代码,打印:Xiao Ming ate 2 steamed buns!;

  8. Feed4 函数调用结束,Feed3 函数的第二行代码开始执行,打印:Xiao Ming ate 3 steamed buns!;

  9. Feed3 函数调用结束,Feed2 函数的第二行代码开始执行,打印:Xiao Ming ate 2 steamed buns!;

  10. Feed2 函数调用结束,Feed1 函数的第二行代码开始执行,打印:Xiao Ming ate 3 steamed buns!;

  11. Feed1 函数调用结束,mian 函数的第二行代码开始执行,打印:Xiao Ming is full!。

伴随函数的执行过程,小明吃完了5个包子,小明吃饱了。

我们使用了5个结构相同的函数,来实现这一过程,这使代码看起来冗余且不优雅。

6.8.2 递归函数的执行流程

小明一顿饭可以吃5个肉包子,我们通过递归函数来满足他的胃口:

package main

import (
 "fmt"
)

func Feed(meatBun int) {
 if meatBun == 1 {
  fmt.Printf("Xiao Ming ate %d steamed buns!\n", meatBun)
        return
 }
 Feed(meatBun - 1)

 fmt.Printf("Xiao Ming ate %d steamed buns!\n", meatBun)
}
func main() {
 Feed(5)
 fmt.Println("Xiao Ming is full!")
}

执行这段代码,返回的结果如下:

>go run meatbun.go
Xiao Ming ate 1 steamed buns!
Xiao Ming ate 2 steamed buns!
Xiao Ming ate 3 steamed buns!
Xiao Ming ate 4 steamed buns!
Xiao Ming ate 5 steamed buns!
Xiao Ming is full!

执行结果和普通函数相同,但代码量减少了很多,并且可读性也提高了。

把每一步的执行过程拆分,是和普通函数的执行过程是一样的。在递归函数中,对函数自身进行调用,并有一个判断语句,达到某个条件时,退出执行。

使用递归函数完成1到100的加法:

package main

import "fmt"

func Add(num int) int {
 if num == 100 {
  return 100
 }

 return num + Add(num+1)
}

func main() {
 fmt.Println(Add(1))
}

6.9 将函数作为类型传递

也许你从其他 Go 相关的学习资料中看到过这句话:函数是 Go 中的一等公民。本质上,Go 将函数视为类型,因此可以将函数分配给变量,也可以将函数作为其他函数的形参或返回值。在下面这个示例中,我们将一个函数分配给了变量 fn,并在作用域内调用了该变量:

package main

import "fmt"

func main() {
 fn := func() {
  fmt.Println("function called")
 }
 fn()
}

代码释义:

  • 使用短变量声明的方式,将函数作为值分配给变量 fn;

  • 函数被声明并打印”function called”;

  • 变量名后的( ),表示对该函数的调用。

扩展上面的示例,定义一个参数是返回字符串的函数的函数,并将 fn 作为参数传递给该函数:

package main

import "fmt"

func anotherFunction(f func() string) string {
    return f()
}

func main()  {
    fn := func() string {
        return "function called"
    }

    fmt.Println(anotherFunction(fn))
}

函数的参数至少有一个是函数类型,这种函数就是回调函数。回调函数的一个重要意义在于实现多态。何为多态?调用同一个接口可以实现不同的表现,这就是多态。下面的示例,我们通过回调函数,实现简单的计算器功能:

package main

import "fmt"

type FuncType func(int, int) int

func Add(a, b int) int {
 return a + b
}

func Minus(a, b int) int {
    return a - b
}

func Multi(a, b int) int {
    return a * b
}

func Divide(a, b int) int {
    return a / b
}

func Calc(a, b int, fc FuncType) int {
    return fc(a, b)
}

func main() {
    numbers := Calc(6, 3, Divide)
 fmt.Println(numbers)
}

在这个示例中,main 只是调用了 Calc 函数,就实现了加减乘除的功能。

6.10 匿名函数和闭包

匿名函数是指在定义函数时,不需要指定函数名称。在 Go 语言里,所有的匿名函数都是闭包。所谓闭包就是一个函数“捕获”了和它同在一个作用域下定义的常量或变量(关于作用域我们在“变量”的章节已有介绍)。这就意味着不论闭包在程序的哪个位置被调用,闭包都能够使用这些常量或变量。它不关心这些变量或常量是否超出了其作用域。所以只要闭包还在使用,这些变量或常量就会一直存在。

在“6.9将函数作为类型传递”小节里,我们定义了一个匿名函数,并把它赋值给了 fn。定义匿名函数有多种方式:

// 通过类型推动,定义匿名函数并把它赋值给变量
anonymity1 := func() {
    ...
}

// 自定义函数类型,并进行赋值
type FuncType func()
var anonymity1 FuncType = func() {
    ...
}

// 定义匿名函数并调用
func() {
    ...
}()  // 圆括号代表调用此匿名函数

// 定义有形参的匿名函数并调用
func(num int) {
    ...
}(1)

// 定义有参有返回值的匿名函数
numbers := func(num int) int {
    ...
    return num
}(1)

我们再来观察下闭包的变量使用特点:

package main

import (
 "fmt"
)

func main() {
 name := "Curry"
 num := 30
 fmt.Printf("闭包外部:name %s, num %d\n", name, num)
 func() {
  name = "Durant"
  num = 15
  fmt.Printf("闭包内部:name %s, num %d\n", name, num)
 }()

 fmt.Printf("闭包外部:name %s, num %d\n", name, num)

}

执行这段代码,name 和 num 的值被分别修改成了“Durant”和15,这是因为闭包以引用的方式捕获外部变量,闭包内部把外部定义的变量值进行修改后,对外部也是有影响的。

闭包不会在意它捕获的变量是否超出了其作用域,,并且只要闭包还在使用,这些变量就会一直存在,请看下面的示例:

package main

import "fmt"

func general() int {
    var x int
    x++
    return x * x
}

// 定义一个名为 anonymity 的函数,返回值是一个匿名函数,这个匿名函数返回一个 int 类型
func anonymity() func() int  {
    var x int
    return func() int {
        x++
        return x * x
    }
}


func main() {
 fmt.Println("general", general())
    fmt.Println("general", general())
    fmt.Println("general", general())
    fmt.Println("general", general())

 // anonymity 的返回值是一个函数,把它赋值给fn变量,并打印出 fn 的调用结果
 fn := anonymity()
    fmt.Println(fn())
    fmt.Println(fn())
    fmt.Println(fn())
    fmt.Println(fn())
}

试着执行这段代码,看看返回结果是否是你预期的。

6.11 总结

本章介绍了函数的概念,可以概括为函数就是对一段代码的引用。介绍了函数的定义和用法。在编程过程中,使用和编写函数是很常见的操作。

文档更新时间: 2021-11-08 10:03   作者:admin