高质量编程 | 青训营笔记

131 阅读3分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 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-126156344218.77 ns/op
BenchmarkSelectParallel-121903481679.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.As

    if _, 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 库, 实在太多了, 挺魔幻的

五、引用参考: