青训营第三课 | 高质量编程与性能调优实战

46 阅读7分钟

高质量编程

高质量代码:

  • 边界条件
  • 异常处理,稳健性
  • 简洁清晰

编码规范

  1. 注释
  • 公共符号(变量 常量 函数 结构)始终要注释
  • 解释代码的作用

image.png

  • 解释代码如何做的

image.png

  • 解释代码实现的原因

image.png

  • 解释代码为什么会报错

image.png 2. 代码格式:使用golang的gofmt tools->action on save -> reformat code goimports 删除无用依赖 按照字母顺序排列依赖

命名规范: 变量: 缩略词全大写,但是当缩略词位于开头且不需要导出时,全小写

函数: 函数名不携带包名的上下文信息,因为包名和函数名总是成对出现的函数名尽量简短

当名为 foo 的包某个函数返回类型 Foo时,可以省略类型信息而不导致歧义

当名为 foo 的包某个函数返回类型T时(T并不是 Foo),可以在函数名中加入类型信息http 包中创建服务的函数如何命名更好?

func Serve(l net. Listener, handlerHandler) error 
func ServeHTTP(I net. Listener, handler Handler) error

第一个好,因为http已经携带了信息,调用的时候是用http.Serve,已经带有了package名的上下文,所以不需要指定http

包:

  • 只由小写字母组成。不包含大写字母和下划线等字符

  • 简短并包含一定的上下文信息。例如 schema、task 等

  • 不要与标准库同名。例如不要使用 sync 或者 strings以下规则尽量满足,以标准库包名为例

  • 不使用常用变量名作为包名。例如使用 bufio 而不是 buf使用单数而不是复数。例如使用 encoding 而不是 encodings

  • 谨慎地使用缩写。例如使用 fmt 在不破坏上下文的情况下比 format 更加简短

控制流程

  • 尽量保持正常代码路径为最小缩进,优先处理错误情况/特殊情况,尽早返回或继续循环来减少嵌套 bad:

image.png

错误处理:

简单错误

简单的错误指的是仅出现一次的错误,且在其他地方不需要捕获该错误

·优先使用 errors.New 来创建匿名变量来直接表示简单错误

如果有格式化的需求,使用 fmt.Errorf

image.png

Wrap和Unwrap 错误的 Wrap 和 Unwrap

错误的 Wrap 实际上是提供了一个 error 嵌套另一个error 的能力,从而生成一个 error 的跟踪链

在 fmt.Errorf 中使用:%w 关键字来将一个错误关联至错误链中

image.png

错误判定 在 Go 语言中,错误处理是通过返回错误值而不是使用异常来实现的。这种方式更明确地表达了程序中的错误控制流程。以下是您提到的各种错误处理方法的详细说明:

1. errors.New

errors.New 是 Go 标准库中的一个函数,用于创建一个简单的错误对象。这个错误对象是实现了 error 接口的值,包含一个描述错误的字符串。

import "errors"

err := errors.New("this is an error message")
fmt.Println(err) // 输出:this is an error message

2. fmt.Errorferrors.Wrap

Go 中可以通过 fmt.Errorf 格式化错误信息,还可以使用 errors.Wrap(在第三方包 github.com/pkg/errors 中)来为原始错误添加上下文信息。这种方式可以提供错误的更多信息,并保留原始错误信息,方便后续的调试和定位。

import (
    "errors"
    "fmt"
)

func readFile() error {
    return errors.New("file not found")
}

func openFile() error {
    err := readFile()
    if err != nil {
        return fmt.Errorf("openFile failed: %w", err)
    }
    return nil
}

err := openFile()
if err != nil {
    fmt.Println(err) // 输出:openFile failed: file not found
}

在这个例子中,我们使用了 %w,这是 fmt.Errorf 用于嵌套原始错误的一种方式,便于稍后使用 errors.Iserrors.As 进行错误类型判断。

3. errors.Is

errors.Is 用于检查错误链中是否包含特定错误。使用它可以判断一个错误是否是特定类型的错误,或者是否包含特定的原始错误。

import (
    "errors"
    "fmt"
)

var ErrNotFound = errors.New("not found")

func findItem() error {
    return fmt.Errorf("failed to find item: %w", ErrNotFound)
}

err := findItem()
if errors.Is(err, ErrNotFound) {
    fmt.Println("Error is ErrNotFound") // 匹配成功
}

在这里,errors.Is 检查 err 是否包含 ErrNotFound。这种模式在处理由多个函数封装的错误时非常有用。

4. errors.As

