Golang学习-CH5 Go语言函数


目录
  • 5.1 Go语言函数声明和定义
    • 普通函数声明(定义)
    • 返回值
  • 5.2 Go语言函数中参数传递影响
  • 5.3 Go语言函数变量
  • 5.4 Go语言字符串的链式处理
  • 5.5 Go语言匿名函数
  • 5.6 Go语言函数类型实现接口*
  • 5.7 Go语言闭包*
    • 在闭包内部修改引用的变量
    • 闭包的记忆效应
    • 闭包实现生成器
  • 5.8 Go语言可变参数
    • 单一类型、数量可变
    • 可变类型、可变数量
    • 在多个可变参数函数中传递参数
  • 5.9 Go语言defer(延迟执行语句)
    • 多个延迟执行语句的处理顺序
    • 使用延迟执行语句在函数退出时释放资源
  • 5.10 Go语言递归函数
  • 5.11 Go语言处理运行时错误*
    • 自定义一个错误
    • 借助自定义结构体实现错误接口*
  • 5.12 Go语言宕机(panic)
    • 手动触发宕机
    • 在运行依赖的必备资源缺失时主动触发宕机
    • 宕机时触发延迟执行语句
  • 5.13 Go语言宕机恢复(recover)
    • 让程序在崩溃时继续执行*
    • panic和recover的关系
  • 5.14 Go语言一些函数应用
    • Go语言计算函数执行时间
    • Go语言通过内存缓存来提升性能
    • Go语言哈希函数
  • 5.15 Go语言函数的底层实现*
  • 5.16 Go语言Test功能测试函数
    • 测试规则
    • 单元测试
    • 压力测试
    • 覆盖率测试

Go 语言支持普通函数匿名函数闭包,从设计上对函数进行了优化和改进,让函数使用起来更加方便。

Go 语言的函数属于“一等公民”(first-class),也就是说:

  • 函数本身可以作为值进行传递。
  • 支持匿名函数和闭包(closure)。
  • 函数可以满足接口。

5.1 Go语言函数声明和定义

  • 在Go语言中,函数的基本组成为:关键字 func、函数名、参数列表、返回值、函数体和返回语句。
  • Go语言里面拥三种类型的函数:
    • 普通的带有名字的函数
    • 匿名函数或者 lambda 函数
    • 方法

普通函数声明(定义)

func 函数名(形式参数列表)(返回值列表){
    函数体
}

//如果一组形参或返回值有相同的类型,不必为每个形参都写出参数类型
func f(i, j, k int, s, t string) { /* ... */ }
func f(i int, j int, k int, s string, t string) { /* ... */ }

//示例
func add(x int, y int) int {return x + y}
func sub(x, y int) (z int) { z = x - y; return}
func first(x int, _ int) int { return x }
func zero(int, int) int { return 0 }
fmt.Printf("%T\n", add) // "func(int, int) int"
fmt.Printf("%T\n", sub) // "func(int, int) int"
fmt.Printf("%T\n", first) // "func(int, int) int"
fmt.Printf("%T\n", zero) // "func(int, int) int"
  • Go语言没有默认参数值,也没有任何方法可以通过参数名指定形参,因此形参和返回值的变量名对于函数调用者而言没有意义。

  • 在函数中,实参通过值传递的方式进行传递,因此函数的形参是实参的拷贝,对形参进行修改不会影响实参。但是,如果实参包括引用类型,如指针、slice(切片)、map、function、channel 等类型,实参可能会由于函数的间接引用被修改。

  • Go语言中函数没有声明后定义函数体的操作,必须直接完成声明和函数体内容

返回值

Go语言支持多返回值,多返回值能方便地获得函数执行后的多个返回参数。

  • 一般来说,多返回值的最后一个返回参数返回函数执行中可能出现的错误。
  • Go语言既支持安全指针,也支持多返回值,因此在使用函数进行逻辑编写时更为方便。

多个返回值:

  • 用括号将多个返回值类型括起来,用逗号分隔每个返回值的类型。
  • 使用 return 语句返回时,值列表的顺序需要与函数声明的返回值类型一致。
