2. 函数学习

Go不是一门纯函数式的编程语言,但函数在Go中的第一公民,表现在:

函数式一种类型,函数类型变量可以像其他类型变量一样使用,可以作为其他函数的参数或返回值,也可以直接调用执行。

函数支持多值返回。

支持闭包。

函数支持可变参数

Go是通过编译成本地代码且基于 堆栈 式执行的,Go的错误处理和函数也有千丝万缕的联系。

2.1 基本概念

2.1.1 函数定义

函数首字母的大小写决定该函数在其他包的可见性:大写时其他包可见,小写时只有相同的包可以访问。

func funcName(param-list)(result-list) {

    function-body

}

1) 函数可以没有输入参数,也可以没有返回值

2)多个相邻的相同类型参数可以使用简写。 例如:

func add(a, b int) int {

    return a + b

}

3)支持有名的返回值,参数名就相当于函数体内最外层的局部变量,命名返回值变量会被初始化为类型零值,最后的return可以不带参数名直接返回。

func add(a, b int) (sum int) {

    sum = a + b

    return // return sum的简写模式

    // sum := a + b // 如果是 sum :=a+b,则相当于新声明一个sum变量命名返回变量sum覆盖

    // return sum   // 最后需要显式地调用return sum

}

4)不支持默认值参数。

5)不支持函数重载。

6)不支持函数嵌套,严格地说是不支持命名函数的嵌套定义 ,但支持嵌套匿名函数。例如:

func add(a, b int) (sum int) {

    anonymous := func(x, y int) int {

        return x + y

}

return anonymous(a, b)

}

2.1.2 多返回值

Go函数支持多值返回,定义多值返回的返回参数列表时,要使用()包括

func swap(a, b int) (int, int) {

    return b, a

}

如果多返回值有错误类型,一般将错误类型作为最后一个返回值。

2.1.3 实参到形参的传递

Go函数实参到形参的传递永远是值拷贝。

2.1.4 不定参数

1) 所有的不定参数类型必须是相同的。

2)不定参数必须是函数的最后一个参数。

3)不定参数名在函数体内相当于切片,对切片的操作同样适合对不定参数的操作。例如:

func sum(arr ...int) (sum int) {

    for _, v := range arr {  // 此时 arr 就相当于切片,可以使用range访问

        sum += v

    }

}

4)切片可以作为参数传递给不定参数,切片名后要加上“...”。例如:

func sum(arr ...int) (sum int) {

    for _, v := range arr {

        sum += v

    }

    return

}

func main() {

    slice := []int {1, 2, 3, 4}

    array := [...]int {1, 2, 3, 4}

    // 数组不可以作为实参传递给不定参数的函数

    sum(slice...)

}

5)形参为不定参数的函数和形参为切片的函数类型不相同

func suma(arr ...int) (sum int) {

    for v := range arr {

        sum += v

     }

return

}

func sumb(arr []int) (sum int) {

    for v := range arr {

        sum += v

    }

return

}

// suma 和 sumb 的类型并不一样

fmt.Printf("%T\n", suma) // func(...int) int

fmt.Printf("%T\n", sumb)  // func([]int) int

2.2 函数签名和匿名函数

2.2.1 函数签名

函数类型又叫函数签名。

func add(a, b int) int {

    return a + b

}

func main() {

    fmt.Printf("%T\n", add)  // func(int, int) int

}

两个函数类型相同的条件是:拥有相同形参列表和返回值列表,以下两个函数类型完全一样:

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

func sub(x int, y int) (c int)  { c = x - y; reutrn c}

可以使用type定义函数类型,函数类型变量可以作为函数的参数或者返回值

type Op func(int, int) int // 定义一个函数类型,输入的是两个int类型,返回值是一个int类型

func add(a, b int) int {

    return a + b

}

func sub(a, b int) int {

    reutrn a - b

}

func do(f Op, a, b i nt) int { // 定义一个函数,第一个参数是函数类型Op

    return f(a, b) // 函数类型变量可以直接用来进行函数调用

}

