定义

函数是什么: 基本的代码块

  1. func 函数名(参数列表) (返回参数列表) {
  2. 函数体
  3. }
  4. func FuncName(/*参数列表*/) (o1 type1 o2 type2/*返回类型*/){
  5. //函数体
  6. return o1, o2 //返回多个值
  7. }

函数定义说明:

  • func:关键字
  • FuncName:函数名,根据约定,首字母大写即为public,小字为private
  • 参数列表:可以有0/N个参数,格式为:变量名 类型,如果有多个参数通过逗号分隔,不支持 默认参数
  • 返回类型
    • 上面返回值声明了两个变量名o1和o2(命名返回参数),这个不是必须,可以只有类型没有变量名
    • 如果只有一个返回值且不声明返回值变量,那么你可以省略,包括返回值的括号
    • 如果没有返回值,那么就直接省略最后的返回信息
    • 如果有返回值,那么必须在函数的内部添加return语句

函数类型

go 旦有三种类型的函数

  • 普通的带有名字的函数
  • 匿名函数或者lambda函数
  • 方法(Methods)

除了main()init()函数外,其它所有类型的函数都可以有参数与返回值。

函数参数、返回值以及它们的类型被称为函数签名

函数是一等公民

  • 函数本身可以作为值进行传递
  • 支持匿名函数和闭包closure
  • 函数可以满足接口
  1. func timeSpent(inner func(op int) int) func(op int) int {
  2. return func(n int) int {
  3. start := time.Now()
  4. ret := inner(n)
  5. fmt.Println("time spent: ", time.Since(start).Seconds())
  6. return ret
  7. }
  8. }

可变参数

  1. func Sum(ops ...int) int {
  2. ret := 0
  3. for _, op := range ops {
  4. ret += op
  5. }
  6. return ret
  7. }
  8. func TestVarParam(t *testing.T) {
  9. t.Log(sum(1,2,3,4))
  10. }

函数调用

基本格式

  1. pack1.Function(arg1, arg2, ..., argn)

Function 是 pack1 包里的一个函数,括号里的是被调用函数的实参:这些值被传递给补以调用函数的形参。

==函数被调用的时候,这些实参将被复制(简单而言)然后传递给被调用函数。==函数一般是在其他函数里被调用,这个其他函数被称为调用函数 calling function。函数能多次调用其他函数,这些被调用函数按顺序(简单而言)执行,理论上,函数调用其他函数的次数是无穷的(直到函数调用栈被耗尽)

一个简单的函数调用其他函数的例子

  1. package main
  2. import (
  3. . "fmt" // 类似于 python 的: from xxx import *
  4. )
  5. func main() {
  6. println("In main before calling greeting")
  7. greeting()
  8. println("In main before after greeting")
  9. }
  10. func greeting() {
  11. println("In greeting: Hi!!!")
  12. }
  13. /*
  14. 1. In main before calling greeting
  15. 2. In greeting: Hi!!!!!
  16. 3. In main after calling greeting
  17. */

函数可以将其他函数调用作为它的参数,只要这个被调用函数的返回值个数、返回值类型和返回值顺序与调用函数所需求的一致。

假设 f1 需要3个参数f1(a, b, c int),同时 f2 返回3个参数f2(a, b int) (int, int, int), 就可以这样调用 f1(f2(a, b))

函数重载

指的是可以编写多个同名函数,只要它们拥有不同的形参或者不同的返回值,在go里函数重载是不被允许的。这将导致一个编译错误

  1. funcName redeclared in this book, previous declaration at lineno

go 不支持这项特性的主要原因是函数重载需要进行多余的顾炎武匹配影响性能;没有重载意味着只是一个简单的函数调试。所以你需要给不同的函数使用不同的名称,我们通常会根据函数的特征对函数进行命名

如果需要申明一个在外部定义的函数,你只需要给出函数名与函数签名,不需要给出函数体

  1. func flushICache(begin, end uintptr) //implemented externally

函数也可以以申明的方式被使用,作为一个函数类型

  1. type binOp func(int, int) int

函数值(functions value )之间可以想到比较;如果它们引用是相同的函数或者都是nil的话, 则认为它们是相同的函数。

函数不能在其他函数里声明(不能嵌套),不过我们可以在通过使用匿名函数来破除这个限制。

泛型

目前 go 没有泛型 generic 的概念,也就是说它不支持那种支持多种类型的函数。不过在大部分情况下可以通过接口,特别是空接口与类型选择(type switch)与或者通过使用反射来实现类型的功能。使用这引技术将导致代码更为复杂、性能更为低下,所以在非常注意性能的场合,最好是为每一个类型单独创建一个函数,而且代码可读性更强。