func typedTwoValues() (int, int) {
    return 1, 2
}
func main() {
    a, b := typedTwoValues()
    fmt.Println(a, b)
}

带有变类名的返回值:

  • 纯类型的返回值对于代码可读性不是很友好
  • Go语言支持对返回值进行命名,这样返回值就和参数一样拥有参数变量名和类型
  • 命名的返回值变量的默认值为类型的默认值
//当函数使用命名返回值时,可以在 return 中不填写返回值列表
func namedRetValues() (a, b int) {
    a = 1
    b = 2
    return
}

注意:同一种类型返回值和命名返回值两种形式只能二选一,混用时将会发生编译错误

func namedRetValues() (a, b int, int)

5.2 Go语言函数中参数传递影响

  • 常规传递:使用普通数值变量时,传递参数类型为值传递。函数内部对参数的处理不会影响外部。
  • 指针传递:使用指针变量传递地址,会影响到外部实参
  • 数组名作为参数传递:不同于其他语言,go语言在将数组名作为函数参数的时候,参数传递即是对数组的复制。在形参中对数组元素的修改都不会影响到数组元素原来的值。
  • 切片作为参数传递:在使用slice作为函数参数时,进行参数传递将是一个地址拷贝,即将底层数组的内存地址复制给参数slice。
  • 函数作为参数:在go语言中,函数也作为一种数据类型,所以函数也可以作为函数的参数来使用。
func function(a, b int, sum func(int, int) int) {

    fmt.Println(sum(a, b))

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

func main() {
    var a, b int = 5, 6
    f := sum
    function(a, b, f)
}

5.3 Go语言函数变量

在Go语言中,函数也是一种类型,可以和其他类型一样保存在变量中。

示例见5.2

5.4 Go语言字符串的链式处理

使用 SQL 语言从数据库中获取数据时,可以对原始数据进行排序(sort by)、分组(group by)和去重(distinct)等操作,SQL 将数据的操作与遍历过程作为两个部分进行隔离,这样操作和遍历过程就可以各自独立地进行设计,这就是常见的数据与操作分离的设计

对数据的操作进行多步骤的处理被称为链式处理,本例中使用多个字符串作为数据集合,然后对每个字符串进行一系列的处理,用户可以通过系统函数或者自定义函数对链式处理中的每个环节进行自定义。

https://mip.yht7.com/golang/5458.html

package main

import (
    "fmt"
    "strings"
)
// 字符串处理函数,传入字符串切片和处理链
func StringProccess(list []string, chain []func(string) string) {
    // 遍历每一个字符串
    for index, str := range list {
        // 第一个需要处理的字符串
        result := str
        // 遍历每一个处理链
        for _, proc := range chain {
            // 输入一个字符串进行处理,返回数据作为下一个处理链的输入。
            result = proc(result)
        }
        // 将结果放回切片
        list[index] = result
    }
}

// 自定义的移除前缀的处理函数
func removePrefix(str string) string {
    return strings.TrimPrefix(str, "go")
}

func main() {
    // 待处理的字符串列表
    list := []string{
        "go scanner",
        "go parser",
        "go compiler",
        "go printer",
        "go formater",
    }

    // 处理函数链
    chain := []func(string) string{
        removePrefix,
        strings.TrimSpace,
        strings.ToUpper,
    }

    // 处理字符串
    StringProccess(list, chain)
    // 输出处理好的字符串
    for _, str := range list {
        fmt.Println(str)
    }
}

链式处理器是一种常见的编程设计,Netty 是使用 Java 语言编写的一款异步事件驱动的网络应用程序框架,支持快速开发可维护的高性能的面向协议的服务器和客户端,Netty 中就有类似的链式处理器的设计。

5.5 Go语言匿名函数

Go语言支持匿名函数,即在需要使用函数时再定义函数,匿名函数没有函数名只有函数体,函数可以作为一种类型被赋值给函数类型的变量,匿名函数也往往以变量方式传递。

//定义格式
func(参数列表)(返回参数列表){
    函数体
}

定义时调用匿名函数:直接定义并使用

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

将匿名函数赋值给变量:

// 将匿名函数体保存到f()中
f := func(data int) {
    fmt.Println("hello", data)
}
// 使用f()调用
f(100)

匿名函数的用途非常广泛,它本身就是一种值,可以方便地保存在各种容器中实现回调函数和操作封装。

匿名函数用作回调函数:

用户传入不同的匿名函数体可以实现对元素不同的遍历操作

// 遍历切片的每个元素, 通过给定函数进行元素访问
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)
    })
}

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

