GO(三) | 青训营笔记

55 阅读10分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 3 天

函数

函数定义

package main
​
import "fmt"func test(fn func() int) int {
    return fn()
}
// 定义函数类型。
type FormatFunc func(s string, x, y int) stringfunc format(fn FormatFunc, s string, x, y int) string {
    return fn(s, x, y)
}
​
func main() {
    s1 := test(func() int { return 100 }) // 直接将匿名函数当参数。
​
    s2 := format(func(s string, x, y int) string {
        return fmt.Sprintf(s, x, y)
    }, "%d, %d", 10, 20)
​
    println(s1, s2)
}

参数

值传递:指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。

func swap(x, y int) int {
       ... ...
  }

引用传递:是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

package main
​
import (
    "fmt"
)
​
/* 定义相互交换值的函数 */
func swap(x, y *int) {
    var temp int
​
    temp = *x /* 保存 x 的值 */
    *x = *y   /* 将 y 值赋给 x */
    *y = temp /* 将 temp 值赋给 y*/
​
}
​
func main() {
    var a, b int = 1, 2
    /*
        调用 swap() 函数
        &a 指向 a 指针,a 变量的地址
        &b 指向 b 指针,b 变量的地址
    */
    swap(&a, &b)
​
    fmt.Println(a, b)
}

使用 slice 对象做变参时,必须展开。(slice...)

package main
​
import (
    "fmt"
)
​
func test(s string, n ...int) string {
    var x int
    for _, i := range n {
        x += i
    }
​
    return fmt.Sprintf(s, x)
}
​
func main() {
    s := []int{1, 2, 3}
    res := test("sum: %d", s...)    // slice... 展开slice
    println(res)
}

返回值

package main
​
import "fmt"var num int = 10
var numx2, numx3 intfunc 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
}

空白符(blank identifier)

空白符用来匹配一些不需要的值,然后丢弃掉

package main
​
import "fmt"func main() {
    var i1 int
    var f1 float32
    i1, _, f1 = ThreeValues()
    fmt.Printf("The int: %d, the float: %f \n", i1, f1)
}
​
func ThreeValues() (int, int, float32) {
    return 5, 6, 7.5
}

改变外部变量(outside variable)

传递指针给函数不但可以节省内存(因为没有复制变量的值),而且赋予了函数直接修改外部变量的能力,所以被修改的变量不再需要使用 return 返回

package main
​
import (
    "fmt"
)
​
// this function changes reply:
func Multiply(a, b int, reply *int) {
    *reply = a * b
}
​
func main() {
    n := 0
    reply := &n
    Multiply(10, 5, reply)
    fmt.Println("Multiply:", *reply) // Multiply: 50
}

匿名函数

package main
​
func main() {
    // --- function variable ---
    fn := func() { println("Hello, World!") }
    fn()
​
    // --- function collection ---
    fns := [](func(x int) int){
        func(x int) int { return x + 1 },
        func(x int) int { return x + 2 },
    }
    println(fns[0](100))
​
    // --- function as field ---
    d := struct {
        fn func() string
    }{
        fn: func() string { return "Hello, World!" },
    }
    println(d.fn())
​
    // --- channel of function ---
    fc := make(chan func() string, 2)
    fc <- func() string { return "Hello, World!" }
    println((<-fc)())
}

闭包,递归

闭包

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

package main
​
import (
    "fmt"
)
​
func a() func() int {
    i := 0
    b := func() int {
        i++
        fmt.Println(i)
        return i
    }
    return b
}
​
func main() {
    c := a()
    c()
    c()
    c()
​
    a() //不会输出i
}

闭包复制的是原对象指针

package main
​
import "fmt"func test() func() {
    x := 100
    fmt.Printf("x (%p) = %d\n", &x, x)
​
    return func() {
        fmt.Printf("x (%p) = %d\n", &x, x)
    }
}
​
func main() {
    f := test()
    f()
}

在汇编层 ,test 实际返回的是 FuncVal 对象,其中包含了匿名函数地址、闭包对象指针。当调 匿名函数时,只需以某个寄存器传递该对象即可。FuncVal { func_address, closure_var_pointer ... }

