第三天,已经分享了Go语言的流程控制的相关内容。

随着编程经验的提高,很多同学常常会发现自己写的代码有很多重复,一旦决定要更新代码,就必须将每个重复的部分修改,费时费力。因此,大家希望可以避免代码的重复,第四天内容可以帮助读者消除重复,使程序更短、更易读、更容易更新。

很久以前的人们想要制作果汁,每次都用手捏,非常麻烦,后来有人发明了榨汁机,人们只要把水果放进去,榨汁机就会把果汁榨出来。函数的功能就像榨汁机一样,帮助人们重复的做任务。函数是组织好的、可重复使用的执行特定任务的代码块。它可以提高应用程序的模块性和代码的重复利用率。Go语言从设计上对函数进行了优化和改进,让函数使用起来更加方便。因为Go语言的函数本身可以作为值进行传递,既支持匿名函数和闭包又能满足接口,所以Go语言的函数属于一等公民。

学习目标

(1)能够掌握函数声明

(2)能够掌握变量作用域

(3)能够掌握函数变量(函数作为值)

(4)能够掌握匿名函数

(5)能够掌握闭包

(6)能够掌握可变参数

(7)能够掌握递归函数

知识讲解

Ø 函数声明

普通函数需要先声明才能调用,一个函数的声明包括参数和函数名等。编译器通过声明才能了解函数应该怎样在调用代码和函数体之间传入参数和返回参数。语法格式如下所示。

1. 函数定义解析

func:函数关键字。函数由 func 开始声明。

funcName:函数名。函数名和参数列表一起构成了函数签名。函数名由字母、数字和下画线组成。函数名的第一个字母不能为数字。在同一个包内,函数不能重名。

parametername type:参数列表。定义函数时的参数叫作形式参数,形参变量是函数的局部变量;函数被调用时,可以将值传递给参数,这个值被称为实际参数。参数列表指定的是参数类型、顺序及参数个数。参数是可选的,也就是说函数可以不包含参数。

output1 type1, output2 type2:返回值列表。返回值返回函数的结果,结束函数的执行。Go语言的函数可以返回多个值。返回值可以是返回数据的数据类型,也可以是变量名+变量类型的组合。函数声明时有返回值,必须在函数体中使用return语句提供返回值列表。如果只有一个返回值并且没有声明返回值变量,那么可以省略包括返回值的括号。return后的数据,要保持和声明的返回值类型、数量、顺序一致。如果函数没有声明返回值,函数中也可以使用return关键字,用于强制结束函数。

函数体:函数定义的代码集合,是能够被重复调用的代码片段。

2. 参数类型简写

在参数列表中,如果有多个参数变量,则以逗号分隔;如果相邻变量是同类型,则可以将类型省略。语法格式如下所示。

Go语言的函数支持可变参数。接受变参的函数是有着不定数量的参数的。语法格式如下所示。

arg ...int告诉Go这个函数接受不定数量的参数。注意,这些参数的类型全部是int。在函数体中,变量arg是一个int的slice(切片)。

Ø 变量作用域

作用域是变量、常量、类型、函数的作用范围。

在函数体内声明的变量称之为局部变量,它们的作用域只在函数体内,生命周期同所在的函数。参数和返回值变量也是局部变量。

在函数体外声明的变量称之为全局变量,全局变量可以在整个包甚至外部包(被导出后)使用,全局变量的生命周期同 main()。

全局变量可以在任何函数中使用。Go 语言程序中全局变量与局部变量名称可以相同,但是函数内的局部变量会被优先考虑。

函数中定义的参数称为形式参数,形式参数会作为函数的局部变量来使用。

为了更直观的理解作用域,下面通过一个案例分析,如例所示。

Ø 函数变量(函数作为值)

在Go语言中,函数也是一种类型,可以和其他类型(如int32、float等等)一样被保存在变量中。

在Go语言中可以通过type来定义一个自定义类型。函数的参数完全相同(包括参数类型、个数、顺序),函数返回值相同。

函数变量的使用步骤及意义如下。

l 定义一个函数类型

l 实现定义的函数类型

l 作为参数调用

函数变量的用法类似接口的用法。

Ø 匿名函数

Go语言支持匿名函数,即在需要使用函数时再定义函数。匿名函数没有函数名,只有函数体,函数可以被作为一种类型被赋值给变量,匿名函数也往往以变量方式被传递。

匿名函数经常被用于实现回调函数、闭包等。语法格式如下所示。