package main
import (
    "flag"
    "fmt"
)
//定义命令行参数 skill,将结果传入skillParam指针
var skillParam = flag.String("skill", "", "skill to perform")
func main() {
    flag.Parse()//解析命令行参数
    //字符串映射到函数的map
    //使用匿名函数作为value
    var skill = map[string]func(){
        "fire": func() {
            fmt.Println("chicken fire")
        },
        "run": func() {
            fmt.Println("soldier run")
        },
        "fly": func() {
            fmt.Println("angel fly")
        },
    }
    //在map查找是否有对应命令
    if f, ok := skill[*skillParam]; ok {
        f()
    } else {
        fmt.Println("skill not found")
    }
}

5.6 Go语言函数类型实现接口*

先跳,等接口结束

5.7 Go语言闭包*

Go语言中闭包是引用了自由变量的函数,被引用的自由变量和函数一同存在,即使已经离开了自由变量的环境也不会被释放或者删除,在闭包中可以继续使用这个自由变量,因此,简单的说:

函数 + 引用环境 = 闭包

同一个函数与不同引用环境组合,可以形成不同的实例。引用环境可以指外部变量的引入。

拓展:https://zhuanlan.zhihu.com/p/92634505

在闭包内部修改引用的变量

闭包对它作用域上部的变量可以进行修改,修改引用的变量会对变量进行实际修改。

// 准备一个字符串
str := "hello world"
// 创建一个匿名函数
foo := func() {
    // 匿名函数中访问str
    str = "hello dude"
}
// 调用匿名函数
foo()  //str会被修改

闭包的记忆效应

被捕获到闭包中的变量让闭包本身拥有了记忆效应,闭包中的逻辑可以修改闭包捕获的变量,变量会跟随闭包生命期一直存在,闭包本身就如同变量一样拥有了记忆效应。可以理解为闭包接着变量一直存在下去。

//累加器示例

// 提供一个值, 每次调用函数会指定对值进行累加
func Accumulate(value int) func() int {
    // 返回一个闭包
    return func() int {
        // 累加
        value++
        // 返回一个累加值
        return value
    }
}
func main() {
    // 创建一个累加器, 初始值为1
    accumulator := Accumulate(1)
    // 累加1并打印
    fmt.Println(accumulator())
    fmt.Println(accumulator())
    // 打印累加器的函数地址
    fmt.Printf("%p\n", &accumulator)
    // 创建一个累加器, 初始值为1
    accumulator2 := Accumulate(10)
    // 累加1并打印
    fmt.Println(accumulator2())
    // 打印累加器的函数地址
    fmt.Printf("%p\n", &accumulator2)
}
  • accumulator 与 accumulator2 输出的函数地址不同,因此它们是两个不同的闭包实例。
  • 每调用一次 accumulator 都会自动对引用的变量进行累加。

闭包实现生成器

闭包的记忆效应被用于实现类似于设计模式中工厂模式的生成器,下面的例子展示了创建一个玩家生成器的过程。

// 创建一个玩家生成器, 输入名称, 输出生成器
func playerGen(name string) func() (string, int) {
    // 血量一直为150
    hp := 150
    // 返回创建的闭包
    return func() (string, int) {
        // 将变量引用到闭包中
        return name, hp
    }
}
func main() {
    // 创建一个玩家生成器
    generator := playerGen("high noon")
    // 返回玩家的名字和血量
    name, hp := generator()
    // 打印值
    fmt.Println(name, hp)
}

5.8 Go语言可变参数

类似C语言的printf()函数,Go语言标准库中的 fmt.Println() 等函数的实现也依赖于语言的可变参数功能。

