高质量编程与性能优化 | 豆包MarsCode AI刷题

31 阅读11分钟

1.4.高质量编程与性能优化

1.4.1.什么是高质量

编写的代码能够达到正确可靠、简洁清晰的目标可称之为高质量代码

  • 各种边界条件是否考虑完备
  • 异常情况处理,稳定性保证
  • 易读易维护

编程原则

实际应用场景干变万化,各种语言的特性和语法各不相同但是高质量编程遵循的原则是相通的

简单性

  • 消除"多余的复杂"/以简单清晰的逻辑编写代码
  • 不理解的代码无法修复改进

可读性

  • 代码是写给人看的,而不是机器
  • 编写可维护代码的第一步是确保代码可读

生产力

  • 团队整体工作效率非常重要

1.4.2.如何编写高质量的Go代码

  1. 代码格式
  2. 注释
  3. 命名规范
  4. 控制流程
  5. 错误和异常处理
代码格式

gofmt,Go语言官方提供的工具,能自动格式化Go语言代码为官方统一风格,常见IDE都支持方便的配置

goimports,也是Go语言官方提供的工具,实际等于gofmt加上依赖包管理,自动增删依赖的包引用、将依赖包按字母序排序并分类

注释

注释应该做的

  • 注释应该解释代码作用
    • 适合注释公共符号
  • 注释应该解释代码如何做的
    • 适合注释实现过程
  • 注释应该解释代码实现的原因
    • 适合解释代码的外部因素
    • 提供额外上下文
  • 注释应该解释代码什么情况会出错
    • 适合解释代码的限制条件
  • 公共符号始终要注释
    • 包中声明的每个公共的符号:变量、常量、函数以及结构都需要添加注释
    • 任何既不明显也不简短的公共功能必须予以注释
    • 无论长度或复杂程度如何,对库中的任何函数都必须进行注释
  • 没有必要的注释可以省略,比如简单的实现接口。
//Read 	implements the io.Reader interface
func (r *FileReader) Read(buf []byte) (int ,error)

代码是最好的注释,注释应提供代码未达出的上下文信息

命名规范

1.变量

  • 简洁胜于冗长
  • 缩略词全大写,但当其位于开头且不需出时,使用全刁=小写
    • 例如使用ServeHTTP而不是ServeHttp
    • 使用XMLHTTPRequest或者xmlHTTPRequest
  • 变量距离其被使用的地方,则需带越多的上下文信息
    • 全局变量在其名字中需要更多的上下文信息,使得在不同地方可以轻易辨认出其含义

2.函数

  • 函数名不携带包名的上下文信息,因为包名和函数名总题对出现的
  • 函数名尽量简短
  • 当名为foo的包某个函回类型Foo时,可以省略类型信息而-歧义
  • 当名为foo的包某个函回类型T时(T并不是Foo),可以在函数名中加入类型信息

3.包

  • 只由小写字母组成,不包写字母和下划线等
  • 简短并包含一定的上下文信息。例如schema、task等
  • 不要与标准库同名。例如不要使用sync或者strings

以下规则尽足,以标准库包名为例

  • 不使用常用变量名作为包名。例如使用bufio而不是buf
  • 使用单数而不是。例如使用encoding而不是encodings
  • 谨慎地使用缩写。例如使用 fmt 破坏上下文的情况下比format更加简短
控制流程
  • 线性原理,处理逻辑尽量走直线,避免复杂的嵌套分支
  • 正常流程代码沿着屏幕向下移动
  • 提升代码可维护性和可读性
  • 故障问题大多出现在复杂的条件语句和循环语句中
错误和异常处理

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

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

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

func defaultCheckRedirect(req *Request,via []*Request) error {
    if len(via) >=10 {
        return error.New("stop after 10 redirects")
    }
}

错误的wrap实际上是供了一个 error 嵌套另一个

error的能力,从而生成一个error的跟踪链

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

list,_,err:=c.GetBytes(cache.Subkey(a.actionID,"srcfiles"))
if err !=nil {
    return fmt.Errorf("reading scrfiles list: %w",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,&pathError) {
        fmt.Println("Failed at path:",pathError.Path)
    } else {
        fmt.Println(err)
    }
}

panic

不建议在业务代码中使用panic

调用函数不包含recover会造成程序崩溃

若问题可以被屏蔽或解决,建议使用error代替panic

当程序启动阶段发生不可逆转的错误时,可以在init或main函数中使用panic

func main() {
    ctx,cancel :=context.WithCancel(context.Background())
    client,err :=sarama.NewConsumerGroup(string.Split(brokers,","),
                                        group,config)
    if err !=nil {
        log.Panicf("Error creating consumer group client: %v",err)
    }
}

func Panicf(format string,v...interface{}) {
    s :=fmt.Sprintf(format,v...)
    std.Output(2,5)
    panic(s)
}

recover

recover只能在被defer的函数中使用

嵌套无法生效

只在当前goroutine生效

defer的语句后进先出

func (s *ss) Token(skipSpace bool,f func(rune) bool) (tok []byte,
                                                     err error) {
    defer func() {
        if e:=recover();e!=nil {
            if se,ok :=e.(scanError); ok{
                err=se.err
            } else {
                panic(e)
            }
        }
    }
}

error尽可自是供简明的上下文信息链,方便定位问题

panic用于真正异常的情况

recover生效范围,在当前goroutine的被defer的函数中生效

1.4.3.性能优化

性能优化的前提是满足正确可靠、简洁清晰等质量因素

性能优化是综合评估,有时候时间效率和空间效率可能对立

针对Go语言特性,介绍Go相关的性能优化建议

Benchmark

性能表现需要实际数据衡量

Go语言提供了支持基准性能测试的benchmark工具

在 Go 语言中,基准测试(Benchmark)是一种用于评估代码性能的技术。通过基准测试,你可以测量函数的执行时间,了解代码的性能瓶颈,并进行优化。Go 的 testing 包提供了基准测试的支持,下面详细介绍如何编写和运行基准测试。

1. 编写基准测试

基准测试函数的命名规则是以 Benchmark 开头,并且接受一个 *testing.B 类型的参数。*testing.B 提供了一些方法来控制测试的执行和记录结果。

示例:基准测试一个简单的加法函数

package yourpackage

import (
    "testing"
)

// 被测试的函数
func Add(a, b int) int {
    return a + b
}

// 基准测试函数
func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Add(2, 3)
    }
}

2. 基准测试函数的结构

  • b.Nb.N 是一个整数,表示基准测试函数需要执行的次数。testing 包会自动调整 b.N 的值,以确保测试的时间足够长,从而获得准确的结果。
  • 循环:通常使用一个 for 循环来执行被测试的函数 b.N 次。

3. 运行基准测试

  • 运行所有基准测试
go test -bench=.
  • 运行特定的基准测试
go test -bench=BenchmarkAdd
slice预分配
  • 尽可能在使用make()初始化切片晡是供容量信息,
  • 切片本质是一个数组片段的描述
    • 包括数组指针
    • 片段的长度
    • 片段的容量(不改变内存分配情况下的最大长度)
  • 切片操作并不复制切片指向的元素
  • 创建一个新的切片会复用原来切片的底层数组

另一个陷阱:大内存未释放

在已有切片基础上创建切片,不会创建新的底组

场景

.原切片较大,代码在原切片基础上新建小切片

.原底组在内存中有引用,得不到到释放

可使用copy替换re-slice

map预分配
  • 不断向map中添加元素的操作会触发map的扩容
  • 提前分配间可以减少内存拷贝和Rehash的消耗
  • 建议根据实际需求提前预估好需要的空间
strings.Builder

使用+拼接性能最差,strings.Builder,bytes.Buffer相近,strings.Buffer更快

分析

  • 字符串在Go语言中是不可变类型,占用内存大小是固定的
  • 使用+每次都会重配内存
  • strings.Builder,bytes.Buffer底层都是[]byte数组
  • 内存扩容策略,不需要每次拼接重分配内存
结构体

空结构体struct{}实例不占据可用的内存空间

可作为各种场景下的占位符使用

  • 节省资源
  • 空结构体本身具备很强的语义,即不需要任何值,仅作为占位符
atomic

atomic 包是 Go 语言标准库中的一个包,用于提供原子操作。原子操作是指在多线程环境中不会被中断的操作,这些操作在执行过程中不会被其他线程干扰,因此可以确保数据的一致性和完整性。atomic 包主要用于解决并发编程中的竞态条件(race conditions)问题。

1. 主要功能

atomic 包提供了以下几种类型的原子操作:

  • 整数类型int32int64uint32uint64uintptr
  • 指针类型unsafe.Pointer
  • 布尔类型bool(通过 int32 实现)

