『GoLang』函数

函数介绍

Go语言函数基本组成包括:

  • 关键字func
  • 函数名
  • 参数列表
  • 返回值
  • 函数体
  • 返回语句

语法如下:

func 函数名(参数列表) (返回值列表) {
    // 函数体
    return
}

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

一个简单的例子:

package main

func main() {
    println("In main before calling greeting")
    greeting()
    println("In main after calling greeting")
}

func greeting() {
    println("In greeting: Hi!!!!!")
}

代码输出:

In main before calling greeting
In greeting: Hi!!!!!
In main after calling greeting

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

funcName redeclared in this book, previous declaration at lineno

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

函数也可以作为函数类型被使用。函数类型也就是函数签名,函数类型表示具有相同参数和结果类型的所有函数的集合。函数类型的未初始化变量的值为nil。就像下面:

type  funcType func (int, int) int

上面通过 type 关键字,定义了一个新类型,函数类型 funcType

函数也可以在表达式中赋值给变量,这样作为表达式中右值出现,我们称之为函数值字面量(function literal),函数值字面量是一种表达式,它的值被称为匿名函数,就像下面一样:

f := func() int { return 7 }  

下面代码对以上2种情况都做了定义和调用:

package main

import (
    "fmt"
    "time"
)

type funcType func(time.Time)  // 定义函数类型funcType

func main() {
    f := func(t time.Time) time.Time { return t }  // 方式一:直接赋值给变量
    fmt.Println(f(time.Now()))

    var timer funcType = CurrentTime  // 方式二:定义函数类型funcType变量timer
    timer(time.Now())

    funcType(CurrentTime)(time.Now())  // 先把CurrentTime函数转为funcType类型,然后传入参数调用
    // 这种处理方式在Go 中比较常见
}

func CurrentTime(start time.Time) {
    fmt.Println(start)
}

函数参数与返回值

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

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

函数定义时,它的形参一般是有名字的,不过我们也可以定义没有形参名的函数,只有相应的形参类型,就像这样:func f(int, int, float64)

没有参数的函数通常被称为 niladic 函数(niladic function),就像 main.main()

按值传递和按引用传递

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