1. 在定义时调用匿名函数

使用方式如例所示。

2. 将匿名函数赋值给变量

使用方式如例所示。

3. 匿名函数的用法——作回调函数

使用方式如例所示。

Ø 闭包

1. 闭包的概念

闭包并不是什么新奇的概念,它早在高级语言开始发展的年代就产生了。闭包(Closure)是词法闭包(Lexical Closure)的简称。

闭包是由函数和与其相关的引用环境组合而成的实体。在实现深约束时,需要创建一个能显式表示引用环境的东西,并将它与相关的子程序捆绑在一起,这样捆绑起来的整体被称为闭包。函数+引用环境=闭包。

闭包只是在形式和表现上像函数,但实际上不是函数。函数是一些可执行的代码,这些代码在函数被定义后就确定了,不会在执行时发生变化,所以一个函数只有一个实例。

闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。闭包在某些编程语言中被称为Lambda表达式。

函数本身不存储任何信息,只有与引用环境结合后形成的闭包才具有“记忆性”。函数是编译器静态的概念,而闭包是运行期动态的概念。

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

2. 闭包的优点

(1)加强模块化。闭包有益于模块化编程,它能以简单的方式开发较小的模块,从而提高开发速度和程序的可复用性。和没有使用闭包的程序相比,使用闭包可将模块划分得更小。

比如要计算一个数组中所有数字的和,这只需要循环遍历数组,把遍历到的数字加起来就行了。如果现在要计算所有元素的积,又或者要打印所有的元素呢?解决这些问题都要对数组进行遍历,如果是在不支持闭包的语言中,程序员不得不一次又一次重复地写循环语句。而这在支持闭包的语言中是不必要的。这种处理方法多少有点像回调函数,不过要比回调函数写法更简单,功能更强大。

(2)抽象。闭包是数据和行为的组合,这使得闭包具有较好抽象能力。

(3)简化代码。一个编程语言需要以下特性来支持闭包。

l 函数是一阶值(First-class value,一等公民),即函数可以作为另一个函数的返回值或参数,还可以作为一个变量的值。

l 函数可以嵌套定义,即在一个函数内部可以定义另一个函数。

l 允许定义匿名函数。

l 可以捕获引用环境,并把引用环境和函数代码组成一个可调用的实体;

没有使用闭包进行计数的代码。

使用闭包函数实现计数器。

闭包案例。

由于闭包函数“捕获”了和它在同一作用域的其他常量和变量。所以当闭包在任何地方被调用,闭包都可以使用这些常量或者变量。它不关心这些变量是否已经超出作用域,只要闭包还在使用它,这些变量就依然存在。

Ø 可变参数

如果一个函数的参数,类型一致,但个数不定,可以使用函数的可变参数。语法格式如下所示。

该语法格式定义了一个接受任何数目、任何类型参数的函数。这里特殊的语法是三个点“...”,在一个变量后面加上三个点后,表示从该处开始接受不定参数。

当要传递若干个值到不定参数函数中得时候,可以手动书写每个参数,也可以将一个slice传递给该函数,通过"..."可以将slice中的参数对应的传递给函数。

计算学员考试总成绩及平均成绩。

使用可变参数注意如下细节。

l 一个函数最多只能有一个可变参数

l 参数列表中还有其他类型参数,则可变参数写在所有参数的最后

Ø 递归函数

在函数内部,可以调用其他函数。如果一个函数在内部调用自身,那么这个函数就是递归函数。递归函数必须满足以下两个条件。

(1)在每一次调用自己时,必须是(在某种意义上)更接近于解;

(2)必须有一个终止处理或计算的准则。

下面通过案例来理解递归函数的作用。

计算阶乘n! = 1 x 2 x 3 x ... x n,用函数fact(n)表示,可以看出:fact(n) = n! = 1 x 2 x 3 x ... x (n-1) x n = (n-1)! x n = fact(n-1) x n。所以,fact(n)可以表示为n x fact(n-1),只有n=1时需要特殊处理。

阶乘。

递归的计算过程如下所示。

使用递归需要注意如下事项。

l 递归函数的优点是定义简单,逻辑清晰。理论上,所有的递归函数都可以用循环的方式实现,但循环的逻辑不如递归清晰。

l 使用递归函数需要注意防止栈溢出。在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈,每当函数返回,栈就会减一层。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出。

l 使用递归函数的优点是逻辑简单清晰,缺点是过深的调用会导致栈溢出。

Go语言视频教程,持续更新中……