go语言函数详解-02

go语言defer(延迟执行语句)

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

处理业务或逻辑中涉及成对的操作是一件比较烦琐的事情,比如打开和关闭文件、接收请求和回复请求、加锁和解锁等。在这些操作中,最容易忽略的就是在每个函数退出处正确地释放和关闭资源。
defer 语句正好是在函数退出时执行的语句,所以使用 defer 能非常方便地处理资源释放问题。

  1. 使用延迟并发解锁
    未使用defer语句时:
var (
	valueByKey = map[string]int{}
	valueByKeyGuard sync.Mutex
)
func readValue(key string) int {
	valueByKeyGuard.Lock()
	value := valueByKey[key]
	valueByKeyGuard.Unlock()
	return value
}

使用defer语句时:

var (
	valueByKey = map[string]int{}
	valueByKeyGuard sync.Mutex
)
func readValue(key string) int {
	valueByKeyGuard.Lock()
	defer valueByKeyGuard.Unlock()
	return valueByKey[key]
}
  1. 使用延迟释放文件句柄
    文件的操作需要经过打开文件、获取和操作文件资源、关闭资源几个过程,如果在操作完毕后不关闭文件资源,进程将一直无法释放文件资源。

在下面的例子中将实现根据文件名获取文件大小的函数,函数中需要打开文件、获取文件大小和关闭文件等操作,由于每一步系统操作都需要进行错误处理,而每一步处理都会造成一次可能的退出,因此就需要在退出时释放资源,而我们需要密切关注在函数退出处正确地释放文件资源,参考下面的代码:

未使用defer语句

func fileSize(filename string) int64 {
	f, err := os.Open("test.txt")
	if err != nil {
		return 0
	}
	info, err := f.Stat()
	if err != nil {
		f.Close()
		return 0
	}
	size := info.Size()
	f.Close()
	return size
}

使用defer语句

func main() {
	fmt.Println(fileSize("test.txt"))
}
func fileSize(filename string) int64 {
	f, err := os.Open(filename)
	if err != nil {
		return 0
	}
	defer func() {
		_ = f.Close()
	}()
	info, err := f.Stat()
	if err != nil {
		// defer机制触发,调用Close关闭文件
		return 0
	}
	// defer机制触发,调用Close关闭文件
	return info.Size()
}

代码中加粗部分为对比前面代码而修改的部分,代码说明如下:

第 10 行,在文件正常打开后,使用 defer,将 f.Close() 延迟调用,注意,不能将这一句代码放在第 4 行空行处,一旦文件打开错误,f 将为空,在延迟语句触发时,将触发宕机错误。
第 16 行和第 22 行,defer 后的语句(f.Close())将会在函数返回前被调用,自动释放资源。

go语言递归函数

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

  • 一个问题可以被拆分成多个子问题
  • 拆分前的原问题和拆分后的子问题除了数据规模不同,但处理问题思路是一样的
  • 不能无限制的调用本身,子问题需要有退出递归状态的条件
斐波那切数列
func main() {
	fmt.Println(fibonacci(10))
}
func fibonacci(n int) int {
	if n < 2 {
		return n
	}
	return fibonacci(n-2) + fibonacci(n-1)
}
数字阶乘
func main() {
	fmt.Println(Factorial(10))
}
func Factorial(n int) int {
	if n == 0 {
		return 1
	}
	return n * Factorial(n-1)
}

go语言处理运行时错误

go语言的错误处理思想及设计包含如下特性:

  • 一个可能造成错误的函数,需要返回值中返回一个错误接口(error),如果调用是成功的,错误接口将返回nil,否则返回错误
  • 在函数调用后需要检查错误,如果发生错误,则进行必要的错误处理
    Go语言希望开发者将错误处理视为正常开发必须实现的环节,正确地处理每一个可能发生错误的函数,同时,Go语言使用返回值返回错误的机制,也能大幅降低编译器、运行时处理错误的复杂度,让开发者真正地掌握错误的处理。
自定义一个错误

var err = errors.New("this is an error")
错误字符串由于相对固定,一般在包作用域声明,应尽量减少在使用时直接使用 errors.New返回。

// 定义除数为0的错误
var errDivisionByZero = errors.New("0不能当做除数")
func main() {
	fmt.Println(divide(15, 0))
}
func divide(v1, v2 int) (float64, error) {
	if v2 == 0 {
		return 0, errDivisionByZero
	}
	return float64(v1/v2), nil
}
在解析中使用自定义错误

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

下面代码将实现一个解析错误(ParseError),这种错误包含两个内容,分别是文件名和行号,解析错误的结构还实现了 error 接口的 Error() 方法,返回错误描述时,就需要将文件名和行号返回。

func main() {
	var e error
	// 创建一个错误实例,包含文件名和行号
	e = NewParseError("main", 88)
	// 通过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("未知错误类型")
	}
}
// ParseError 解析错误
type ParseError struct {
	Filename string  // 文件名
	Line int  // 行号
}
func (p *ParseError) Error() string {
	return fmt.Sprintf("%s:%d", p.Filename, p.Line)
}
func NewParseError(filename string, line int) *ParseError {
	return &ParseError{filename, line}
}

错误对象都要实现error接口的Error()方法,这样,所有的错误都可以获得字符串的描述,如果想进一步知道错误的详细信息,可以通过类型断言,将错误对象转换为具体的错误类型,进行错误详细信息的获取。

go语言宕机(panic)

go语言的类型系统会在编译时捕获很多错误,但有些错误只能在运行时检查,如:数组访问越界,空指针引用等;