如果你希望函数可以直接修改参数的值,而不是对参数的副本进行操作,你需要将参数的地址(变量名前面添加&符号,比如 &variable)传递给函数,这就是按引用传递,比如 Function(&arg1),此时传递给函数的是一个指针。如果传递给函数的是一个指针,指针的值(一个地址)会被复制,但指针的值所指向的地址上的值不会被复制;我们可以通过这个指针的值来修改这个值所指向的地址上的值。(指针也是变量类型,有自己的地址和值,通常指针的值指向一个变量的地址。所以,按引用传递也是按值传递。

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

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

命名返回值

如下的几个函数带有一个 int 参数,返回两个 int 值;其中一个函数的返回值在函数调用时就已经被赋予了一个初始零值。

getX2AndX3getX2AndX3_2 两个函数演示了如何使用非命名返回值与命名返回值的特性。当需要返回多个非命名返回值时,定义函数时需要在参数列表后使用 () 把它们括起来,比如 getX2AndX3(input int) (int, int)

需要注意的是,即使只有一个命名返回值,也需要使用 () 括起来,如:

func getX2AndX3_3(input int) (x2 int){
    x2 = 2 * input
    return
}

命名返回值作为结果形参(result parameters)被初始化为相应类型的零值,当需要返回的时候,我们只需要一条简单的不带参数的 return 语句。

package main

import "fmt"

var num int = 10
var numx2, numx3 int

func main() {
    numx2, numx3 = getX2AndX3(num)
    PrintValues()
    numx2, numx3 = getX2AndX3_2(num)
    PrintValues()
}

func PrintValues() {
    fmt.Printf("num = %d, 2x num = %d, 3x num = %d\n", num, numx2, numx3)
}

func getX2AndX3(input int) (int, int) {
    return 2 * input, 3 * input
}

func getX2AndX3_2(input int) (x2 int, x3 int) {
    x2 = 2 * input
    x3 = 3 * input
    // return x2, x3, 这两种返回对于命名返回值等价
    return
}

输出结果:

num = 10, 2x num = 20, 3x num = 30    
num = 10, 2x num = 20, 3x num = 30 

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

func getX2AndX3_2(input int) (x2 int, x3 int) {
    x2 = 2 * input
    x3 = 3 * input
    return 1,2
}

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

变参函数

如果函数的最后一个参数是采用 ...type 的形式,那么这个函数就可以处理一个变长的参数,这个长度可以为 0,这样的函数称为变参函数。

func myFunc(a, b, arg ...int) {}

例如:

func Greeting(prefix string, who ...string)
Greeting("hello:", "Joe", "Anna", "Eileen")

Greeting 函数中,变量 who 的值为 []string{"Joe", "Anna", "Eileen"}

如果参数被存储在一个 slice 类型的变量 l 中,则可以通过 l... 的形式来传递参数,调用变参函数。

package main

import "fmt"

func main() {
    x := min(1, 3, 2, 0)
    fmt.Printf("The minimum is: %d\n", x)
    
    l := []int{7,9,3,5,1}
    x = min(l...)
    fmt.Printf("The minimum in the slice is: %d", x)
}

func min(s ...int) int {
    if len(s)==0 {
        return 0
    }
    min := s[0]
    for _, v := range s {
        if v < min {
            min = v
        }
    }
    return min
}

defer和追踪

关键字 defer 允许我们推迟到函数返回之前(或任意位置执行 return 语句之后)一刻才执行某个语句或函数(为什么要在返回之后才执行这些语句?因为 return 语句同样可以包含一些操作,而不是单纯地返回某个值)。

关键字 defer 的用法类似于面向对象编程语言 Java 和 C# 的 finally 语句块,它一般用于释放某些已分配的资源。

示例:

package main

import "fmt"

func main() {
    function1()
}

func function1() {
    fmt.Printf("In function1 at the top\n")
    defer function2()
    fmt.Printf("In function1 at the bottom!\n")
}

func function2() {
    fmt.Printf("Function2: Deferred until the end of the calling function!")
}

输出:

In Function1 at the top
In Function1 at the bottom!
Function2: Deferred until the end of the calling function!

使用 defer 的语句同样可以接受参数:

func a() {
    i := 0
    defer fmt.Println(i)
    i++
    return
}

输出:

0

这里可以看出来,defer语句虽然推迟执行,但是执行命令是已经确认的,不会由于后面i增加了就改变打印出来的i的值

当有多个defer行为被注册时,它们会以逆序执行(类似栈,即后进先出):

func f() {
    for i := 0; i < 5; i++ {
        defer fmt.Printf("%d ", i)
    }
}

输出:

4 3 2 1 0 

关键字 defer 允许我们进行一些函数执行完成后的收尾工作,例如:

  1. 关闭流文件
// open a file  
defer file.Close()
  1. 解锁一个加锁的资源
mu.Lock()  
defer mu.Unlock() 
  1. 打印最终报告
printHeader()  
defer printFooter()
  1. 关闭数据库链接
// open a database connection  
defer disconnectFromDB()

内置函数

以下是一个简单的列表,会在后面对它们进行逐个深入的讲解。

名称 说明
close 用于管道通信
len、cap len 用于返回某个类型的长度或数量(字符串、数组、切片、map 和管道);cap 是容量的意思,用于返回某个类型的最大容量(只能用于切片和 map)
new、make new 和 make 均是用于分配内存:new 用于值类型和用户定义的类型,如自定义结构,make 用于内置引用类型(切片、map 和管道)。它们的用法就像是函数,但是将类型作为参数:new(type)make(type)
new(T) 分配类型 T 的零值并返回其地址,也就是指向类型 T 的指针。它也可以被用于基本类型:v := new(int)
make(T) 返回类型 T 的初始化之后的值,因此它比 new 进行更多的工作new() 是一个函数,不要忘记它的括号
copy、append 用于复制和连接切片
panic、recover 两者均用于错误处理机制
print、println 底层打印函数,在部署环境中建议使用 fmt 包
complex、real imag 用于创建和操作复数

这些再具体到某种应用时再介绍详细用法

函数回调

Go 语言中函数可以作为其它函数的参数进行传递,然后在其它函数内调用执行,一般称之为回调。

package main

import (
    "fmt"
)

func main() {
    callback(1, Add)
}

func Add(a, b int) {
    fmt.Printf("%d 与 %d 相加的和是: %d\n", a, b, a+b)
}

func callback(y int, f func(int, int)) {
    f(y, 2) // 回调函数f
}

输出:

1 与 2 相加的和是: 3

匿名函数

函数值字面量是一种表达式,它的值被称为匿名函数。从形式上看当我们不给函数起名字的时候,可以使用匿名函数,例如:

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

这样的函数不能够独立存在,但可以被赋值于某个变量,即保存函数的地址到变量中:

fplus := func(x, y int) int { return x + y }

然后通过变量名对函数进行调用:

fplus(3, 4)

当然,也可以直接对匿名函数进行调用,注意匿名函数的最后面加上了括号并填入了参数值,如果没有参数,也需要加上空括号,代表直接调用:

func(x, y int) int { return x + y } (3, 4)

闭包

匿名函数同样也被称之为闭包

闭包可被允许调用定义在其环境下的变量,可以访问它们所在的外部函数中声明的所有局部变量、参数和声明的其他内部函数。闭包继承了函数所声明时的作用域,作用域内的变量都被共享到闭包的环境中,因此这些变量可以在闭包中被操作,直到被销毁。也可以理解为内层函数引用了外层函数中的变量或称为引用了自由变量。

实质上看,闭包是由函数及其相关引用环境组合而成的实体(即:闭包=函数+引用环境)。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。由闭包的实质含义,我们可以推论:闭包获取捕获变量相当于引用传递,而非值传递;对于闭包函数捕获的常量和变量,无论闭包何时何处被调用,闭包都可以使用这些常量和变量,而不用关心它们表面上的作用域。

换句话说闭包函数可以访问不是它自己内部的变量(这个变量在其它作用域内声明),且这个变量是未赋值的,它在闭包里面赋值。

应用闭包:将函数作为返回值

package main

import "fmt"

func main() {
    var f = Adder()
    fmt.Println(f(1))
    fmt.Println(f(20))
    fmt.Println(f(300))
}

func Adder() func(int) int {
    var x int
    return func(delta int) int {
        x += delta
        return x
    }
}

输出:

1
21
321

三次调用函数 f 的过程中函数 Adder() 中变量 delta 的值分别为:1、20 和 300。

我们可以看到,在多次调用中,变量 x 的值是被保留的,即 0 + 1 = 1,然后 1 + 20 = 21,最后 21 + 300 = 321闭包函数保存并积累其中的变量的值,不管外部函数退出与否,它都能够继续操作外部函数中的局部变量

工厂函数

一个返回值为另一个函数的函数可以被称之为工厂函数,这在您需要创建一系列相似的函数的时候非常有用:书写一个工厂函数而不是针对每种情况都书写一个函数。下面的函数演示了如何动态返回追加后缀的函数:

func MakeAddSuffix(suffix string) func(string) string {
    return func(name string) string {
        if !strings.HasSuffix(name, suffix) {
            return name + suffix
        }
        return name
    }
}

现在,我们可以生成如下函数:

addBmp := MakeAddSuffix(".bmp")
addJpeg := MakeAddSuffix(".jpeg")

然后调用它们:

addBmp("file")   // returns: file.bmp
addJpeg("file")  // returns: file.jpeg
posted @ 2020-04-04 20:08  芜情  阅读(248)  评论(0编辑  收藏  举报