func main() {

    a := do(add, 1, 2) // 函数名add可以当作相同函数类型形参,不需要强制转换类型

    fmt.Println(a) // 3

    s := do(sub, 1, 2)

    fmt.Println(s)  // -1

}

函数类型和map、slice、chan一样,实际函数类型变量和函数名都可以当作指针变量,该指针指向函数代码的开始位置。

通常说函数类型变量是一中引用类型,未初始化的函数类型的变量默认值是 nil

有名函数的函数名可以看做函数类型的常量,可以直接使用函数没那个调用,也可以直接赋值给函数类型变量,后续通过该变量来调用。例如:

func add(a , b int) int {

    return a + b

}

func main() {

    add(1, 2) //直接调用

    f := add   // 赋值给变量,然后通过变量来调用

    f(1, 2)

}

2.2.2 匿名函数

 

var sum = func(a, b int) int { // 匿名函数被直接赋值函数变量

    return a + b

}

func doinput(f func(int, int) int, a, b int) int {

    return f(a, b)

}

//匿名函数作为返回值

func wrap(op string) func(int, int) int {

    switch op {

    case "add":

        return func(a, b int) int {

            return a + b

        }

    case "sub":

        return func(a, b int) int {

           return a + b

       }

     default:

        return nil

    }

}

func main() {

    // 匿名函数直接被调用

        defer func() {

            if err := recover(); err != nil {

                fmt.Println(err)

            }

        }()

        sum(1, 2)

        //匿名函数作为实参

        doinput(func(x, y int) int {

             return x + y

        }, 1, 2)

        opFunc := wrap("add")  //这里是把一个函数赋值给了 opFunc 这个变量

        re := opFunc(2, 3)         // 这里是调用 上面函数 返回 的函数

}

2.3 defer

注册多个延迟调用,先进后出(FILO),常用于回收和释放

func main() {

   //先进后出

    defer func() {

        println(fister)

    }()

   defer func() {

        println("second")

    }

     println("function body")

}

defer后面必须是函数或者方法的调用,不能是语句。

defer函数的实参在注册时通过值拷贝传递进去。下面代码,实参a的值在defer注册时通过值拷贝传递进入,后续a++并不会影响defer语句最后的输出结果.

func f() int {

    a := 0

    defer func(i int) {

       println("defer i=", i)

    }(a)

    a++

    return a

}

// defer打印结果

defer i=0

defer语句必须先注册 后才能执行,如果defer位于returtn之后,则defer因为没有注册,不会执行。

func main() {

    defer func() {

        println("first")

    }()

    a := 0

    println(a)

    return

    defer func() {

       println("second")

    }()

}

结果 :

0

first

主动调用os.Exit(int)退出进程时,defer将不再被执行(即使defer已经提前注册)

func main() {

    defer func() {

        println("defer")

    }()

    println("func body")

    os.Exit(1)

}

结果

func body

exit status 1

defer的好处是可以在一定程度上避免资源泄漏,特别是在有很多return语句,有多个资源需要关闭的场景,很容易漏掉资源的关闭操作,例如:

func CopyFile(dst, src string) (w int64, err error) {

    src, err := os.Open(src)

    if err != nil {

       return

    }

    dst, err := os.Create(dst)

    if err != nil {

        // src很容易被忘记关闭

        src.Close()

        return

    }

    w, err = i.Copy(dst, src)

    dst.Close()

    src.Close()

    return

}

使用defer改写后,在打开资源无报错后直接调用defer关闭资源,一旦养成这样的编程习惯,则很难会忘记资源的释放。例如:

func CopyFile(dst, src string) (w int64, err error) {

    src, err := os.Open(src)

    if err !=nil {

        return

    }

    defer src.Close()

    dst, err := os.Create(dst)

    if err != nil {

        return

    }

    defer dst.Close()

    w, err = io.Copy(dst, src)

    return

}

defer语句的位置不当,有可能导致panic,一般defer语句放在错误检查语句之后。

