函数

函数

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

基本概念

函数定义

func funcName(param-list) (result-list) {
    function-body
}

函数的特点

1、函数可以没有输入, 也可以没有返回值(默认返回0)
2、多个相邻的相同类型的参数可以使用简写模式:
    func add(a, b int) int { //a int, b int 简写为 a,b 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) {
        anonymouse := function(x, y int) int {
            return x + y
        }
        return anonymouse(a, b)
    }

多值返回

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

实参到形参的传递

package main

import "fmt"

func chvalue(a int) int {
    a = a + 1
    return a
}

func chpointer(a *int)  {
    *a = *a + 1
    return
}

func main() {
    a := 10
    chvalue(a)  //实参传递给形参是值拷贝
    fmt.Println(a)

    chpointer(&a) // 实参传递给形参仍然是值拷贝,只不过复制的是a的地址值
    fmt.Println(a)
}

不定参数

go 函数支持不定数目的形式参数, 不定参数声明使用param ...type的语法形式

函数的不定参数有如下几个特点:
1、所有的不定参数类型必须是相同的 
2、不定参数必须是函数的最后一个参数
3、不定参数名在函数体内相当于切片,对切片的操作同同样适合对不定参数的操作:
   例如:
        func sum(arr ...int) (sum int) {
            for _, v := range arr { //此时arr就相当于切片,可以使用range访问
                sum += v
            }
            return
        }
4、切片可以作为参数传递给不定参数,切片名后要加上"..."。
    例如:
        func sum(arr ...int) (sum int) {
            sum += v
        }
        
        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)
        fmt.Printf("%T\n", sumb)  //func([]int)

函数签名和匿名函数

函数签名

函数类型又叫函数签名,一个函数的类型就是函数定义首行去掉函数名,参数名和{, 可以使用fmt.Printf的%T
格式化参数打印函数的类型。

package main

import "fmt"

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

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

两个函数类型相同的条件是: 拥有相同的形参列表和返回值列表(列表元素的次序、个数和类型都相同), 形参名可以不同。
以下2个函数的参数类型完全一样.
func add(a, b int) int {return a + b}
func sub(x int, y int) (c int) {c = x -y; return c}


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

import "fmt"

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

func sub(a, b int) int {
    return a - b
}

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

func do(f Op, a, b int) int { // 定义一个函数,第一个参数是函数类型Op
    return f(a, b)	//函数类型变量可以直接用来进行函数调用
}

func main() {
    a := do(add, 1, 2) // 函数名add可以当做相同函数类型形象,不需要强制类型转换
    fmt.Println(a) // 3
    s := do(sub, 1, 3)
    fmt.Println(s) // -1
}
函数类型和map, slice, chan一样, 实际函数类型变量和函数名都可以当做指针变量,该指针执行函数代码的
开始位置. 通常说函数类型变量是一种引用类型,未初始化的函数类型的变量默认值是nil
    Go中函数是"第一公民", 有名函数的函数名可以看做函数类型的常量,可以直接使用函数名调用函数,也可以直接
赋值给函数类型变量, 后续通过该变量来调用该函数.
package main

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


func main() {
    sum(3, 4)  // 直接调用
    f := sum   //有名函数可以直接赋值给变量
    f(1, 3)
}

匿名函数

package main

import "fmt"

// 匿名函数被直接复制函数变量
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 int, 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 int, y int) int {
        return x + y
    }, 1, 2)

    opFunc := wrap("add")
    re := opFunc(2, 3)
    fmt.Printf("%d\n", re)
}

defer

defer关键字, 可以注册多个延迟调用

package main

func main() {
    // 先进后出
    defer func() {
        println("firts")
    }()

    defer func() {
        println("second")
    }()

    println("function body")
}
defer后面必须是函数或方法, 不能是语句。
defer函数的实参在注册时通过值拷贝传递进去.

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

import "os"

func main() {
    defer func() {
        println("defer") //这种情况 不执行
    }()
    println("func body")

    os.Exit(1)
}

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

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

defer也有明显的副作用; defer会推迟资源的释放,defer尽量不要放到循环语句里面, 将大函数内部的defer
语句单独拆分成一个小函数是一种很好的实践方式. 另外,defer相当于普通的函数调用需要间接的数据结构的支持。
相对于普通函数调用有一定的性能耗损。

defer中最好不要对有名返回值参数进行操作,否则会引发匪夷所思的结果

闭包

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

闭包对闭包外的环境引入是直接引用,编译器检测到闭包,会将闭包引用的外部变量分配到堆上。
如果函数返回的闭包引用了该函数的局部变量(参数或函数内部变量)
  (1) 多次调用该函数,返回的多个闭包所引用的外部变量是多个副本,原因是每个调用函数都会为局部变量分配内存。
  (2) 用一个闭包函数多次, 如果该闭包修改了其引用的外部变量。则每一次调用该闭包对该外部变量都有影响,因为闭包函数共享外部引用。
  
package main

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
    println(f(1)) //多次调用f引用的是同一个副本a
    println(f(1))

    // g中a的值仍然是1
    println(g(1))
    println(g(1))
}
f和g引用的是不同的a
如果一个函数调用返回的闭包引用修改了全局变量,则每次调用都会影响全局变量。
如果函数返回的闭包引用的是全局变量a, 则多次调用该函数返回的多个闭包引用的都是同一个a。同理,调用一个闭包
多次引用的也是同一个a。此时如果闭包中修改了a值的逻辑,则每次闭包调用都会影响全局变量a的值。
使用闭包是为了减少全局变量,所以闭包引用全局变量不是好的编程方式。