Go基础编程 - 11 - 函数(func)

上一篇:接口(interface)
下一篇:流程控制


函数

Go语言函数的特点:

1. 无需声明原型
2. 支持不定变参
3. 支持多返回值
4. 支持命名返回参数
5. 函数也是一种类型,一个函数可以复制给变量;可以作为参数传递给其他函数
6. 不支持嵌套(一个包不能有重名的函数)
7. 不支持重载
8. 不支持默认参数

1. 函数定义

使用关键字 func 定义函数,左大括号不能另起一行。

func 函数名(参数列表)(返回值列表) { // 左侧大括号不能另起一行
    函数体
}
1.1. 函数名

函数名是函数的标识符,在Go语言中,函数名必须遵循标识符的命名规则。

1.2. 参数列表

参数列表在函数定义时指出。函数定义时的参数,称为函数的形参;当调用函数时,传递过来的变量就是函数的实参

  • 函数可以没有参数,也可以有多个参数,参数之间用逗号隔开。
  • 参数列表中,类型在变量名之后。
  • 类型相同的相邻参数,参数类型可合并。
  • 不定参数传值 就是函数的参数不是固定的,后面的类型是固定的; 可使用interface{}定义任意类型的不定参数。
  • 不定参数必须是参数列表中最后一个参数。
package main

import "fmt"

// 无参数
func sayHello() {
    fmt.Println("Hello, world!")
}

// 相同类型的相邻参数,参数类型可合并
func add(x, y int) int {
    return x + y
}

// 不定参数,为同一类型,用...表示;接收到的参数为切片类型
func sum(nums...int) int {
    // 参数nums为切片类型
    sum := 0
    for _, num := range nums {
        sum += num
    }
    return sum
}

// 使用interface{}定义任意类型的不定参数,使用类型断言接收参数值。
func myFunc(args ...interface{}) {
    name, _ := args[0].(string)
    age, _ := args[1].(int)
    fmt.Printf("%s今年%d岁!", name, age)
}

func main() {
    sayHello()
    fmt.Println(add(1, 2))
    fmt.Println(sum(1, 2, 3, 4, 5))
    fmt.Println(sum([]int{1, 2, 3}...)) // 使用slice做变参传入是,须使用“...”展开slice
    myFunc("小明", 20)
}

值传递和引用传递
无论是值传递,还是引用传递,传递给函数的都是参数的副本。不过,值传递是值的copy,引用传递是地址的copy。
map、slice、chan、指针、interface默认以引用的方式传递。

package main

func modifyArray(arr [3]int) [3]int {
    for i := range arr {
        arr[i] = arr[i] * 10
    }
    return arr
}

// 1.如果是对数组某个完整元素值的进行修改,那么原有实参数组不变;
// 2.如果是对数组某个元素(切片)内的某个元素的值进行修改,那么原有数据也会跟着改变;
// 传参可以理解为浅copy,参数本身的指针是不同的,但元素指针相同,对元素指针所指向的内容操作会影响到传参过程中的原始数据。
func modifyMultiArray(arr [3][]int) [3][]int {
    for i := range arr[0] {
        arr[0][i] = arr[0][i] * 10  // 实际修改的为arr[0]引用类型值的指针所指向的内存的值,原始实参元素指向同样内存,因此也改变原始实参数据。
    }
    arr[1][2] = 60
    arr[2] = []int{7, 8, 9} // 修改整个引用类型元素的值,实际是给arr[2]重新赋值了一个指向新slice的指针值,原始实参元素指向的内存并未改变,因此不影响原始实参数据。
    return arr
}

func main() {
    arr := [3]int{1, 2, 3}
    arrRes := modifyArray(arr)
    fmt.Println(arr)    // [1 2 3],值传递函数内部修改并未改变arr变量
    fmt.Println(arrRes) // [10 20 30]
    
    arrSlice := [3][]int{
        {1, 2, 3},
        {4, 5, 6},
    }
    arrSlice1 := modifyMultiArray(arrSlice)
    fmt.Println(arrSlice)   // [[10 20 30] [4 5 60] []]
    fmt.Println(arrSlice1) // [[10 20 30] [4 5 60] [7 8 9]]
}

在这里插入图片描述