外部引用函数参数局部变量

package main
​
import "fmt"// 外部引用函数参数局部变量
func add(base int) func(int) int {
    return func(i int) int {
        base += i
        return base
    }
}
​
func main() {
    tmp1 := add(10)
    fmt.Println(tmp1(1), tmp1(2))
    // 此时tmp1和tmp2不是一个实体了
    tmp2 := add(100)
    fmt.Println(tmp2(1), tmp2(2))
}

递归

构成递归必备条件:

  1. 子问题必须和原问题为同样的事,且更简单
  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))
}

延迟调用defer

defer特性:

  1. 关键字 defer 用于注册延迟调用。
  2. 这些调用直到 return 前才被执。因此,可以用来做资源清理。
  3. 多个defer语句,按先进后出的方式执行。
  4. defer语句中的变量,在defer声明时就决定了。

defer用途:

  1. 关闭文件句柄
  2. 锁资源释放
  3. 数据库连接释放

defer 是先进后出

package main
​
import "fmt"func main() {
    var whatever [5]struct{}
​
    for i := range whatever {
        defer fmt.Println(i)
    }
}

defer 碰上闭包

package main
​
import "fmt"func main() {
    var whatever [5]struct{}
    for i := range whatever {
        defer func() { fmt.Println(i) }()
    }
}

defer后面的语句在执行的时候,函数调用的参数会被保存起来,但是不执行

多个 defer 注册,按 FILO 次序执行 ( 先进后出 )。函数或某个延迟调用发生错误,这些调用依旧会被执行。

package main
​
func test(x int) {
    defer println("a")
    defer println("b")
​
    defer func() {
        println(100 / x) // div0 异常未被捕获,逐步往外传递,最终终止进程。
    }()
​
    defer println("c")
}
​
func main() {
    test(0)
}

*延迟调用参数在注册时求值或复制,可用指针或闭包 "延迟" 读取。

package main
​
func test() {
    x, y := 10, 20
​
    defer func(i int) {
        println("defer:", i, y) // y 闭包引用
    }(x) // x 被复制
​
    x += 10
    y += 100
    println("x =", x, "y =", y)
}
​
func main() {
    test()
}

*滥用 defer 可能会导致性能问题,尤其是在一个 "大循环" 里。

package main
​
import (
    "fmt"
    "sync"
    "time"
)
​
var lock sync.Mutex
​
func test() {
    lock.Lock()
    lock.Unlock()
}
​
func testdefer() {
    lock.Lock()
    defer lock.Unlock()
}
​
func main() {
    func() {
        t1 := time.Now()
​
        for i := 0; i < 10000; i++ {
            test()
        }
        elapsed := time.Since(t1)
        fmt.Println("test elapsed: ", elapsed)
    }()
    func() {
        t1 := time.Now()
​
        for i := 0; i < 10000; i++ {
            testdefer()
        }
        elapsed := time.Since(t1)
        fmt.Println("testdefer elapsed: ", elapsed)
    }()
}

defer陷阱

defer 与 closure

package main
​
import (
    "errors"
    "fmt"
)
​
func foo(a, b int) (i int, err error) {
    defer fmt.Printf("first defer err %v\n", err)
    defer func(err error) { fmt.Printf("second defer err %v\n", err) }(err)
    defer func() { fmt.Printf("third defer err %v\n", err) }()
    if b == 0 {
        err = errors.New("divided by zero!")
        return
    }
​
    i = a / b
    return
}
​
func main() {
    foo(2, 0)
}

defer 与 return

package main
​
import "fmt"func foo() (i int) {
​
    i = 0
    defer func() {
        fmt.Println(i)
    }()
​
    return 2
}
​
func main() {
    foo()
}

defer nil 函数

package main
​
import (
    "fmt"
)
​
func test() {
    var run func() = nil
    defer run()
    fmt.Println("runs")
}
​
func main() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println(err)
        }
    }()
    test()
}

在错误的位置使用 defer

当 http.Get 失败时会抛出异常。

package main
​
import "net/http"func do() error {
    res, err := http.Get("http://www.google.com")
    defer res.Body.Close()
    if err != nil {
        return err
    }
​
    // ..code...
​
    return nil
}
​
func main() {
    do()
}

在这里我们并没有检查我们的请求是否成功执行,当它失败的时候,我们访问了 Body 中的空变量 res ,因此会抛出异常

解决方案

总是在一次成功的资源分配下面使用 defer ,对于这种情况来说意味着:当且仅当 http.Get 成功执行时才使用 defer

package main
​
import "net/http"func do() error {
    res, err := http.Get("http://xxxxxxxxxx")
    if res != nil {
        defer res.Body.Close()
    }
​
    if err != nil {
        return err
    }
​
    // ..code...
​
    return nil
}
​
func main() {
    do()
}

在上述的代码中,当有错误的时候,err 会被返回,否则当整个函数返回的时候,会关闭 res.Body 。

需要检查 res 的值是否为 nil ,这是 http.Get 中的一个警告。通常情况下,出错的时候,返回的内容应为空并且错误会被返回,可当获得的是一个重定向 error 时, res 的值并不会为 nil ,但其又会将错误返回。上面的代码保证了无论如何 Body 都会被关闭,如果没有打算使用其中的数据,那么你还需要丢弃已经接收的数据。

不检查错误

在这里,f.Close() 可能会返回一个错误,可这个错误会被我们忽略掉

package main
​
import "os"func do() error {
    f, err := os.Open("book.txt")
    if err != nil {
        return err
    }
​
    if f != nil {
        defer f.Close()
    }
​
    // ..code...
​
    return nil
}
​
func main() {
    do()
}

改进:通过命名的返回变量来返回 defer 内的错误。

package main
​
import "os"func do() (err error) {
    f, err := os.Open("book.txt")
    if err != nil {
        return err
    }
​
    if f != nil {
        defer func() {
            if ferr := f.Close(); ferr != nil {
                err = ferr
            }
        }()
    }
​
    // ..code...
​
    return nil
}
​
func main() {
    do()
}

释放相同的资源

如果你尝试使用相同的变量释放不同的资源,那么这个操作可能无法正常执行。

package main
​
import (
    "fmt"
    "os"
)
​
func do() error {
    f, err := os.Open("book.txt")
    if err != nil {
        return err
    }
    if f != nil {
        defer func() {
            if err := f.Close(); err != nil {
                fmt.Printf("defer close book.txt err %v\n", err)
            }
        }()
    }
​
    // ..code...
​
    f, err = os.Open("another-book.txt")
    if err != nil {
        return err
    }
    if f != nil {
        defer func() {
            if err := f.Close(); err != nil {
                fmt.Printf("defer close another-book.txt err %v\n", err)
            }
        }()
    }
​
    return nil
}
​
func main() {
    do()
}

当延迟函数执行时,只有最后一个变量会被用到,因此,f 变量会成为最后那个资源 (another-book.txt)。而且两个 defer 都会将这个资源作为最后的资源来关闭

解决方案:

package main
​
import (
    "fmt"
    "io"
    "os"
)
​
func do() error {
    f, err := os.Open("book.txt")
    if err != nil {
        return err
    }
    if f != nil {
        defer func(f io.Closer) {
            if err := f.Close(); err != nil {
                fmt.Printf("defer close book.txt err %v\n", err)
            }
        }(f)
    }
​
    // ..code...
​
    f, err = os.Open("another-book.txt")
    if err != nil {
        return err
    }
    if f != nil {
        defer func(f io.Closer) {
            if err := f.Close(); err != nil {
                fmt.Printf("defer close another-book.txt err %v\n", err)
            }
        }(f)
    }
​
    return nil
}
​
func main() {
    do()
}

异常处理

Go中可以抛出一个panic的异常,然后在defer中通过recover捕获这个异常,然后正常处理。