单一类型、数量可变

func myfunc(args ...int) {
    for _, arg := range args {
        fmt.Println(arg)
    }
}

//调用示例:
myfunc(2, 3, 4)
myfunc(1, 3, 7, 13)
  • 函数 myfunc() 接受不定数量的参数,这些参数的类型全部是 int
  • 从内部实现机制来说,类型...type本质上是一个数组切片,也就是[]type,所以可以直接使用range进行遍历。
  • 作为一个语法糖,如果不采用这种方式,完全可以使用数组切片替代。但是这种缺陷使得调用方也得使用切片作为参数传入。
func myfunc2(args []int) {
    for _, arg := range args {
        fmt.Println(arg)
    }
}
//功能完全等价,调用示例
myfunc2([]int{1, 3, 7, 13})

语法糖(syntactic sugar),即这种语法对语言的功能并没有影响,但是更方便程序员使用,通常来说,使用语法糖能够增加程序的可读性,从而减少程序出错的可能。

如果要获取可变参数的数量,可以使用 len() 函数对可变参数变量对应的切片进行求长度操作,以获得可变参数数量。

可变类型、可变数量

如果希望传任意类型,可以指定类型为 interface{}。用 interface{} 传递任意类型数据是Go语言的惯例用法,使用 interface{} 仍然是类型安全的

func MyPrintf(args ...interface{}) {
    for _, arg := range args {
        switch arg.(type) {
            case int:
                fmt.Println(arg, "is an int value.")
            case string:
                fmt.Println(arg, "is a string value.")
            case int64:
                fmt.Println(arg, "is an int64 value.")
            default:
                fmt.Println(arg, "is an unknown type.")
        }
    }
}
func main() {
    var v1 int = 1
    var v2 int64 = 234
    var v3 string = "hello"
    var v4 float32 = 1.234
    MyPrintf(v1, v2, v3, v4)
}

在多个可变参数函数中传递参数

可变参数变量是一个包含所有参数的切片,如果要将这个含有可变参数的变量传递给下一个可变参数函数,可以在传递时给可变参数变量后面添加...,这样就可以将切片中的元素进行传递,而不是传递可变参数变量本身。

// 实际打印的函数
func rawPrint(rawList ...interface{}) {
    // 遍历可变参数切片
    for _, a := range rawList {
        // 打印参数
        fmt.Println(a)
    }
}
// 打印函数封装
func print(slist ...interface{}) {
    // 将slist可变参数切片完整传递给下一个函数
    rawPrint(slist...)	//传递了可变参数
}
func main() {
    print(1, 2, 3)	
}
  • 如果代码修改为rawPrint(slist),则传递进的是切片整体,打印的结果为[1,2,3]

5.9 Go语言defer(延迟执行语句)

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

关键字 defer 的用法类似于面向对象编程语言 Java 和 C# 的 finally 语句块,它一般用于释放某些已分配的资源,典型的例子就是对一个互斥解锁,或者关闭一个文件。

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

以逆序执行,栈顺序。

func main() {
    fmt.Println("defer begin")
    // 将defer放入延迟调用栈
    defer fmt.Println(1)
    defer fmt.Println(2)
    // 最后一个放入, 位于栈顶, 最先调用
    defer fmt.Println(3)
    fmt.Println("defer end")
}
//结果:
//defer begin
//defer end
//3
//2
//1
  • 代码的延迟顺序与最终的执行顺序是反向的。
  • 延迟调用是在 defer 所在函数结束时进行,函数结束可以是正常返回时,也可以是发生宕机时。

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

处理业务或逻辑中涉及成对的操作是一件比较烦琐的事情,比如打开和关闭文件、接收请求和回复请求、加锁和解锁等。在这些操作中,最容易忽略的就是在每个函数退出处正确地释放和关闭资源。

解决了C中函数末尾经常忘记关闭和释放。使用 defer 能非常方便地处理资源释放问题。

  • 使用延迟并发解锁
