高质量编程
高质量代码:
- 边界条件
- 异常处理,稳健性
- 简洁清晰
编码规范
- 注释
- 公共符号(变量 常量 函数 结构)始终要注释
- 解释代码的作用
- 解释代码如何做的
- 解释代码实现的原因
- 解释代码为什么会报错
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:
错误处理:
简单错误
简单的错误指的是仅出现一次的错误,且在其他地方不需要捕获该错误
·优先使用 errors.New 来创建匿名变量来直接表示简单错误
如果有格式化的需求,使用 fmt.Errorf
Wrap和Unwrap 错误的 Wrap 和 Unwrap
错误的 Wrap 实际上是提供了一个 error 嵌套另一个error 的能力,从而生成一个 error 的跟踪链
在 fmt.Errorf 中使用:%w 关键字来将一个错误关联至错误链中
错误判定 在 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.Errorf 和 errors.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.Is 或 errors.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. panic 和 recover
在 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:将错误链中的错误转换为特定的错误类型。panic和recover:用于处理 Go 中的严重错误,通过panic触发错误,recover捕获错误。
性能优化指南
Benchmark用法
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
使用空结构体占位
节省内存+增加语义
使用Atomic包
性能调优
性能分析工具pprof 知道什么地方用了多少CPU,多少memory