1.3. 返回值列表
  • 函数可以返回任意数量的返回值,返回值之间用逗号隔开,多返回值必须用括号。
  • 可以使用 “_” 标识符忽略函数的某个返回值。
  • Go语言返回值可以被命名,但不可与形参重名,且必须在函数体中使用;命名返回参数可看做与形参类似的局部变量,最后由 return 隐式返回。
  • 有返回值的函数,必须有明确的终止(return)语句,否则会引发编译错误。
  • Go语言不能使用容器接收多返回值,必须使用多个变量,或者“_”忽略。
package main

import "fmt"

func numbers() (int, int){ // 多返回值
    return 6, 8
}

func add(x, y int) int { // 单个返回值,可省略括号
    return x + y
}

func calc(a, b int) (sum int, avg int) {  // 命名返回值必须使用括号,且不可与形参重名 
    sum = a + b
    avg = (a + b) / 2
    return  
}

func main() {
    //多返回值函数,必须使用多个变量接收,或“_”忽略。
    //var s = make([]int, 2)
    //s = calc(2, 4)	//报错:assignment mismatch: 1 variable but calc returns 2 values
    sum, _ := calc(2, 4)
    fmt.Println(sum)    //6

    // 返回值作为其他函数调用实参
    fmt.Println(add(numbers()))
}

命名返回参数可被同名局部变量遮蔽,此时需要显式返回。

func add(x, y int) (sum int) {
    //var sum = 0 //同级代码块内变量不可重声明 Error:sum redeclared in this block
    { //子代码块
        var sum = x + y
        // return   //不可使用隐式返回 Error:result parameter sum not in scope at return
        return sum
    }
    return  // 隐式返回 0
}

命名返回参数允许 defer 延迟调用通过闭包读取和修改。

func calc(a, b int) (sum int, avg int) {
    defer func() {
        sum += 100
    }()

    sum = a + b
    avg = (a + b) / 2
    return
}

func add(x, y int) int {
    var sum int

    defer func() {
        sum += 100  // 修改无效
    }()

    sum = x + y
    return sum
}

func main(){
    sum, avg := calc(6, 10)
    fmt.Println(sum, avg) // 116 8

    fmt.Println(add(2, 8)) // 10
}

2. 匿名函数

匿名函数由一个不带函数名的函数声明和函数体构成。优势是可以直接使用函数内的变量,不必申明。

package main

import (
    "fmt"
    "math"
)

func main() {
    // Golang可以赋值给变量,做为结构字段,或在channel中传送

    // 变量
    getSqrt := func(a float64) float64 {
        return math.Sqrt(a)
    }
    fmt.Println(getSqrt(4))

    // collection 
    cfn := []()func() string{
        func() string { return "hello" },
        func() string { return "world" },
    }

    // as field
    s := struct{
        fn func() string
    }{
        fn: func() string {
            return "hello"
        },
    }   
    fmt.Println(s.fn())

    // channel
    cf := make(chan func() string, 2)
    fc <- func() string { return "Say hello" }
    fmt.Println((<-fc)())
}

3. 闭包、递归

3.1 闭包

参考资料

3.1.1 函数、引用环境

闭包是有函数及其相关引用环境组合而成的实体(即:闭包=函数+引用环境)

**函数:**在闭包实际实现的时候,往往通过一个外部函数返回其内部函数来实现。内部函数可能是内部实名函数匿名函数或则一个lambda表达式

引用环境:

  • 在函数式语言中,当内嵌函数体内引用到函数体外的变量时,将会把定义时涉及到的引用环境和函数体打包成一个整体(闭包)返回。引用环境是指在程序执行中的某个点所有处于活跃状态的约束(一个变量的名称和其所代表的对象之间的联系)所组成的集合。
  • 由于闭包把函数和运行时的引用环境打包成一个新的整体,所以就解决了函数编程中的嵌套所引发的问题。当每次调用包含闭包的函数是都将返回一个新的闭包实例,这些实例之间是隔离的,分别包含调用时不同的引用环境现场。

闭包与外部函数的生命周期
内函数对外函数的变量的修改,是对变量的引用。变量被引用后,它所在的函数结束,这个变量也不会马上被销毁;相当于变相延长了函数的生命周期。

package main

import "fmt"

func incrIn(n int) func() int {
    fmt.Printf("%p, %d\n", &n, n)
    return func() int {
        n++ // 内函数对外函数的变量的修改,是对变量的引用
        fmt.Printf("%p, %d\n", &n, n)
        return n
    }
}

func incr1(i *int) func() {
    // 自由变量为引用传递,闭包则不再封闭,修改全局可见的变量,也会对闭包内的这个变量造成影响。
    return func() {
        *i += 1
        fmt.Println(*i)
    }
}

func incr2(i int) func() {
    return func() {
        i += 1
        fmt.Println(i)
    }
}

func main() {
    n := incrIn(100)()
    fmt.Printf("%d\n\n", n)

    i := 100
    f1 := incr1(&i)
    f2 := incr2(i)

    f1() //101   
    f1() //102
    f2() //101
    f2() //102
    fmt.Println()

    i = 1000
    f1() //1001  
    f1() //1002
    f2() //103
    f2() //104
    fmt.Println()

    incr1(&i)() // 1003
    incr1(&i)() // 1004
    incr2(i)() // 1005  每次调用都返回独立的闭包实例,这些实例是隔离的
    incr2(i)() // 1005
    fmt.Println()
}
3.1.2 闭包的延迟绑定
func delay1(x int) []func() {
    var fns []func()
    data := []int{1, 2, 3}

    for _, val := range data {
        fns = append(fns, func() {
            fmt.Printf("%d + %d = %d\n", x, val, x+val)
        })
    }
    return fns
}

func delay2() func() {
    x := 1
    fn := func() {
        fmt.Printf("x = %d\n", x)
    }

    x = 100
    return fn
}

func main(){
    fns := delay1(100)
    for _, fn := range fns {
        fn()
    }
    // 输出:
    // 100 + 3 = 103
    // 100 + 3 = 103
    // 100 + 3 = 103

    delay2()()  // 100
}

上面代码解析:

闭包会保存相关引用的环境,也就是说变量在闭包的生命周期得到了保证;因此在执行闭包的时候,会去外部环境寻找最新的值。

delay1()返回的仅是闭包函数的定义,只有在执行fn()是在真正执行了闭包;执行时寻找最新的值3delay2可以更直观的看到,实际执行的为x最新值100

3.1.3 goroutine 的延迟绑定
func show(v interface{}) {
    fmt.Printf("show v: %v\n", v)
}

func gor1() {
    data := []int{1, 2, 3}
    for _, v := range data {
        go show(v)
    }
}

func gor2() {
    data := []int{1, 2, 3}
    for _, v := range data {
        go func() {
            fmt.Printf("gor2 v: %v\n", v)
        }()
    }
}

var ch = make(chan int, 10)

func gor3() {
    for v := range ch {
        go func() {
            fmt.Printf("gor 3 v: %v\n", v)  // 闭包, v为引用
        }()
    }
}

func main() {
    gor1()  // goroutine执行顺序是随机的
    // 输出:
    // show v: 2
    // show v: 1
    // show v: 3

    gor2()
    // 输出:
    // gor2 v: 3
    // gor2 v: 3
    // gor2 v: 3

    go gor3()
    ch <- 1
    ch <- 2
    ch <- 3
    ch <- 4
    ch <- 11
    time.Sleep(time.Duration(1) * time.Nanosecond)
    ch <- 12
    time.Sleep(time.Duration(1) * time.Nanosecond)
    ch <- 13
    time.Sleep(time.Duration(1) * time.Nanosecond)
    ch <- 15
    // 输出:随机输出,大部分为11,个别为1~4
    // gor 3 v: 11
    // gor 3 v: 3
    // gor 3 v: 12
    // gor 3 v: 11
    // gor 3 v: 11
    // gor 3 v: 11
    // gor 3 v: 13
    // gor 3 v: 15

    time.Sleep(5 * time.Second)
}

上面代码解析:

gor2()内的匿名函数就是闭包(参考闭包内部函数的定义),v为引用,且延长了v的生命周期,在gor2()中for-loop的遍历几乎是“瞬时”完成的,goroutine真正被执行在其后。所以输出都为 3。

gor3()中,加入Sleep机制,使得goroutine在赋值前执行。输出结果与赋值及goroutine执行时v的实际值有关

3.2 递归函数

递归,就是在运行的过程中调用自己。一个函数调用自己,就叫递归函数。

package main

import "fmt"

func factorial(i int) int {
    if i <= 1 {
        return 1
    }
    return i * factorial(i-1)
}

func main() {
    var i int = 7
    fmt.Printf("Factorial of %d is %d\n", i, factorial(i))
}

4. 高阶函数

高阶函数满足下面两个条件:

  1. 接受其他的函数作为参数传入
  2. 把其他的函数作为结果返回

函数类型
函数类型属于引用类型,它的值可以为nil,零值为nil。

package main