errors.As 用于将错误链中某个错误转换为特定的错误类型。当您定义了实现 error 接口的自定义错误类型时,可以使用 errors.As 提取该类型的错误。

import (
    "errors"
    "fmt"
)

type MyError struct {
    Msg string
}

func (e *MyError) Error() string {
    return e.Msg
}

func doSomething() error {
    return &MyError{Msg: "something went wrong"}
}

err := doSomething()
var myErr *MyError
if errors.As(err, &myErr) {
    fmt.Println("Caught MyError:", myErr.Msg) // 匹配成功并获取 Msg
}

在此例中,errors.As 用于检查 err 是否为 MyError 类型,并将其转换为 myErr 以便于后续处理。

5. panicrecover

在 Go 中,panic 用于表示程序遇到严重的错误而无法继续运行。panic 会终止当前函数的执行,并递归地向上传递到调用栈的顶层,直到程序崩溃(或者在顶层通过 recover 捕获)。

  • panic:立即停止当前的控制流,并开始沿调用栈向上传递。
  • recover:用于从 panic 中恢复并继续执行,通常在 defer 函数中使用。
func mayPanic() {
    panic("something bad happened") // 引发 panic
}

func main() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered from panic:", r)
        }
    }()

    mayPanic()
    fmt.Println("This will not run if panic is not recovered")
}

在这个示例中,mayPanic 函数中引发了 panic,但由于在 main 函数中使用了 recover,程序捕获了这个 panic 并避免崩溃,从而能够输出 "Recovered from panic: something bad happened"


  • errors.New:创建简单的错误信息。
  • fmt.Errorf:格式化错误信息,并可以嵌套原始错误。
  • errors.Is:检查错误链中是否存在特定的错误。
  • errors.As:将错误链中的错误转换为特定的错误类型。
  • panicrecover:用于处理 Go 中的严重错误,通过 panic 触发错误,recover 捕获错误。

性能优化指南

Benchmark用法

image.png

Slice优化

在 Go 中,slice 使用灵活,但为了优化内存性能,尤其是在大量数据或高频操作时,理解和注意其底层实现是非常重要的。以下是一些提升 slice 内存性能的优化指南:

1. 预分配容量

slice 容量不足时,每次调用 append 可能会导致底层数组的重新分配和复制。提前分配足够的容量可以减少内存重新分配次数,提高性能。

  • 方法:使用 make 创建一个有足够容量的 slice,避免多次扩容。

    s := make([]int, 0, 1000) // 提前分配1000个元素的容量
    for i := 0; i < 1000; i++ {
        s = append(s, i)
    }
    

在循环中每次 append 都使用了提前分配的空间,避免了反复扩容。

2. 减少切片长度的扩展

当切片的长度需要扩大到超过其容量时,Go 会分配一个新数组,通常是当前容量的两倍,并将原始数组复制过去。频繁的扩展会增加内存和时间的开销。因此,能预估到大致的大小时,尽量减少动态扩展。

  • 优化方法:按需设置初始容量;避免频繁 append,特别是在不确定的情况下。

3. 大内存未释放问题及优化方案

  • 问题描述:在已有切片的基础上创建切片,不会创建新的底层数组,导致内存得不到释放。

  • 场景

    • 原切片较大,代码在原切片基础上新建小切片。
    • 底层数组在内存中有引用,得不到释放。
  • 解决方案:可以使用 copy 替代 re-slice,以避免引用原底层数组。

    示例代码

    • GetLastBySlice:使用 re-slice 方法返回最后两个元素,内存得不到释放。
    • GetLastByCopy:使用 copy 方法将最后两个元素复制到新切片,避免引用原底层数组。
func GetLastBySlice(origin []int) []int {
    return origin[len(origin)-2:]
}

func GetLastByCopy(origin []int) []int {
    result := make([]int, 2)
    copy(result, origin[len(origin)-2:])
    return result
}

内存测试代码

  • 测试代码
    • 测试不同方法的内存占用情况。
    • 使用 generateWithCap(128 * 1024) 生成大容量切片。
    • TestLastBySlice 内存占用为 100.14 MB。
    • TestLastByCopy 内存占用为 3.14 MB。

运行测试命令

go test -run=. -v

Map优化

提前make分配内存给map,减少map反复扩容的overhead data := make(map[int] int, size)

字符换处理

使用strings.Builder

使用空结构体占位

节省内存+增加语义

image.png

使用Atomic包

image.png

性能调优

性能分析工具pprof 知道什么地方用了多少CPU,多少memory