//未使用defer的情况:
var (
    // 一个演示用的映射
    valueByKey      = make(map[string]int)
    // 保证使用映射时的并发安全的互斥锁,map默认不是并发安全,使用互斥量保护
    valueByKeyGuard sync.Mutex
)
// 根据键读取值
func readValue(key string) int {
    // 对共享资源加锁
    valueByKeyGuard.Lock()
    // 取值
    v := valueByKey[key]
    // 对共享资源解锁
    valueByKeyGuard.Unlock()
    return v
}

//使用defer简化的情况:
func readValue(key string) int {
    valueByKeyGuard.Lock()
    // defer后面的语句不会马上调用, 而是延迟到函数结束时调用
    defer valueByKeyGuard.Unlock()
    return valueByKey[key]
}
  • 使用延迟释放文件句柄
func fileSize(filename string) int64 {
    f, err := os.Open(filename)
    if err != nil {
        return 0
    }
    // 延迟调用Close, 此时Close不会被调用
    defer f.Close()
    info, err := f.Stat()
    if err != nil {
        // defer机制触发, 调用Close关闭文件
        return 0
    }
    size := info.Size()
    // defer机制触发, 调用Close关闭文件
    return size
}
  • defer在函数结束后会自动调用,免去了不同错误情况下可能出现多个文件关闭函数。

5.10 Go语言递归函数

Go语言也支持递归函数,递归函数能解决分而治之的问题。

构成递归需要具备以下条件:

  • 一个问题可以被拆分成多个子问题;
  • 拆分前的原问题与拆分后的子问题除了数据规模不同,但处理问题的思路是一样的;
  • 不能无限制的调用本身,子问题需要有退出递归状态的条件。
//斐波那契数列
func main() {
    result := 0
    for i := 1; i <= 10; i++ {
        result = fibonacci(i)
        fmt.Printf("fibonacci(%d) is: %d\n", i, result)
    }
}
func fibonacci(n int) (res int) {
    if n <= 2 {
        res = 1
    } else {
        res = fibonacci(n-1) + fibonacci(n-2)
    }
    return
}

5.11 Go语言处理运行时错误*

Go语言的错误处理思想及设计包含以下特征:

  • 对于开发者:一个可能造成错误的函数,需要返回值中返回一个错误接口(error),如果调用是成功的,错误接口将返回 nil,否则返回错误。
  • 对于使用者:在函数调用后需要检查错误,如果发生错误,则进行必要的错误处理。

Go语言没有类似 Java 或 .NET 中的异常处理机制,虽然可以使用 defer、panic、recover 模拟,但官方并不主张这样做,Go语言的设计者认为其他语言的异常机制已被过度使用,上层逻辑需要为函数发生的异常付出太多的资源,同时,如果函数使用者觉得错误处理很麻烦而忽略错误,那么程序将在不可预知的时刻崩溃。

Go语言希望开发者将错误处理视为正常开发必须实现的环节,正确地处理每一个可能发生错误的函数,同时,Go语言使用返回值返回错误的机制,也能大幅降低编译器、运行时处理错误的复杂度,让开发者真正地掌握错误的处理。

//net包中的例子
func Dial(network, address string) (Conn, error) {
    var d Dialer
    return d.Dial(network, address)
}
//错误接口的定义格式
type error interface {
    Error() string
}

自定义一个错误

errors包:

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

var err = errors.New("this is an error")


// 创建错误对象
func New(text string) error {
    return &errorString{text}
}
// 错误字符串
type errorString struct {
    s string
}
// 返回发生何种错误
func (e *errorString) Error() string {
    return e.s
}

错误字符串由于相对固定,一般在包作用域声明,应尽量减少在使用时直接使用 errors.New 返回

使用示例:

package main
import (
    "errors"
    "fmt"
)
// 定义除数为0的错误
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))
}

借助自定义结构体实现错误接口*

使用 errors.New 定义的错误字符串的错误类型是无法提供丰富的错误信息的,那么,如果需要携带错误信息返回,就需要借助自定义结构体实现错误接口。

示例:返回错误描述时,带回文件名和行号信息。