// 函数类型声明
type operate func (x, y int) int

func Calc(x, y int, op operate) (int, error) {
    if op == nil {
        return 0, errors.New("invalid operation")
    }
    return op(x, y), nil
}

func getCalc(op operate) func(x, y int) (int, error) {
    return func(x, y int) (int, error) {
        if op == nil {
            return 0, errors.New("invalid operation")
        }
        return op(x, y), nil
    }
}

func main() {
    var op operate
    fmt.Printf("%T, %#v", op, op) // main.operate, (main.operate)(nil)

    op = func(x, y int) int { return x + y }
    n, _ := Calc(100, 100, op)
    fmt.Println(n) // 200

    add := getCalc(op)
    v, err := add(10, 10)
    if err != nil {
        fmt.Println(err)
    } else {
        fmt.Println(v) // 20
    }
}

5. 异常处理

Go语言没有结构化异常,使用panic函数来抛出异常,recover捕获异常。
panic、recover 参数类型为interface{},可以处理任意类型。

  • panicrecover 参数类型为 interface{},可以处理任意类型。
  • 利用 recover 处理 panic 指令,defer 必须放在 panic 之前定义。
  • recover 只有在 defer 调用的函数中才有效,否则当 panic 时,recover 无法捕获到 panic,无法防止 panic 扩散。
  • recover 处理异常后,逻辑并不会回复到 panic 那个点去,函数跑到 defer 之后的那个点。
  • 多个 defer 会形成 defer 栈,后定义的 defer 语句会被最先调用。
package main

import (
	"fmt"
)

func demo1() {
    defer func() {
        // 验证是否有panic
        if err := recover(); err != nil {
            fmt.Println("报错:", err)
        }
    }()

    ch := make(chan int, 10)
    close(ch)
    ch <- 1 // 向已关闭的通道发送数据,引发panic
}

func demo2() {
    defer func() {
        fmt.Println("报错:", recover())
    }()

    defer func() {
        // defer 中引发的错误,可以被后续延迟调用捕获,但仅最后一个错误可被捕获。
        panic("defer panic")
    }()

    panic("code panic")
}

func main() {
    demo1() //报错: send on closed channel

    demo2() //报错: defer panic
}

recover函数只有在defer内直接调用才会终止错误,否则返回nil。任何未捕获的错误都会沿用堆栈向外传递


func except() {
    fmt.Println("函数输出错误:", recover())
}

// recover 只有在 defer 调用的函数中才有效。
func main() {

    defer except()  // 有效

    defer func() {  // 有效
        fmt.Println("报错:", recover())
        panic("again panic")
    }()

    defer recover() // 无效 nil

    defer fmt.Println("无效:", recover()) // 无效 nil

    defer func() {
        func() {
            fmt.Println("defer:", recover()) // 无效 nil
        }()
    }()

    panic("panic error!")

    // 输出
    // defer: <nil>
    // 报错: panic error!
    // 函数输出错误: again panic
    // 无效: <nil>
}

常用的异常处理方式

  • 保护代码段,将代码块重构成匿名函数。
func div1(x, y int) int {
    var z int

    func() {
        defer func() {
            if recover() != nil {
                z = 0
            }
        }()

        if y == 0 {
            panic("division by zero")
        }

        z = x / y
    }()

    return z
}
func main() {
    fmt.Println(div1(100, 10))  // 0
    fmt.Println(div1(100, 0))   // 10    
    fmt.Println()
}
  • 除用 panic 引发中断性错误外,还可返回 error 类型错误对象来表示函数调用状态.
    标准库 errors.Newfmt.Errorf 函数用于创建实现 error 接口的错误对象,通过判断错误对象实例来确定具体错误类型。

如何区别使用 panicerror导致关键流程出现不可修复性错误使用 panic,其它使用 error

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

func div(x, y int) (int, error) {
	if y == 0 {
		return 0, errDivZero
	}
	return x / y, nil
}

func main() {
	defer func() {
		fmt.Println("错误:", recover())
	}()

	switch z, err := div(100, 0); err {
	case nil:
		fmt.Println("结果:", z)
	case errDivZero:
		panic(err)
	}
}
  • Go 实现类似 try catch
func Try(fn func(), handler func(interface{})) {

    defer func() {
        if err := recover(); err != nil {
            handler(err)
        }
    }()

    fn()
}

func main() {
    Try(func() {
        panic("Try panic")
    }, func(err interface{}) {
        fmt.Println(err)
    })
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值