2. 常见方法

整数类型

  • Load:读取值。
atomic.LoadInt32(addr *int32) (val int32)
atomic.LoadInt64(addr *int64) (val int64)
  • Store:写入值。
atomic.StoreInt32(addr *int32, val int32)
atomic.StoreInt64(addr *int64, val int64)
  • Swap:交换值。
atomic.SwapInt32(addr *int32, new int32) (old int32)
atomic.SwapInt64(addr *int64, new int64) (old int64)
  • CompareAndSwap:比较并交换值。
atomic.CompareAndSwapInt32(addr *int32, old int32, new int32) (swapped bool)
atomic.CompareAndSwapInt64(addr *int64, old int64, new int64) (swapped bool)
  • Add:增加值。
atomic.AddInt32(addr *int32, delta int32) (new int32)
atomic.AddInt64(addr *int64, delta int64) (new int64)

指针类型

  • Load:读取指针。
atomic.LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)
  • Store:写入指针。
atomic.StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)
  • Swap:交换指针。
atomic.SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer)
  • CompareAndSwap:比较并交换指针。
atomic.CompareAndSwapPointer(addr *unsafe.Pointer, old unsafe.Pointer, new unsafe.Pointer) (swapped bool)

3. 示例

package main

import (
    "fmt"
    "sync"
    "sync/atomic"
)

func main() {
    var counter int32
    var wg sync.WaitGroup

    numGoroutines := 100
    wg.Add(numGoroutines)

    for i := 0; i < numGoroutines; i++ {
        go func() {
            defer wg.Done()
            for j := 0; j < 1000; j++ {
                atomic.AddInt32(&counter, 1)
            }
        }()
    }

    wg.Wait()
    fmt.Println("Final counter value:", counter)
}

在这个示例中,我们使用 atomic.AddInt32 来增加计数器的值,确保在多个 goroutine 并发执行时不会发生竞态条件。

锁的实现通过系统来实现,属于系统调用

atomic操作通过硬件实现,效率比锁高

sync.Mutex应该用来保护一段逻辑,不仅仅用于保护一个变量

对于非数值操作,可以使用atomic.Value,能承载一个interface{}

避免常见的性能陷阱可以保证大部分程序的性能

普通应用的代码,不要一味追求程序的性能

越高级的性能优化手段越容易出现问题

在满足正确可靠、简洁清晰的质量要求的前提下提高程序性能

1.4.4.性能优化实战

优化原则

  • 要依靠数据而不是猜想
  • 要定位最大瓶颈而不是细枝末节
  • 不要过早优化
  • 不要过度优化
pprof
1 cpu
  • 收集 CPU Profile
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30

这条命令会收集 30 秒的 CPU 使用数据,并启动 pprof 交互式 shell。

  • 生成 CPU Profile 文件
go tool pprof -output=profile.out http://localhost:6060/debug/pprof/profile?seconds=30
2 内存分配
  • 收集内存分配数据
Sh


深色版本

go tool pprof http://localhost:6060/debug/pprof/heap

这条命令会收集当前的内存分配数据,并启动 pprof 交互式 shell。

  • 生成内存分配文件
Sh


深色版本

go tool pprof -output=heap.out http://localhost:6060/debug/pprof/heap

这条命令会将当前的内存分配数据保存到 heap.out 文件中。

3 阻塞和互斥锁
  • 收集阻塞数据
Sh


深色版本

go tool pprof http://localhost:6060/debug/pprof/block
  • 生成阻塞数据文件
Sh


深色版本

go tool pprof -output=block.out http://localhost:6060/debug/pprof/block
  • 收集互斥锁数据
Sh


深色版本

go tool pprof http://localhost:6060/debug/pprof/mutex
  • 生成互斥锁数据文件
Sh


深色版本

go tool pprof -output=mutex.out http://localhost:6060/debug/pprof/mutex

这条命令会将 30 秒的 CPU 使用数据保存到 profile.out 文件中。

4.分析性能数据

启动 pprof 交互式 shell 后,你可以使用以下命令来分析数据:

  • top:显示占用 CPU 时间最多的函数。
(pprof) top

  • list:显示指定函数的详细信息。
(pprof) list MyFunction

  • web:生成可视化图表(需要安装 graphviz)。
(pprof) web

  • pdf:生成 PDF 文件。
(pprof) pdf
  • svg:生成 SVG 文件。
(pprof) svg