package main
import (
    "fmt"
)
// 声明一个解析错误
type ParseError struct {
    Filename string // 文件名
    Line     int    // 行号
}
// 实现error接口,返回错误描述
func (e *ParseError) Error() string {
    return fmt.Sprintf("%s:%d", e.Filename, e.Line)
}
// 创建一些解析错误
func newParseError(filename string, line int) error {
    return &ParseError{filename, line}
}
func main() {
    var e error
    // 创建一个错误实例,包含文件名和行号
    e = newParseError("main.go", 1)
    // 通过error接口查看错误描述
    fmt.Println(e.Error())
    // 根据错误接口具体的类型,获取详细错误信息
    switch detail := e.(type) {
    case *ParseError: // 这是一个解析错误
        fmt.Printf("Filename: %s Line: %d\n", detail.Filename, detail.Line)
    default: // 其他类型的错误
        fmt.Println("other error")
    }
}

http://c.biancheng.net/view/62.html

5.12 Go语言宕机(panic)

Go语言的类型系统会在编译时捕获很多错误,但有些错误只能在运行时检查,如数组访问越界、空指针引用等,这些运行时错误会引起宕机。宕机不是一件很好的事情,可能造成体验停止、服务中断,但是宕机同时也是一种合理的止损方法。

一般而言,当宕机发生时,程序会中断运行,并立即执行在该 goroutine(可以先理解成线程)中被延迟的函数(defer 机制)。随后,程序崩溃并输出日志信息,日志信息包括 panic value 和函数调用的堆栈跟踪信息,panic value 通常是某种错误信息。

对于每个 goroutine,日志信息中都会有与之相对的,发生 panic 时的函数调用堆栈跟踪信息,通常,我们不需要再次运行程序去定位问题,日志信息已经提供了足够的诊断依据,因此,在我们填写问题报告时,一般会将宕机和日志信息一并记录。

虽然Go语言的 panic 机制类似于其他语言的异常,但 panic 的适用场景有一些不同,由于 panic 会引起程序的崩溃,因此 panic 一般用于严重错误,如程序内部的逻辑不一致。任何崩溃都表明了我们的代码中可能存在漏洞,所以对于大部分漏洞,我们应该使用Go语言提供的错误机制,而不是 panic。

手动触发宕机

Go语言可以在程序中手动触发宕机,让程序崩溃,这样开发者可以及时地发现错误,同时减少可能的损失。

Go语言程序在宕机时,会将堆栈和 goroutine 信息输出到控制台,所以宕机也可以方便地知晓发生错误的位置。

package main
func main() {
    panic("crash")
}

//func panic(v interface{})    //panic() 的参数可以是任意类型

在运行依赖的必备资源缺失时主动触发宕机

func MustCompile(str string) *Regexp {
    regexp, error := Compile(str)
    if error != nil {
        panic(`regexp: Compile(` + quote(str) + `): ` + error.Error())
    }
    return regexp
}

手动宕机进行报错的方式不是一种偷懒的方式,反而能迅速报错,终止程序继续运行,防止更大的错误产生,不过,如果任何错误都使用宕机处理,也不是一种良好的设计习惯,因此应根据需要来决定是否使用宕机进行报错。

宕机时触发延迟执行语句

当 panic() 触发的宕机发生时,panic() 后面的代码将不会被运行,但是在 panic() 函数前面已经运行过的 defer 语句依然会在宕机发生时发生作用

package main
import "fmt"
func main() {
    defer fmt.Println("宕机后要做的事情1")
    defer fmt.Println("宕机后要做的事情2")
    panic("宕机")
}

这个特性可以用来在宕机发生前进行宕机信息处理。

5.13 Go语言宕机恢复(recover)

Recover 是一个Go语言的内建函数,可以让进入宕机流程中的 goroutine 恢复过来,recover 仅在延迟函数 defer 中有效

在正常的执行过程中,调用 recover 会返回 nil 并且没有其他任何效果,如果当前的 goroutine 陷入恐慌,调用 recover 可以捕获到 panic 的输入值,并且恢复正常的执行。