defer也有明显的副作用:defer会推迟资源的释放,defer尽量不要放到循环语句里面,将大函数内部的defer语句单独拆分一个小函数是一个和好的方式。

另外,defer相当于普通的函数调用需要间接的数据结构的支持,相对普通函数调用有一定的性能损耗。

defer 中最好不要对有名返回值参数进行操作,否则会发生奇怪的结果。

2.4 闭包

2.4.1 概念

闭包是由函数及其引用环境组合而成的实体,一般通过在匿名函数中引用外部函数的局部变量或包全局变量构成。

闭包 = 函数 + 引用环境

闭包对闭包外的环境引入是直接引用,编译器检查到闭包,会将闭包引用的外部变量分配到堆上。

func fa(a int) func(i int) int {

    return func(i int) int {

        println(&a, a)

        a = a + i

        return a

    }

}

func main() {

    f := fa(1) // f 引用的外部的闭包环境包括本次函数调用的形参a的值  1

    g := fa(1) // g 引用的外部的闭包环境包括本次函数调用的形参a的值 1

    // 此时 f、g 引用的必报环境中的a的值并不是同一个,而是两次函数调用产生的副本

    println(f(1))

    //多次调用 f  引用的是同一个副本 a

    println(f(1))

    // g 中 a 的值仍然是 1

    println(g(1))

    println(g(1))

}

f 和 g 引用的是不同的 a

如果一个函数调用返回的闭包引用修改了全局变量,则每次调用都会影响全局变量。

使用闭包是为了减少全局变量,所以闭包引用全局变量不是好的编程方式。例如:

var (

    a = 0

)

func fa() func(i int) int {

    return func(i int) int {

        println(&a, a)

        a = a + 1

        return a

    }

}

func main() {

    f := fa() // f 引用的外部的必报环境包括全局变量 a

    g := fa() // g 引用的外部的必报环境包括全局变量 a

    // 此时, f 、 g 引用的必报环境中的a的值是同一个。

}

2.4.2 闭包的价值

闭包最初的目的是减少全局变量,但是一般不建议使用闭包

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

2.5 panic 和 recover

2.5.1 基本概念

引发panic的两种情况,一是主动调用 panic 函数, 另一张是程序产生运行时错误,并由运行时检测并抛出。

recover()用来捕获panic,阻止panic继续向上传递。recover()和defer一起使用,但是recover()只有在defer后面的函数体内被直接调用才能捕获panic终止异常,否则返回nil,异常继续向外传递.

// 这个会捕获失败

defer recover()

//这个会捕获失败

defer fmt.Println(recover())

// 这个嵌套两层也会捕获失败

defer func() {

    func() {

        println("defer inner")

        recover() //无效

    }()

}()

//如下场景会捕获成功

defer func() {

    println("defer inner")

    recover()

}()

func except() {

    recover()

}

func test() {

    defer except()

    panic("test panic")

}

2.5.2 使用场景

1)程序遇到无法正常执行下去的错误,主动调用panic

2)调试程序,通过主动调用panic实现快速退出,panic打印出的堆栈能够更快的定位错误。

2.6 错误处理

2.6.1 error

2.6.2 错误和异常

2.7 底层实现

2.7.1 函数调用规约

2.7.2 汇编基础

2.7.3 多值返回分析

GOOS=linux GOARCH=amd64 go tool complie -l -N -S swap.go > swap.s 2>&1

从汇编的代码得知:

1)函数的调用者负责环境准备,包括为参数和返回值开辟栈空间。

2)寄存器的保存和恢复也由调用方负责。

3)函数调用后回收栈空间,恢复BP也由主调函数负责。

函数的多值返回 实质上是在栈上开辟多个地址分别存放返回值,这个并没有什么特别的地方,如果返回值是存放在堆上,则多了一个复制的动作。

2.7.4 闭包底层实现

func a(i int) func() { //函数返回引用了外部变量i的闭包

    return func() {

        print(i)

    }

}

func main() {

    f := a(1)

    f()

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值