美文网首页
第五章 函数

第五章 函数

作者: Benedict清水 | 来源:发表于2021-02-19 17:55 被阅读0次

5.1声明函数

普通函数需要声明才能调用。一个函数的声明包括参数和函数名等。

5.1.1 普通函数的声明形式

Go语言的函数声明以func标识,后面紧接着函数名、参数列表、返回参数列表以及函数体,具体形式如下:

func 函数名(参数列表) (返回参数列表){
    函数体
}
  • 函数名:由字母、数字、下划线组成。其中,函数名的第一个字母不能为数字。在同一个包内,函数名称不能重名。
  • 参数列表:一个参数由参数变量和参数类型组成,例如:
func foo(a int, b string)

其中,参数列表中的变量作为函数的局部变量而存在

  • 返回参数列表:可以是返回值类型列表,也可以是类似参数列表中变量名和类别名的组合。函数在声明有返回值时,必须在函数体中使用return 语句提供返回值列表。
  • 函数体:能够被重复调用的代码片段。

5.1.2 参数类型的简写

在参数列表中,如有多个参数变量,则以逗号分隔;如果相邻变量是同类型,则可以将类型省略。例如:

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

a和b都为int型,因此可以省略a的类型,在b的后面说明,这个类型也是a的类型。

5.1.3 函数的返回值

Go支持多个返回值,多返回值能方便的获得函数执行后的多个返回参数,Go语言经常使用多返回值的最后一个返回参数返回函数执行中可能发生的错误。例如:

conn,err := connectToNetwork()

在这段代码中,connectToNetwork返回两个参数,conn表示连接对象,err返回错误。

1. 同一种类型返回值

如果返回值是同一种类型,则用括号将多个返回值类型括起来,用逗号分隔每个返回值的类型。
使用return语句返回时,值列表的顺序需要与函数声明的返回值类型一致。示例代码如下:

func typedTwoValues() (int, int){
    return 1, 2
}
a, b := typedTwoValues()
fmt.Println(a, b)
2. 带有变量名的返回值

Go 语言支持对返回值进行命名,这样返回值就和参数一样拥有参数变量名和类型。命名的返回值变量的默认值为类型的默认值,即数值为0,字符串为空字符串,布尔为false,指针为nil等。

func namedRetValues() (a, b int) {
    a = 1
    b = 2
    return
}

代码说明:

  • 第1行,对两个整型返回值进行命名,分别为a和b。
  • 第3行和第4行,命名返回值的变量与这个函数的局部变量的效果一致,可以对返回值进行赋值和值获取。
  • 第6行,当函数使用命名返回值时,可以在return中不写返回值列表,如果填写也是可行。
func namedRetVlues() (a, b int){
    a = 1
    return a,2
}

5.1.4 调用函数

Go语言的函数的调用格式如下:
返回值列表 = 函数名(参数列表)

  • 函数名:需要调用的函数名。
  • 参数列表:参数变量以逗号隔开,尾部无需以分号结尾。
  • 返回值变量列表:多个返回值使用逗号分隔。
    示例如下:
result := add(1,1)

5.1.5 函数中的参数传递

学习过其他编程语言的应该会知道函数参数有值传递引用传递两个概念,但Go语言中传入和返回参数在调用和返回时都是使用值传递。

1.在python中有值传递和引用传递,我们来看个例子:
def passFuncParam(strParam, listParam):
    strParam = "no san"
    listParam[0] = 'a'
    print("函数内部:")
    print("strVar:", strParam)
    print("lists:", listParam)


if __name__ == "__main__":
    strVar = "san"
    lists = [1, 2, 3]
    print("函数调用前:")
    print("strVar:", strVar)
    print("lists:", lists)
    passFuncParam(strVar, lists)
    print("函数调用后:")
    print("strVar:", strVar)
    print("lists:", lists)

结果如下:

函数调用前:
strVar: san
lists: [1, 2, 3]
函数内部:
strVar: no san
lists: ['a', 2, 3]
函数调用后:
strVar: san
lists: ['a', 2, 3]