通常来说,不应该对进入 panic 宕机的程序做任何处理,但有时,需要我们可以从宕机中恢复,至少我们可以在程序崩溃前,做一些操作,举个例子,当 web 服务器遇到不可预料的严重问题时,在崩溃前应该将所有的连接关闭,如果不做任何处理,会使得客户端一直处于等待状态,如果 web 服务器还在开发阶段,服务器甚至可以将异常信息反馈到客户端,帮助调试。
在其他语言里,宕机往往以异常的形式存在,底层抛出异常,上层逻辑通过 try/catch 机制捕获异常,没有被捕获的严重异常会导致宕机,捕获的异常可以被忽略,让代码继续运行。
Go语言没有异常系统,其使用 panic 触发宕机类似于其他语言的抛出异常,recover 的宕机恢复机制就对应其他语言中的 try/catch 机制。

让程序在崩溃时继续执行*

http://c.biancheng.net/view/64.html

package main
import (
    "fmt"
    "runtime"
)
// 崩溃时需要传递的上下文信息
type panicContext struct {
    function string // 所在函数
}
// 保护方式允许一个函数
func ProtectRun(entry func()) {
    // 延迟处理的函数
    defer func() {
        // 发生宕机时,获取panic传递的上下文并打印
        err := recover()
        switch err.(type) {
        case runtime.Error: // 运行时错误
            fmt.Println("runtime error:", err)
        default: // 非运行时错误
            fmt.Println("error:", err)
        }
    }()
    entry()
}
func main() {
    fmt.Println("运行前")
    // 允许一段手动触发的错误
    ProtectRun(func() {
        fmt.Println("手动宕机前")
        // 使用panic传递上下文
        panic(&panicContext{
            "手动触发panic",
        })
        fmt.Println("手动宕机后")
    })
    // 故意造成空指针访问错误
    ProtectRun(func() {
        fmt.Println("赋值宕机前")
        var a *int
        *a = 1
        fmt.Println("赋值宕机后")
    })
    fmt.Println("运行后")
}

代码输出结果:

运行前
手动宕机前
error: &{手动触发panic}
赋值宕机前
runtime error: runtime error: invalid memory address or nil pointer dereference
运行后

对代码的说明:

  • 第 9 行声明描述错误的结构体,保存执行错误的函数。
  • 第 17 行使用 defer 将闭包延迟执行,当 panic 触发崩溃时,ProtectRun() 函数将结束运行,此时 defer 后的闭包将会发生调用。
  • 第 20 行,recover() 获取到 panic 传入的参数。
  • 第 22 行,使用 switch 对 err 变量进行类型断言。
  • 第 23 行,如果错误是有 Runtime 层抛出的运行时错误,如空指针访问、除数为 0 等情况,打印运行时错误。
  • 第 25 行,其他错误,打印传递过来的错误数据。
  • 第 44 行,使用 panic 手动触发一个错误,并将一个结构体附带信息传递过去,此时,recover 就会获取到这个结构体信息,并打印出来。
  • 第 57 行,模拟代码中空指针赋值造成的错误,此时会由 Runtime 层抛出错误,被 ProtectRun() 函数的 recover() 函数捕获到。

panic和recover的关系

panic 和 recover 的组合有如下特性:

  • 有 panic 没 recover,程序宕机。
  • 有 panic 也有 recover,程序不会宕机,执行完对应的 defer 后,从宕机点退出当前函数后继续执行。

虽然 panic/recover 能模拟其他语言的异常机制,但并不建议在编写普通函数时也经常性使用这种特性。

在 panic 触发的 defer 函数内,可以继续调用 panic,进一步将错误外抛,直到程序整体崩溃。

如果想在捕获错误时设置当前函数的返回值,可以对返回值使用命名返回值方式直接进行设置。

5.14 Go语言一些函数应用

Go语言计算函数执行时间

函数的运行时间的长短是衡量这个函数性能的重要指标。在Go语言中我们可以使用 time 包中的 Since() 函数来获取函数的运行时间。

package main
import (
    "fmt"
    "time"
)
func test() {
    start := time.Now() // 获取当前时间
    sum := 0
    for i := 0; i < 100000000; i++ {
        sum++
    }
    elapsed := time.Since(start)
    fmt.Println("该函数执行完成耗时:", elapsed)
}
func main() {
    test()
}
  • Since() 函数返回从 t 到现在经过的时间,等价于time.Now().Sub(t)
