这是我参与「第五届青训营 」伴学笔记创作活动的第 3 天
一、本堂课重点内容
-
测试
- 单元测试
- 基准测试
- 回归测试
- 测试覆盖率
-
mock
- monkey
-
bench
- 性能分析方法
- 优化
-
高质量编程
- 代码格式
- 注释
- 命名规范
- 控制流程
- 错误和异常处理
二、详细知识点介绍:
测试
覆盖率计算方法
go test xxx -cover
计算测试代码会执行的代码行数 / 代码总行数
以下面的代码为例:
func is(a int) bool {
if a > 6 {
return true
}
return false
}
func Testis(t *testing.T) {
assert.Equal(t, true, is(7))
}
mock 及打桩
打桩是通过运行时替换函数地址实现的
下面的用例对 ReadFirstLine 打桩, 让他永远返回 'line101', 这样就能让测试不依赖于本地文件
func TestProcessFirstLineWithMock(t *testing.T) {
monkey.Patch (ReadFirstLine, func() string {
return "Line110"
])
defer monkey.Unpatch (ReadFirstLine)
line := ProcessFirstLine()
assert.Equal(t, "line000", line)
}
基准测试
测试性能
案例:
func BenchmarkSelect (b *testing.B) {
InitServerIndex()
b.ResetTimer ()
for i := 0; i < b.N; i++ {
SelectServer()
}
}
func BenchmarkSelectParallel(b *testing.B) {
InitServerIndex()
b.ResetTimer()
b.RunParallel(func (pb *testing.PB) {
for pb.Next() {
SelectServer()
}
})
}
go test -bench=.
| 测试用例 | cpu 执行时间 | |
|---|---|---|
| BenchmarkSelect-12 | 61563442 | 18.77 ns/op |
| BenchmarkSelectParallel-12 | 19034816 | 79.42 ns/op |
这里发现并行的执行, 性能反而变差, 原因是并行测情况下, Select 函数用到了 rand 函数, 为了保证全局的随机性和并发安全, 使用了一把全局锁
可以使用 [fastrand] 库代替
高质量编程
- 代码格式
- 注释
- 命名规范
- 控制流程
- 错误和异常处理
单例模式的初始化
保证示例只会被创建一次
var (
topicDao *TopicDao
topicOnce sync.Once
)
func NewTopicDaoInstance () *TopicDao {
topicOnce.Do(func() {
topicDao = &TopicDao{)
})
return topicDao
}
Service 设计
考虑调用 repo, service 之间的并行性 如果可以并行执行, 尝试使用 waitgroup
错误处理
错误的 Wrap 和 Unwrap
Go1.13 在 errors 中新增了三个新 API 和一个新的 format 关键字,分别是 errors.Is , errors.As, errors.Unwrap 以及 fmt.Errorf 的 %w。
- 错误的 Wrap 实际上是提供了一个 error 嵌套另一个 error 的能力,从而生成一个 error 的跟踪链
- 在fmt.Errorf 中使用:%w 关键字来将一个错误关联至错误链中
list, -, err := c. GetBytes (cache.Subkey(a.actionID, "srcfiles" ) )
if err != nil {
return fmt. Errorf("reading srcfiles list: Sw", err)
}
错误判定
-
判定一个错误是否为特定错误,使用
errors.Is不同于使用
==,使用该方法可以判定错误链上的所有错误是否含有特定的错误data, err = lockedfile. Read(targ) if errors.Is(err, fs.ErrNotExist) { return []byte{}, nil } return data, err -
在错误链上获取特定种类的错误,使用
errors.Asif _, err := os.Open( "non-existing"); err != nil { var pathError *fs.PathError if errors.As(err, SpathError) { fmt.Println("Failed at path:", pathError. Path) } else { fmt.Println(err) } }
Panic 和 Recover
Panic
- 一般在程序启动时如果发生不可逆转的错误时抛出
Recover
- 只能在 defer 中使用
- 一般用来解决第三方库抛出影响系统自身逻辑时使用
- 只在当前 goroutine 生效
三、实践练习例子:
过去的 errors 太过简单, 但是错误的嵌套很常见
如果想要判断错误类型, 一般需要这样:
type NotFoundError struct {
Name string
}
func (e *NotFoundError) Error() string { return e.Name + ": not found" }
if e, ok := err.(*NotFoundError); ok {
// e.Name wasn't found
}
如果要嵌套错误信息只能:
if err != nil {
return fmt.Errorf("decompress %v: %v", name, err)
}
而在 go 1.13 之后我们可以直接使用 %w 嵌套错误:
if err != nil {
// Return an error which unwraps to err.
return fmt.Errorf("decompress %v: %w", name, err)
}
并使用 errors.Is, errors.As 做错误类型的判断和转换
err := fmt.Errorf("access denied: %w", ErrPermission)
...
if errors.Is(err, ErrPermission) ...
四、课后个人总结:
Go 的 errors 没想到是这样的. 反观 Rust 的 error 库, 实在太多了, 挺魔幻的
五、引用参考:
- Go 1.13 Error go.dev/blog/go1.13…