我们可以看到strVar的值并没有改变,而在函数内部更改listParam的值后,函数外的lists的值也改变了。说明strVar是值传递,而lists是引用传递。
我们再看一个Go的例子:

package main

import "fmt"

type Data struct {
    complax []int
    instance InnerData
    ptr *InnerData
}

type InnerData struct {
    a int
}

func passByValue(inFunc Data) Data {
    fmt.Printf("inFunc value: %+v\n", inFunc)
    fmt.Printf("inFunc ptr:%p\n", &inFunc)
    return inFunc
}

func main()  {
    in := Data{
        complax: []int{1,2,3},
        instance: InnerData{a:5},
        ptr: &InnerData{1},
    }
    fmt.Printf("in value:%+v\n", in)

    fmt.Printf("in ptr:%p\n", &in)

    out := passByValue(in)

    fmt.Printf("out value: %+v\n", out)

    fmt.Printf("out ptr:%p\n", &out)
}

运行结果:

in value:{complax:[1 2 3] instance:{a:5} ptr:0xc00001c090}
in ptr:0xc000076180
inFunc value: {complax:[1 2 3] instance:{a:5} ptr:0xc00001c090}
inFunc ptr:0xc000076210
out value: {complax:[1 2 3] instance:{a:5} ptr:0xc00001c090}
out ptr:0xc0000761e0
  • 所有的Data结构指针地址发生了变化,意味着所有的结构都是一块新的内存,无论是将Data结构传入函数内部还是通过函数返回值传回Data都会发生复制行为。
  • 所有的Data结构中的成员值都没有发声变化,原样传递,意味着所有参数都是值传递。
  • Data结构的ptr成员在传递过程中保持一致,表示指针在函数参数值传递中传递的只是指针值,不会复制指针指向的部分。

5.2匿名函数

匿名函数没有函数名,只有函数体,函数可以被作为一种类型被赋值给函数类型的变量,匿名函数也往往以变量的方式被传递。匿名函数经常被用于实现回调函数、闭包等。

5.2.1 定义一个匿名函数

匿名函数的定义格式如下:

func (参数列表) (返回参数列表){
函数体
}

匿名函数的定义就是没有名字的普通函数的定义。

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

匿名函数可以在声明后调用,例如:

package main

import "fmt"

func main()  {
    func(data int) {
        fmt.Println("hello,", data)
    }(100)
}

注意第三行“(100)”,表示对匿名函数进行调动,传递参数为100。

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

匿名函数体可以被赋值,例如:

package main

import "fmt"

func main()  {
    f := func(data int) {
        fmt.Println("hello,", data)
    }
    f(100)
}

5.2.2 匿名函数用作回调函数

下面的代码实现对切片的遍历操作,遍历中访问每个元素的操作使用匿名函数来实现。

package main

import "fmt"

func visit(list []int, f func(int))  {
    for _, v := range list{
        f(v)
    }
}

func main()  {
    visit([]int{1,2,3,4}, func(v int) {
        fmt.Println(v)
    })
}
  • 使用visit()函数将整个遍历过程进行封装,当腰获取遍历期间的切片值时,只需要给visit()传入一个回调函数即可。
  • 准备一个整型切边[]int{1,2,3,4}传入visit()函数作为遍历的数据。
  • 定义一个匿名函数,作用是将遍历的每个值打印出来。

5.2.3 使用匿名函数实现操作封装

下面这段代码将匿名函数作为map的键值,通过命令行参数动态调用匿名函数。代码如下:

package main

import "fmt"
import "flag"

var skillParam = flag.String("skill", "", "skill to perform")

func main()  {
    flag.Parse()
    var skill = map[string]func(){
        "fire": func() {
            fmt.Println("chicken fire")
        },
        "run": func() {
            fmt.Println("soldier run")
        },
        "fly": func() {
            fmt.Println("angle fly")
        },
    }
    if f, ok := skill[*skillParam]; ok {
        f()
    } else {
        fmt.Println("skill not found")
    }
}
  • 定义命令行参数skill,从命令行输入--skill可以将空格后的字符串传入skillParam指针变量
  • 解析命令行参数,解析完成后,skillParam指针变量将指向命令行传入的值。
  • 定义一个从字符串映射到func()的map,然后填充这个map。
  • 初始化map的键值对,值为匿名函数。
  • skillParam是一个*string类型的指针变量,使用*skillParam获取到命令行传过来的值,并在map中查找对应命令行参数指定的字符串的函数。
$go run main.go --skill=run
soldier run

5.3 延迟执行语句

Go语言的defer语句会将其后面跟随的语句进行延迟处理。在defer归属的函数即将返回时,将延迟处理的语句按defer的逆序进行执行,也就是说先被defer的语句最后执行,最后的defer的语句,最先被执行。

5.3.1 多个延迟执行语句的处理顺序

下面的代码是将一些列的数值打印语句按顺序延迟处理,如下:

package main

import "fmt"

func main()  {
    fmt.Println("defer begin")

    defer fmt.Println(1)
    defer fmt.Println(2)
    defer fmt.Println(3)
    fmt.Println("defer end")
}

运行结果如下:

defer begin
defer end
3
2
1

5.3.2使用延迟执行语句在函数退出时释放资源

1. 使用延迟并发解锁

map默认并不是并发安全的,准备一个sync.Mutex互斥量保护map的访问。

package main

import (
    "fmt"
    "sync"
)

var valueByKey = make(map[string]int)

var valueByKeyGuard sync.Mutex

func readValue(key string) int {
    valueByKeyGuard.Lock()

    v := valueByKey[key]

    valueByKeyGuard.Unlock()

    return v
}

func main()  {
    valueByKey["first"] = 1
    valueByKey["second"] = 2
    var readKey = "first"
    fmt.Println(readValue(readKey))
}

下面使用defer语句对上面的语句进行简化。

func readValue(key string) int {
    valueByKeyGuard.Lock()
    defer valueByKeyGuard.Unlock()
    return valueByKey[key]
}
2. 使用延迟释放文件句柄

文件的操作需要经过打开文件、获取和操作文件资源、关闭资源几个过程,如果再操作完毕后不关闭文件资源,进程将一直无法释放文件资源。

package main

import (
    "fmt"
    "os"
)

func fileSize(filename string) int64 {
    f, err := os.Open(filename)
    // 如果打开时发生错误,返回文件大小为0
    if err != nil {
        return 0
    }
    defer f.Close()
    // 取文件状态信息
    info, err := f.Stat()
    // 如果获取信息时发生错误,关闭文件并返回文件大小为0
    if err != nil{
        // defer机制触发,调用close关闭文件
        return 0
    }
    // 取文件大小
    size := info.Size()
    //关闭文件
    // defer机制触发,调用close关闭文件
    //返回文件大小
    return size
}
func main()  {
    size := fileSize("./ch02")
    fmt.Println(size)
}

5.4 处理运行时发生的错误

error类型是go语言的一种内置类型,使用的时候不用特定去import。是Go系统声明的接口类型,代码如下:

type error interface{
    Error() string
}

Error()方法返回错误的具体描述,使用者可以通过这个字符串知道发生了什么错误。

5.4.1自定义一个错误。

在Go语言中,使用errors包进行错误的定义,格式如下:

var err = errors.New("This is an error")
在代码中使用错误定义

下面的代码会定义一个除法函数,当除数为0时,返回一个预定义的除数为0的错误。

package main

import "errors"
import "fmt"

var errDivisionByZero = errors.New("division by zero")

func div(dividend, divisor int) (int,error) {
    // 判断除数为0的情况返回
    if divisor == 0 {
        return 0, errDivisionByZero
    }
    // 正常计算,返回空错误
    return dividend / divisor, nil
}

func main()  {
    fmt.Println(div(1,0))
}

运行结果:

$ go run main.go
0 division by zero

相关文章

网友评论

      本文标题:第五章 函数

      本文链接:https://www.haomeiwen.com/subject/genaxltx.html