func test() {
    start := time.Now() // 获取当前时间
    sum := 0
    for i := 0; i < 100000000; i++ {
        sum++
    }
    elapsed := time.Now().Sub(start)
    fmt.Println("该函数执行完成耗时:", elapsed)
}

Go语言通过内存缓存来提升性能

当在进行大量计算的时候,提升性能最直接有效的一种方式是避免重复计算,通过在内存中缓存并重复利用缓存从而避免重复执行相同计算的方式称为内存缓存。

其实就是动态规划过程中的信息记忆

Go语言哈希函数

go语言中提供了MD5、SHA-1等几种哈希函数

package main
import (
    "crypto/md5"
    "crypto/sha1"
    "fmt"
)

func main() {
    TestString := "Hi, pandaman!"
    Md5Inst := md5.New()
    Md5Inst.Write([]byte(TestString))
    Result := Md5Inst.Sum([]byte(""))
    fmt.Printf("%x\n\n", Result)

    Sha1Inst := sha1.New()
    Sha1Inst.Write([]byte(TestString))
    Result = Sha1Inst.Sum([]byte(""))
    fmt.Printf("%x\n\n", Result)
}

5.15 Go语言函数的底层实现*

Go语言函数使用的是 caller-save 的模式,即由调用者负责保存寄存器,所以在函数的头尾不会出现push ebp; mov esp ebp这样的代码,相反其是在主调函数调用被调函数的前后有一个保存现场和恢复现场的动作。

http://c.biancheng.net/view/4784.html

5.16 Go语言Test功能测试函数

完善的测试体系,能够提高开发的效率,当项目足够复杂的时候,想要保证尽可能的减少 bug,有两种有效的方式分别是代码审核测试,Go语言中提供了 testing 包来实现单元测试功能。

Go语言自带了 testing 测试包,可以进行自动化的单元测试,输出结果验证,并且可以测试性能。

测试规则

func TestXxx( t *testing.T ){
    //......
}
  • 测试用例文件不会参与正常源码的编译,不会被包含到可执行文件中;
  • 测试用例的文件名必须以_test.go结尾;
  • 需要使用 import 导入 testing 包;
  • 测试函数的名称要以TestBenchmark开头,后面可以跟任意字母组成的字符串,但第一个字母必须大写,例如 TestAbc(),一个测试用例文件中可以包含多个测试函数;
  • 单元测试则以(t *testing.T)作为参数,性能测试以(t *testing.B)做为参数;
  • 测试用例文件使用go test命令来执行,源码中不需要 main() 函数作为入口,所有以_test.go结尾的源码文件内以Test开头的函数都会自动执行。

单元测试

在同一文件夹下创建两个Go语言文件,分别命名为 demo.go 和 demt_test.go:

//demo.go
package demo
// 根据长宽获取面积
func GetArea(weight int, height int) int {
    return weight * height
}
//demo_test.go
package demo
import "testing"
func TestGetArea(t *testing.T) {
    area := GetArea(40, 50)
    if area != 2000 {
        t.Error("测试失败")
    }
}

执行测试命令:

go test -v

单元测试下最好没有main.go

压力测试

//改造demo_test.go
package demo
import "testing"
func BenchmarkGetArea(t *testing.B) {
    for i := 0; i < t.N; i++ {
        GetArea(40, 50)
    }
}

执行测试命令:

go test -bench="."

覆盖率测试

覆盖率测试能知道测试程序总共覆盖了多少业务代码(也就是 demo_test.go 中测试了多少 demo.go 中的代码),可以的话最好是覆盖100%。

//改造demo_test.go
package demo
import "testing"
func TestGetArea(t *testing.T) {
    area := GetArea(40, 50)
    if area != 2000 {
        t.Error("测试失败")
    }
}
func BenchmarkGetArea(t *testing.B) {
    for i := 0; i < t.N; i++ {
        GetArea(40, 50)
    }
}