go语言程序在宕机时,会将堆栈和goroutine信息输出到控制台,

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

在宕机时触发延迟执行语句

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

func main() {
	defer fmt.Println("123456")
	defer fmt.Println("dafsjfsalkja")
	panic("宕机了")
	defer fmt.Println("哈哈哈哈")
}

宕机前,defer 语句会被优先执行,由于第 7 行的 defer 后执行,因此会在宕机前,这个 defer 会优先处理,随后才是第 6 行的 defer 对应的语句,这个特性可以用来在宕机发生前进行宕机信息处理。

go语言宕机恢复--recover()

Recover 是一个Go语言的内建函数,可以让进入宕机流程中的 goroutine 恢复过来,recover 仅在延迟函数 defer 中有效,在正常的执行过程中,调用 recover 会返回 nil 并且没有其他任何效果,如果当前的 goroutine 陷入恐慌,调用 recover 可以捕获到 panic 的输入值,并且恢复正常的执行。

go语言没有异常系统,其使用panic触发宕机类似于其它语言的异常,recover的宕机恢复机制就类似于其它语言的try/catch机制

让程序在崩溃时继续执行

下面的代码实现了 ProtectRun() 函数,该函数传入一个匿名函数或闭包后的执行函数,当传入函数以任何形式发生 panic 崩溃后,可以将崩溃发生的错误打印出来,同时允许后面的代码继续运行,不会造成整个进程的崩溃。

保护运行函数:

type panicContext struct {
	function string  // 所在函数
}
func main() {
	// 手动触发宕机
	ProtectRun(func() {
		fmt.Println("手动宕机前")
		panic(&panicContext{"手动宕机了"})
		fmt.Println("手动宕机后")
	})

	// 空指针赋值宕机
	ProtectRun(func() {
		fmt.Println("赋值宕机前")
		var a *int
		*a = 1
		fmt.Println("赋值宕机后")
	})

	// 数组访问越界宕机
	ProtectRun(func() {
		fmt.Println("数组越界宕机前")
		a := []int{11, 22}
		fmt.Println(a[6])
		fmt.Println("数组越界宕机后")
	})
}
// ProtectRun 保护方式允许一个函数
func ProtectRun(entry func()) {
	defer func() {
		err := recover()
		fmt.Println(err)
		switch err.(type) {
		case runtime.Error:
			fmt.Println("运行时错误")
		default:
			fmt.Println("error:", err)
		}
	}()
	entry()
}
panic和recover的关系
  • 有panic没recover程序宕机
  • 有panic也有recover程序不会宕机,执行完对应的defer后,从宕机点退出当前函数后继续执行。

go语言计算函数执行时间

在Go语言中我们可以使用 time 包中的Since() 函数来获取函数的运行时间,Go语言官方文档中对 Since() 函数的介绍是这样的。
func Since(t Time) Duration
Since() 函数返回从 t 到现在经过的时间,等价于time.Now().Sub(t)。

使用Since()函数获取函数的运行时间
func main() {
	test()
}
func test() {
	start := time.Now()
	sum := 0
	for i := 0; i < 100000000; i++ {
		sum++
	}
	fmt.Println("函数执行时间:", time.Since(start))
}
使用time.Now.Sub()函数获取函数的运行时间
func main() {
	test()
}
func test() {
	start := time.Now()
	sum := 0
	for i := 0; i < 100000000; i++ {
		sum++
	}
	fmt.Println("函数执行时间:", time.Now().Sub(start))
}

由于计算机 CPU 及一些其他因素的影响,在获取函数运行时间时每次的结果都有些许不同,属于正常现象。

go语言Test功能测试函数

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

为什么需要测试

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

测试规则

要开始一个单元测试,需要准备一个 go 源码文件,在命名文件时文件名必须以_test.go结尾,单元测试源码文件可以由多个测试用例(可以理解为函数)组成,每个测试用例的名称需要以 Test 为前缀,例如:

func TestXxx( t *testing.T ){
    //......
}

编写测试用例有以下几点需要注意:

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

Go语言的 testing 包提供了三种测试方式,分别是单元(功能)测试、性能(压力)测试和覆盖率测试。

单元(功能)测试

在同一文件夹下创建两个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("测试失败")
    }
}

执行测试命令,运行结果:

PS C:\Users\mayanan\Desktop\pro_go\test_go\unit_test> go test -v
=== RUN   TestGetArea
--- PASS: TestGetArea (0.00s)
PASS                         
ok      mayanan/unit_test       0.791s
性能(压力)测试

将 demo_test.go 的代码改造成如下所示的样子:

func BenchmarkGetArea(t *testing.B) {
	for i := 0; i < t.N; i++ {
		GetArea(40, 50)
	}
}

执行测试命令,运行结果如下:

PS C:\Users\mayanan\Desktop\pro_go\test_go\unit_test> go test -bench="."
goos: windows
goarch: amd64
pkg: mayanan/unit_test
cpu: Intel(R) Core(TM) i5-6200U CPU @ 2.30GHz
BenchmarkGetArea-4      1000000000               0.4150 ns/op
PASS
ok      mayanan/unit_test       1.155s

上面信息显示了程序执行 1000000000 次,共耗时 0.415 纳秒。

覆盖率测试

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

将 demo_test.go 代码改造成如下所示的样子:

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

执行测试命令,运行结果如下:

PS C:\Users\mayanan\Desktop\pro_go\test_go\unit_test> go test -cover
PASS
coverage: 100.0% of statements
ok      mayanan/unit_test       0.896s
posted @ 2022-08-31 15:08  专职  阅读(62)  评论(0编辑  收藏  举报