panic:

  1. 内置函数
  2. 假如函数F中书写了panic语句,会终止其后要执行的代码,在panic所在函数F内如果存在要执行的defer函数列表,按照defer的逆序执行
  3. 返回函数F的调用者G,在G中,调用函数F语句之后的代码不会执行,假如函数G中存在要执行的defer函数列表,按照defer的逆序执行
  4. 直到goroutine整个退出,并报告错误

recover:

  1. 内置函数

  2. 用来控制一个goroutine的panicking行为,捕获panic,从而影响应用的行为

  3. 一般的调用建议

    • 在defer函数中,通过recever来终止一个goroutine的panicking过程,从而恢复正常代码的执行
    • 可以获取通过panic传递的error

注意:

  1. 利用recover处理panic指令,defer 必须放在 panic 之前定义,另外 recover 只有在 defer 调用的函数中才有效。否则当panic时,recover无法捕获到panic,无法防止panic扩散。
  2. recover 处理异常后,逻辑并不会恢复到 panic 那个点去,函数跑到 defer 之后的那个点。
  3. 多个 defer 会形成 defer 栈,后定义的 defer 语句会被最先调用。
package main
​
func main() {
    test()
}
​
func test() {
    defer func() {
        if err := recover(); err != nil {
            println(err.(string)) // 将 interface{} 转型为具体类型。
        }
    }()
​
    panic("panic error!")
}

由于 panic、recover 参数类型为 interface{},因此可抛出任何类型对象。

向已关闭的通道发送数据会引发panic

package main
​
import (
    "fmt"
)
​
func main() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println(err)
        }
    }()
​
    var ch chan int = make(chan int, 10)
    close(ch)
    ch <- 1
}

延迟调用中引发的错误,可被后续延迟调用捕获,但仅最后一个错误可被捕获。

package main
​
import "fmt"func test() {
    defer func() {
        fmt.Println(recover())
    }()
​
    defer func() {
        panic("defer panic")
    }()
​
    panic("test panic")
}
​
func main() {
    test()
}

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

package main
​
import "fmt"func test() {
    defer func() {
        fmt.Println(recover()) //有效
    }()
    defer recover()              //无效!
    defer fmt.Println(recover()) //无效!
    defer func() {
        func() {
            println("defer inner")
            recover() //无效!
        }()
    }()
​
    panic("test panic")
}
​
func main() {
    test()
}

使用延迟匿名函数或下面这样都是有效的。

package main
​
import (
    "fmt"
)
​
func except() {
    fmt.Println(recover())
}
​
func test() {
    defer except()
    panic("test panic")
}
​
func main() {
    test()
}

如果需要保护代码 段,可将代码块重构成匿名函数,如此可确保后续代码被执 。

package main
​
import "fmt"func test(x, y int) {
    var z int
​
    func() {
        defer func() {
            if recover() != nil {
                z = 0
            }
        }()
        panic("test panic")
        z = x / y
        return
    }()
​
    fmt.Printf("x / y = %d\n", z)
}
​
func main() {
    test(2, 1)
}

除用 panic 引发中断性错误外,还可返回 error 类型错误对象来表示函数调用状态。

type error interface {
    Error() string
}

标准库 errors.New 和 fmt.Errorf 函数用于创建实现 error 接口的错误对象。通过判断错误对象实例来确定具体错误类型。

package main
​
import (
    "errors"
    "fmt"
)
​
var ErrDivByZero = errors.New("division by zero")
​
func div(x, y int) (int, error) {
    if y == 0 {
        return 0, ErrDivByZero
    }
    return x / y, nil
}
​
func main() {
    defer func() {
        fmt.Println(recover())
    }()
    switch z, err := div(10, 0); err {
    case nil:
        println(z)
    case ErrDivByZero:
        panic(err)
    }
}

Go实现类似 try catch 的异常处理

package main
​
import "fmt"func Try(fun func(), handler func(interface{})) {
    defer func() {
        if err := recover(); err != nil {
            handler(err)
        }
    }()
    fun()
}
​
func main() {
    Try(func() {
        panic("test panic")
    }, func(err interface{}) {
        fmt.Println(err)
    })
}

区别使用 panic 和 error 两种方式:导致关键流程出现不可修复性错误的使用 panic,其他使用 error