第5章 函数与方法 函数是执行计算的命名语句序列。将一段代码封装为函数并在需要的位置进行调用, 不仅可以实现代码的重复利用,更重要的是可以保证代码完全一致。 5.1 函数的定义与使用 5.1.1 函数的定义 在Go语言中,定义函数的语法格式如下: func funcName( [arg argType] ) [returnType] { function body } 其中,funcName是函数名,arg是形式参数,argType是形式参数的类型,returnType是返 回值的类型。Go语言使用关键字func定义函数(Function)。func后面有一个空格,然后 是函数名,接下来是一对小括号,小括号里是可选的形式参数及其对应的类型,小括号后面 是可选的返回值及其类型,最后是一对大括号括起来的函数体。函数名、形式参数类型和返 回值类型三者共同构成了函数签名。形参名以及返回值的变量名不属于函数签名。定义函 数时还需要注意如下几个问题: (1)一个函数即使不需要接收任何参数,也必须保留一对小括号; (2)如果一个函数没有返回值,则返回值可省略(包括小括号)。 func f1() { } //没有形参,没有返回值 func f2(a int) { } //一个形参,没有返回值 //两个形参,一个返回值,返回一个整数 func f3(a int, b int) int { return 1 } func f4(x float64) (y int, z float64) { //一个形参,两个返回值 return 1, 2 } 举例:自定义函数bigger()。 func main() { var a, b int = 10, 20 //定义局部变量a 和b var result int result = bigger(a, b) 44 Go语言程序设计教程 fmt.Printf("较大值: %d\n", result) } /*函数的返回值是两个形式参数(Formal Parameter)x 和y 中的较大者*/ func bigger(x, y int) int { //函数签名bigger(int, int) int if x > y { return x } return y } 上述代码的输出结果: 较大值: 20 当一组形参或返回值的数据类型相同时,可不必逐个为它们标明数据类型。下面两个 函数声明是等价的,即它们的函数签名相同。 func f(a, b int, m, n string) { … } func f(a int, b int, m string, n string) { … } 举例: func main() { //输出数据类型使用格式控制符%T,Type fmt.Printf("%T\n", add) fmt.Printf("%T\n", sub) fmt.Printf("%T\n", first) fmt.Printf("%T\n", zero) }f unc add(x int, y int) int { return x + y } func sub(x, y int) (z int) { z = x - y; return } func first(x int, _ int) int { return x } func zero(int, int) int { return 0 } 上述代码的输出结果: func(int, int) int func(int, int) int func(int, int) int func(int, int) int 5.1.2 函数的调用 函数定义完毕并不能自动运行,只有被调用(Call)时才能运行。下面的代码用整数10 和20调用bigger()函数,该函数的返回值被赋值给变量result。 result = bigger(10, 20) 上述调用bigger()函数时使用的整数10和20是实际参数(ActualParameter),简称实 参。形参没有具体的值,形参的值来自实参。调用函数时必须按照声明的顺序为所有的形 参赋值。Go语言的形参不能带有默认值,这点与Python语言等不同。 第5章 函数与方法 45 实参通过值传递的方式为形参赋值,形参是实参的一个副本,对形参执行操作通常不会 影响实参的值。但是,如果实参是引用类型,如指针、切片slice、投影map,则可以通过形参 修改实参的值。 举例: func modify(z *int) { //形参z 是指针类型 *z = 20 }f unc main() { var x int = 10 fmt.Printf("调用函数前x 的值= %d\n", x) modify(&x) //实参&x 是引用类型 fmt.Printf("调用函数后x 的值= %d\n", x) } 上述代码的输出结果: 调用函数前x 的值= 10 调用函数后x 的值= 20 5.1.3 函数的返回值 通常,定义一个函数是希望它能够返回一个或多个计算结果,这在Go语言中是通过关 键字return实现的。无论return语句出现在函数的什么位置,一旦被执行,它都会立即结 束函数的执行过程。Go语言支持函数返回多个值。 func twoRetValues() (int, int) { //返回值必须与return 语句相匹配 return 1, 2 //不能写作(1, 2) }f unc main() { a, b := twoRetValues() fmt.Println(a, b) } 上述代码的执行结果: 1 2 与形参名一样,Go语言支持对返回值进行命名,此时需要在函数体中显式地使用 return语句返回。 func namedRetValues() (x, y int) { //两个返回值被命名为x 和y x = 2 y = 1 return //return 语句可为空,等价于return x, y }f unc main() { a, b := namedRetValues() fmt.Println(a, b) } 46 Go语言程序设计教程 上述代码的输出结果: 2 1 main()函数是一种特殊类型的函数,它不接收任何参数,也没有返回值。main()函数 是一个可执行程序的入口。Go编译器会自动调用main()函数,因此不需要显式地调用它。 main包是一个特殊的包。一个可执行程序必须拥有一个main包,而且在该包中必须有且 只能有一个main()函数。 与main()函数类似,init()函数不接收任何参数,也没有返回值。每个包中可以包含一 个或多个init()函数。init()函数按照出现的先后顺序依次执行。init()函数也不需要显式 地调用,Go编译器会自动调用它。init()函数在main()函数之前执行,它的主要用途是初 始化全局变量。如果发生多个包的嵌套引用,则最后导入的包会最先执行其包含的init() 函数。 5.2 lambda 函数 lambda函数又称为匿名函数。匿名函数没有函数名,只有函数体。定义匿名函数的语 法格式如下: func ( [arg argType] ) [returnType] { function body } (1)在定义的同时调用匿名函数 func main() { func(x int) { //函数嵌套 fmt.Println(x) }(10) } 上述代码的输出结果: 10 (2)将匿名函数赋值给一个变量 func main() { f := func(x int) { //将匿名函数赋值给变量f fmt.Println(x) } f(10) //使用f()调用匿名函数 } 上述代码的输出结果: 10 再举一个例子: 第5章 函数与方法 47 func main() { add := func(x int) int { return x + 1 } fmt.Println(add(10)) } 上述代码的输出结果: 11 5.3 闭包 可以将闭包(Closure)理解为定义在一个函数内部的匿名函数。本质上,闭包是连接匿 名函数内部与外部的桥梁。闭包对外部环境中变量的引用过程称为“捕获”。闭包可以用一 个简单的公式表示如下: 闭包= 匿名函数+ 引用的外部环境 举例: func main() { i := 42 //声明一个整型变量i f := func() { //将匿名函数赋值给变量f j := i / 2 //访问外部变量i fmt.Println(j) } f() //输出21 } 上述代码创建了包含整型变量i的匿名函数f的闭包。函数f可以直接访问变量i,这 是闭包的属性。 func f() func() int { i := 0 return func() int { //函数f()的返回值是一个匿名函数 i += 1 return i } }f unc main() { a := f() //闭包a b := f() //闭包b fmt.Println(a()) //输出1 fmt.Println(b()) //输出1 b() fmt.Println(a()) //输出2 fmt.Println(b()) //输出3 } 当需要创建一个对状态进行封装的函数时,就需要使用闭包。闭包可以实现很多高级 48 Go语言程序设计教程 的功能,如创建一个生成器,限于篇幅,本书不再讲述。 5.4 defer 语句 defer将其后的语句进行延迟处理。在defer语句所属的函数返回之前,将延迟处理语 句按照后进先出(LastInFirstOut,LIFO)的顺序执行。 举例: func main() { defer fmt.Printf("%s\n", "first") defer fmt.Printf("%s ", "second") defer fmt.Printf("%s ", "third") //最后进入,最先出来 fmt.Println("exit") } 上述代码的输出结果: exit third second first defer语句一般用于释放某些已分配的系统资源,如关闭文件。下面定义函数fileSize() 用于打开并获取一个文件的大小。 举例: func fileSize(fileName string) int64 { f, err := os.Open(fileName) if err != nil { return 0 } //延迟调用Close()函数,此刻不会调用它 defer f.Close() info, err := f.Stat() //统计Statistics if err != nil { return 0 //函数返回前,会调用Close()函数关闭文件 } size := info.Size() return size //函数返回前,会调用Close()函数关闭文件 } 在main()主函数中调用fileSize()函数。 func main() { size := fileSize("test.txt") fmt.Printf("File size = %d", size) } 上述代码的输出结果: File size = 25 在上述例子中,如果没有defer语句,则在fileSize()函数中后两个return语句的前面都 第5章 函数与方法 49 必须添加语句f.Close()关闭文件,以释放系统资源。读者是否能通过上述例子,总结出 defer语句的优点? 5.5 递归函数 什么是算法? 简单地说,算法是解决问题的方法与步骤。递归算法(RecursiveAlgorithm) 的核心思想是分治策略。分治是“分而治之”(DivideandConquer)的意思。分治策略将一 个复杂的问题反复分解为两个或更多个相同的或相似的子问题,直至这些子问题可以直接 求解,最后将子问题的解合并起来,就能得到原问题的解,如图5-1所示。 图5-1 分治策略 一个函数在其函数体内部调用它自身,这种函数叫作递归函数。递归函数使用了分治 策略,其由终止条件和递归条件两部分组成。下面定义一个计算阶乘的函数factorial(n): func factorial(n int) int { //阶乘factorial if n <= 1 { //终止条件 return 1 } return n * factorial(n-1) //递归条件 }f unc main() { var n int = 5 fmt.Println(factorial(n)) //输出120 } 调用上述定义的factorial(n)函数计算5的阶乘,其执行流程如下所示: factorial(5) = 5 * factorial(4) = 5 * 4 * factorial(3) = 5 * 4 * 3 * factorial(2) = 5 * 4 * 3 * 2 * factorial(1) = 5 * 4 * 3 * 2 * 1 = 120 50 Go语言程序设计教程 下面定义一个计算斐波那契数列(Fibonacci)的递归函数fib(n): func fib(n int) int { if n <= 1 { //终止条件 return n } return fib(n-1) + fib(n-2) //递归条件 }f unc main() { var n int = 10 for i := 0; i <= n; i++ { fmt.Printf("%d ", fib(i)) } fmt.Println() } 上述代码的输出结果: 0 1 1 2 3 5 8 13 21 34 55 5.6 可变长度参数 与Python语言类似,Go语言也支持可变长度参数。也就是说,一个函数可以接收任意 数量的实际参数。 举例: func multiply(args …int) int { //形参类型是int 型 z := 1 for _, arg := range args { z *= arg } return z }f unc main() { fmt.Println(multiply(2, 3, 4)) //输出24 fmt.Println(multiply(4, 5)) //输出20 fmt.Println(multiply(10, 9)) //输出90 } …int本质上是一个切片,也就是[]int,因此可以将其用在for循环中。如果想传入任 意类型的数据,则需要将int修改为空接口interface{}。 举例: func what(args …interface{}) { for _, arg := range args { switch arg.(type) { case int: fmt.Println(arg, "int") case int64: 第5章 函数与方法 51 fmt.Println(arg, "int64") case string: fmt.Println(arg, "string") default: fmt.Println(arg, "unknown") } } }f unc main() { var val1 int = 1 var val2 int64 = 2 var val3 string = "good" var val4 float32 = 1.5 what(val1, val2, val3, val4) } 上述代码的输出结果: 1 int 2 int64 good string 1.5 unknown 5.7 方法 函数和方法分别是面向过程和面向对象编程范畴的概念。从某种角度说,Go是将两种 编程理念融为一体的语言。那么,在Go语言中怎样定义方法呢? 定义方法的语法格式 如下: func (receiver receiverType) methodName ( [arg argType] )( [returnType] ){ method body } 其中,receiver是接受者的名称,receiverType是接受者的类型,methodName是方法名,arg 是形式参数,argType是形式参数的类型,returnType是返回值的类型。显然,函数与方法 的区别之一是方法有接受者。在Go语言中,方法的接受者可以是结构体。 举例: type User struct { //定义结构体User name string email string }f unc (u User) userInfo() string { //方法的接受者是User 类型 return fmt.Sprintf("User name: %s and email: %s\n", u.name, u.email) }f unc main() { user1 := User{name: "Hui", email: "whui2008@tust.edu.cn"} fmt.Print(user1.userInfo()) } 52 Go语言程序设计教程 上述代码的输出结果: User name: Hui and email: whui2008@tust.edu.cn 在上述代码中,u是接受者的名称,User是接受者的类型(结构体),userInfo是方法名, 形式参数及其类型为空,返回值的类型是string。 方法的接受者也可以是非结构体类型,如int。Go语言要求方法methodName()与接 受者类型receiverType必须定义在同一个包内。 举例: type myNumber int //定义整数类型myNumber func (num myNumber) square() myNumber { if num <= 1 { return 1 } return num * num }f unc main() { var n myNumber = 15 result := n.square() fmt.Printf("The square of %d is %d.\n", n, result) } 上述代码的输出结果: The square of 15 is 225. 如果读者删除代码行typemyNumberint,则编译器会给出错误信息“cannotdefine new methodsonnon-localtypeint”。 在Go语言中,函数也是一种数据类型,与其他数据类型一样可以将其赋值给变量。 举例: func demo() { fmt.Println("in demo()") }f unc main() { //声明变量f 为func()函数类型,此时f = nil var f func() f = demo f() } 上述代码的输出结果: in demo() 5.8 小结 函数是执行计算的命名语句序列,而方法则是有接受者的函数。定义函数,使用关键字 func。函数定义完毕后并不能自动运行,只有被调用时才能运行。参数分为形参和实参,形