函数参数

函数能够接收参数供自己使用,也可以返回零个或者多个值(我们通常把返回多个值称为返回一组值)。相比与 c、c++、java和C#,多值返回是go的一大特性,为我们判断一个函数是否正常执行提供了方便。

我们通过 return 关键字返回一组值。事实上,任何一个有返回值(单个或者多个)的函数都心须以 return 或者 panic 结尾

在函数块里面,return 之后的语句都不会执行。如果一个函数需要返回值,那么这个函数里面的每一个代码分支 code-path 都要有 return语句。

问题:下面的函数将不会被编译,为什么呢? 大家可以试着纠正过来

  1. func (st *Stack) Pop() int {
  2. v := 0
  3. for ix := len(st) -1; ix>=0; ix-- {
  4. if v = st[ix]; v!=0 {
  5. st[ix] = 0
  6. return v
  7. }
  8. }
  9. }

函数参数与返回值

参数传递

  • 值传递
  • 引用传递

go 默认使用按值传递来传递参数,也就是传递参数的副本。函数接收参数副本之后,在使用变量的过程中可能对副本的值进行更改,但是不会影响到原来的变量

如果你希望函数可以直接修改参数的值,而不是对参数的副本进行操作,你需要将参数的地址(变量名前面添加&取地址符号)传递给函数,这就是按引用传递(指针传递)

如果传递给函数的是一个指针,指针的值(一个地址)会被复制,但是指针的值所指向的地址上的值不会被复制;我们可以通过这个指针的值来修改这个值所指向的地址上的值。(指针也是变量类型,有自己的地址和值,通常指针的值指向一个变量的地址。所以,按引用传递也是按值传递)

几乎在任何情况下,传递指针(一个32位或者64位的值)的消耗都比传递副本来得少。

在函数调用时,像切片slice、字典map、接口interface、通道channel这样的引用类型都是默认使用引用传递(即使没有显式的指出指针)。

有道函数只是完成一个任务,并没有返回值。我们仅仅利用了这种函数的副作用,就像输出文本到终端,发送一个邮件或者是记录一个错误等。

但是绝大部分的函数还是带有返回值的。

有返回值的函数,举例

  1. package main
  2. import "fmt"
  3. func main() {
  4. fmt.Printf("Multiply 2 * 5 * 6 = %d\n", Multiply3Nums(2, 5, 6))
  5. }
  6. func Multiply3Nums(a, b, c int) int {
  7. return a * b * c
  8. }
  9. /*
  10. output:
  11. Multiply 2 * 5 * 6 = 60
  12. */

如果一个函数需要返回四到五个值,我们可以传递一个切片给函数(如果返回值具有相同的函数)或者是传递一个结构体(如果返回值具有不同的类型)。因为传递一个指针允许直接修改变量的仩,消耗也更少。

如下两个函数调用有什么不同:

  1. func DoSomething(a *A){
  2. b = a
  3. }
  4. func DoSomething2(a A){
  5. b = &a
  6. }

命名的函数返回值

当需要返回多个非命名返回值时,需要使用 () 把地它们括起来,比如 (int, int)

命名返回值作为结果形参(result parameters)被初始化为相应的零值,当需要返回的时候,我们只需要一条简单的带参数姝return 语句。需要注意的是,即使只有一个命名返回值,也需要使用 () 括号括起来。

  1. package main
  2. import "fmt"
  3. var num int = 10
  4. var numx2, numx3 int
  5. func main() {
  6. numx2, numx3 = getx2andx3(num)
  7. PrintValues()
  8. numx2, numx3 = getx2andx3_2(num)
  9. PrintValues()
  10. }
  11. func PrintValues() {
  12. fmt.Println(num, numx2, numx3)
  13. }
  14. func getx2andx3(input int) (int, int) {
  15. return 2 * input, 3*input
  16. }
  17. func getx2andx3_2(input int) (x2 int, x3 int) {
  18. x2 = 2* input
  19. x3 = 3* input
  20. return
  21. }

即使函数使用了命名返回值,你依旧可以无视它而返回明确的值

任何一个非命名返回值(使用非命名返回值是很糟的编程习惯)在return语句里面都要明确指出包含返回值的变量或者是一个可计算的值(就像上面警告把指出的那样)。

尽量使用命名返回值: 会使代码更清晰、更简短,同时更加容易主动性。