Go 100个容易犯的错误总结(下篇)

函数和方法

不清楚接收者使用哪种类型

直接说结论:所有的参数类型 receiver 都使用指针为好

理由如下:

  • 如果方法需要修改 receiver,并且 receiver 是切片,需要追加元素
Go 复制代码
type slice []int

func (s *slice) add(element int) {
    *s = append(*s, element)
}
  • 如果 receiver 是一个大的结构体,使用指针就会避免值拷贝
  • 如果 receiver 包含一个字段无法被复制,比如最常见的 sync.Mutex
Go 复制代码
type Counter struct {
    mu       sync.Mutex
    counters map[string]int
}

具名结果参数的注意事项

有的时候我们定义函数的时候返回值会加上一个具名的字段,比如下面接口里面的方法定义,就能看到这个里面的函数的具体每个结果返回的含义是什么

Go 复制代码
type locator interface {
    getCoordinates(address string) (lat, lng float32, err error)
}

但是需要注意具名结果参数的坑,比如下面

Go 复制代码
func (l loc) getCoordinates(ctx context.Context, address string) (
    lat, lng float32, err error) {
    isValid := l.validateAddress(address) (1)
    if !isValid {
        return 0, 0, errors.New("invalid address")
    }

    if ctx.Err() != nil { (2)
        return 0, 0, err
    }

    // Get and return coordinates
}

上面的 (2) 里面我们其实已经逻辑出错了,但是 err 并没有赋任何值,导致上游的调用其实返回的 error 是一个空的 nil

所以,请务必记住下面这两个使用逻辑

  • 具名参数限制在接口里面的方法使用,让使用者一下就能知道方法的每个参数的定义,提高可读性
  • 返回的任何 error 都应该加上具体的一些信息,比如上面的可以修改成
Go 复制代码
if ctx.Err() != nil { (2)
    return 0, 0, fmt.Errof("ctx err %w",err)
}
  • 非接口里面的方法请不要使用具名的参数,避免上面的错误导致自己给自己挖坑

返回一个 nil 的接收者

返回接口时,注意不要返回一个 nil 的指针,而是应该返回显示的 nil 值

Go 复制代码
type MultiError struct {
    errs []string
}

func (m *MultiError) Add(err error) {
    m.errs = append(m.errs, err.Error())
}

func (m *MultiError) Error() string {
    return strings.Join(m.errs, ";")
}

type Customer struct {
    Age  int
    Name string
}

// 错误用法
func (c Customer) Validate1() error {
    var m *MultiError

    if c.Age < 0 {
        m = &MultiError{}
        m.Add(errors.New("age is negative"))
    }
    if c.Name == "" {
        if m == nil {
            m = &MultiError{}
        }
        m.Add(errors.New("name is nil"))
    }
    
    // 这里没有判断对应的m是否为nil,所以返回的error永远不为nil
    return m
}

// 正确用法
func (c Customer) Validate2() error {
    var m *MultiError

    if c.Age < 0 {
        m = &MultiError{}
        m.Add(errors.New("age is negative"))
    }
    if c.Name == "" {
        if m == nil {
            m = &MultiError{}
        }
        m.Add(errors.New("name is nil"))
    }

    if m != nil {
        return m
    }
    return nil
}

func main() {
    customer := Customer{Age: 33, Name: "John"}
    if err := customer.Validate1(); err != nil {
        log.Fatalf("customer is invalid: %v", err)
    }
}

使用文件名来作为参数参数

函数参数使用 io.Reader 而不是文件的名字能够很大程度上提高函数的复用性,并且能使得单元测试更加方便

Go 复制代码
func countEmptyLinesInFile(filename string) (int, error) {
    file, err := os.Open(filename)
    if err != nil {
        return 0, err
    }
    // Handle file closure

    scanner := bufio.NewScanner(file)
    for scanner.Scan() {
        // ...
    }

    return 0, nil
}

func countEmptyLines(reader io.Reader) (int, error) {
    scanner := bufio.NewScanner(reader)
    for scanner.Scan() {
        // ...
    }
    return 0, nil
}

func main() {
    file, err := os.Open("main.go")
    if err != nil {
        panic(err)
    }
    _, _ = countEmptyLines(file)
}

对于上面第一个函数,复用性低体现在

  1. 传入的只能是一个文件名,表明只能读取文件流,如果业务输入是一个字符串、HTTP请求、gRPC请求呢,岂不是每个都新加一个函数

  2. 单元测试不能mock,因为每次都是读取一个实际的文件;相比第二个函数,便可以进行 io.Reader 接口的 mock 即可

不清楚defer的函数调用方式

defer 的函数参数是在函数注册的时候的确定下来了,所以如果是值拷贝一定要注意后面的逻辑是否对这个值有修改

Go 复制代码
const (
    StatusSuccess  = "success"
    StatusErrorFoo = "error_foo"
    StatusErrorBar = "error_bar"
)

// 错误示范,下面的 defer 里面的 status 一定为空,即使后面就行了修改
func f1() error {
    var status string
    defer notify(status)
    defer incrementCounter(status)

    if err := foo(); err != nil {
        status = StatusErrorFoo
        return err
    }

    if err := bar(); err != nil {
        status = StatusErrorBar
        return err
    }

    status = StatusSuccess
    return nil
}

// 正确示范:直接传递指针,修改指针指向的值也能生效
func f2() error {
    var status string
    defer notifyPtr(&status)
    defer incrementCounterPtr(&status)

    if err := foo(); err != nil {
        status = StatusErrorFoo
        return err
    }

    if err := bar(); err != nil {
        status = StatusErrorBar
        return err
    }

    status = StatusSuccess
    return nil
}

// 正确示范:使用闭包,闭包里面的函数仍然可以使用值传递
func f3() error {
    var status string
 defer  func () {
notify(status)
incrementCounter(status)
}()

    if err := foo(); err != nil {
        status = StatusErrorFoo
        return err
    }

    if err := bar(); err != nil {
        status = StatusErrorBar
        return err
    }

    status = StatusSuccess
    return nil
}

func notify(status string) {
    fmt.Println("notify:", status)
}

func incrementCounter(status string) {
    fmt.Println("increment:", status)
}

func notifyPtr(status *string) {
    fmt.Println("notify:", *status)
}

func incrementCounterPtr(status *string) {
    fmt.Println("increment:", *status)
}

func foo() error {
    return nil
}

func bar() error {
    return nil
}

错误处理

Panicking

panic 要慎重使用,使用场景就如下两种情况

  1. 一些致命性的错误,比如数据库注册失败,sql.Register 返回错误,或者已经注册
  2. 无法创建依赖关闭,比如上游调用强依赖下游,比如依赖某个配置文件的读取,但是读取失败,上游应该 panic 报错

除去上面的两种情况,此外的所有情形都应该尽量避免panic,而���返回特定的 error 来进行判断处理

Error 的包装

从 Go 1.13 开始,提供了一个 %w 来进行 error 的包装,Go 语言里面的错误应该尽量都使用 %w 进行错误的包装,并提供必要的信息,比如:

Go 复制代码
func test() error {
    err = sql.Register()
    if err != nil {
        return fmt.Error("sql register error %w", err)
    }
}

Error 比较

从 Go 1.13 开始,error 的类型的判断请使用 As,判断是否是特定的 error 请使用 Is,不要使用 ==

Go 复制代码
func query() error {
    return nil
}

func listing2() {
    err := query()
    if err != nil {
        if errors.Is(err, sql.ErrNoRows) {
            // ...
        } else {
            // ...
        }
    }
}

正确处理 Error

记住一个法则:error 只应该被处理一次,打印 error 日志也算是处理 error;不要处理 error 多次

  • 要么打印 error
  • 要么 wrap error 返回上层

好的 error 处理是只打印一次 error 日志,并且能够通过 error 的信息观察到知悉具体的函数调用的过程

Go 复制代码
type Route struct{}

// 错误示范一:log 日志太多,淹没重要信息
func GetRoute1(srcLat, srcLng, dstLat, dstLng float32) (Route, error) {
    err := validateCoordinates1(srcLat, srcLng)
    if err != nil {
        log.Println("failed to validate source coordinates")
        return Route{}, err
    }

    err = validateCoordinates1(dstLat, dstLng)
    if err != nil {
        log.Println("failed to validate target coordinates")
        return Route{}, err
    }

    return getRoute(srcLat, srcLng, dstLat, dstLng)
}

func validateCoordinates1(lat, lng float32) error {
    if lat > 90.0 || lat < -90.0 {
        log.Printf("invalid latitude: %f", lat)
        return fmt.Errorf("invalid latitude: %f", lat)
    }
    if lng > 180.0 || lng < -180.0 {
        log.Printf("invalid longitude: %f", lng)
        return fmt.Errorf("invalid longitude: %f", lng)
    }
    return nil
}

//  错误示范二:没有 wrap 对应的处理的 error 逻辑
// 上游不知道具体的处理逻辑在哪里
func GetRoute2(srcLat, srcLng, dstLat, dstLng float32) (Route, error) {
    err := validateCoordinates2(srcLat, srcLng)
    if err != nil {
        return Route{}, err
    }

    err = validateCoordinates2(dstLat, dstLng)
    if err != nil {
        return Route{}, err
    }

    return getRoute(srcLat, srcLng, dstLat, dstLng)
}

func validateCoordinates2(lat, lng float32) error {
    if lat > 90.0 || lat < -90.0 {
        return fmt.Errorf("invalid latitude: %f", lat)
    }
    if lng > 180.0 || lng < -180.0 {
        return fmt.Errorf("invalid longitude: %f", lng)
    }
    return nil
}

// 正确示范:应该能够通过error的信息明确看出到函数的调用栈调用
func GetRoute3(srcLat, srcLng, dstLat, dstLng float32) (Route, error) {
    err := validateCoordinates2(srcLat, srcLng)
    if err != nil {
        return Route{},
            fmt.Errorf("failed to validate source coordinates: %w", err)
    }

    err = validateCoordinates2(dstLat, dstLng)
    if err != nil {
        return Route{},
            fmt.Errorf("failed to validate target coordinates: %w", err)
    }

    return getRoute(srcLat, srcLng, dstLat, dstLng)
}

func getRoute(lat, lng, lat2, lng2 float32) (Route, error) {
    return Route{}, nil
}

defer 函数里面的 error 也不应该忽视掉,要么返回 _,要么打印日志

Go 复制代码
func getBalance1(db *sql.DB, clientID string) (float32, error) {
    rows, err := db.Query(query, clientID)
    if err != nil {
        return 0, err
    }
    // 忽略不重要的 error
    defer func() { _ = rows.Close() }()

    // Use rows
    return 0, nil
}

func getBalance2(db *sql.DB, clientID string) (balance float32, err error) {
    rows, err := db.Query(query, clientID)
    if err != nil {
        return 0, err
    }
    defer func() {
        // 打印日志
        closeErr := rows.Close()
        if err != nil {
            if closeErr != nil {
                log.Printf("failed to close rows: %v", closeErr)
            }
            return
        }
        err = closeErr
    }()

    // Use rows
    return 0, nil
}

并发基础

不理解并发 (concurrency) 和并行 (parallelism)

误区一:总是以为并发程序会比串行程序要快

不知道什么时候用 channel 或者 mutex

Mutex 是用来保证对资源的排他性访问

Channel 是用来做通知,比如消息到达或者未到达

通常 mutex 用于 parallel goroutines,而 channel 用于 concurrent ones.

不清楚竞态问题

弄清楚数据竞态(Data Race)和竞态条件(Race Condition)

Data-Race 发生在两个或者多个的 Goroutine 访问内存里面的同一个数据的时候,而且至少有一个是写操作

我们可以通过下面的方法来预防 Data-Race

  • 使用 sync/atomic
  • 使用互斥锁 mutex
  • 使用 channel 来进行消息的通知

Race-Condition 是当行为取决于无法控制的事件的顺序或时间时才会发生

Go 复制代码
package races

import (
    "sync"
    "sync/atomic"
)

// 错误用法:会导致i的值不可控
func listing1() {
    i := 0

    go func() {
        i++
    }()

    go func() {
        i++
    }()
}

// 正确:使用atomic原子操作
func listing2() {
    var i int64

    go func() {
        atomic.AddInt64(&i, 1)
    }()

    go func() {
        atomic.AddInt64(&i, 1)
    }()
}

// 正确:使用互斥锁
func listing3() {
    i := 0
    mutex := sync.Mutex{}

    go func() {
        mutex.Lock()
        i++
        mutex.Unlock()
    }()

    go func() {
        mutex.Lock()
        i++
        mutex.Unlock()
    }()
}

// 正确:使用 channel
func listing4() {
    i := 0
    ch := make(chan int)

    go func() {
        ch <- 1
    }()

    go func() {
        ch <- 1
    }()

    i += <-ch
    i += <-ch
}

// 使用互斥锁
func listing5() {
    i := 0
    mutex := sync.Mutex{}

    go func() {
        mutex.Lock()
        defer mutex.Unlock()
        i = 1
    }()

    go func() {
        mutex.Lock()
        defer mutex.Unlock()
        i = 2
    }()

    _ = i
}

不理解并发对于程序执行的影响

在应用程序中,程序的执行时间会收到以下因此的影响

  • CPU的处理速度,比如归并排序;这类应用称为CPU密集型应用(CPU-Bound)

  • I/O 的速度,比如 REST 请求或者数据库查询;这类应用称为 I/O 密集型应用(I/O-Bound)

  • 内存容量,现在内存都很便宜,这个已经不是瓶颈了

不理解 Context

并发练习

传递不正确的 context

Go 复制代码
func handler(w http.ResponseWriter, r *http.Request) {
    response, err := doSomeTask(r.Context(), r)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
    return
    }
    go func() {
        err := publish(r.Context(), response)
        // Do something with err
    }()
    writeResponse(response)
}

上述代码可能带来潜在的 bug,因为 publish 里面传递的是一个新的 r.Context()

  • 如果 HTTP 的 Response 是在 publish 动作完成之后再写入,则这段代码没问题
  • 如果 HTTP 的 Response 是在 publish 动作完成之前就写入,那 context 提前 cancel 就会导致 publish 没有完成对应的任务

从 Go.12 开始,新增了 context.WithoutCancel

不知道什么时候关闭 Goroutine

看下面的代码

Go 复制代码
func main() {
    newWatcher()
    // Run the application
}

type watcher struct { /* Some resources */ }

func (w watcher) watch() {}

func newWatcher() {
    w := watcher{}
    go w.watch() // Creates a goroutine that watches some external configuration
}

上面代码的问题在于当主进程退出之后(可能是由于OS的信号或者其他原因),主函数退出了,导致整个程序都推出了;但是 watcher 创建出的子资源却没有被优雅的关闭

第一种想法可能是利用 context,改动如下

Go 复制代码
func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    newWatcher(ctx)
    // Run the application
}

func newWatcher(ctx context.Context) {
    w := watcher{}
    go w.watch(ctx)
}

上述代码会有一个潜在的问题,我们的想法是当 context 被 cancel 掉时,watcher 资源应该会自动关闭掉其自身拥有的资源;但是问题在于我们能保证 watch 有足够的时间去做这些操作嘛

上面的问题在于我们发送了信号来通知对应的 Goroutine 应该关闭,但是没有block主进程直到子 Goroutine 全部释放掉其资源;

解决如下

Go 复制代码
func main() {
    w := newWatcher()
    defer w.close()
    // Run the application
}

func newWatcher() watcher {
    w := watcher{}
    go w.watch()
    return w
}

func (w watcher) close() {
    // Close the resources
}

我们直接使用 defer 来保证资源被释放掉才退出应用程序

总结:

  • 必须记住 Goroutine 和其他资源类似,最终必须释放其占有的资源

  • 启动一个 Goroutine 但是不清楚什么时候停止是一个设计上的缺陷

  • 如果一个 Goroutine 创建了资源并且其与应用程序的整个生命周期绑定起来了,那么应该先等待所有 Goroutine 完成对应的任务再退出主应用程序会更加安全,这样程序运行结束能保证所有的资源都是被释放掉了

期待通过 select 和 channel 获取确定性的行为

不清楚 select 对于多 channel 情况下的使用

Go 复制代码
go func() {
  for i := 0; i < 10; i++ {
      messageCh <- i
    }
    disconnectCh <- struct{}{}
}()

for {
    select {
    case v := <-messageCh:
        fmt.Println(v)
    case <-disconnectCh:
        fmt.Println("disconnection, return")
        return
    }
}

上面的代码如果我们运行多次,可能得到的输出都不太一样

Go 复制代码
0
1
2
disconnection, return

0
disconnection, return

select 的运行机制不同于 switch,当有一个 case 匹配的时候,就会进行对应的操作;也就是 select 其实是属于边缘触发的机制

上面案例的正确写法如下

Go 复制代码
func main() {
    messageCh := make(chan int, 10)
    disconnectCh := make(chan struct{})

    go listing2(messageCh, disconnectCh)

    for i := 0; i < 10; i++ {
        messageCh <- i
    }
    disconnectCh <- struct{}{}
    time.Sleep(10 * time.Millisecond)
}

// 正确写法
func listing2(messageCh <-chan int, disconnectCh chan struct{}) {
    for {
        select {
        case v := <-messageCh:
            fmt.Println(v)
        case <-disconnectCh:    //
            for {
                select {
                case v := <-messageCh:
                    fmt.Println(v)
                default:
                    fmt.Println("disconnection, return")
                    return
                }
            }
        }
    }
}

select 语句用于处理一到多个通道的发送和接收操作。select 语句会阻塞,直到其中一个case可以执行,然后执行该case对应的代码块。如果没有任何case可以执行且存在default语句,那么会执行default对应的代码块

另外观察到上面代码可以发现 disconnectCh 纯粹是用作 Goroutine 之前通知消息,所以我们定义为 chan struct{} 类型;

通知类型的 channel 都应该统一定义为 chan struct{} 类型,不要定义为其他类型,比如

Go 复制代码
disconnectCh := make(chan bool)

这样带来的问题就是,我们可以从 disconnectCh 获取到 true 类型的值知道什么意思,但是如果获取到 false 类型,代表的是什么?这就会引起一些不必要的歧义

所以从根源上避免这种情况,理想的方式就是使用 channel of empty structs: chan struct{}.

不清楚 channel 的 size

区别有缓冲的 channel 和无缓冲的 channel

创建 channel 时没有提供对应的 size 或者 size 为 0 均代表是一个无缓冲的 channel

Go 复制代码
ch1 := make(chan int)
ch2 := make(chan int, 0)

有缓冲的 channel

Go 复制代码
ch3 := make(chan int, 1)
ch3 <-1     // Non-blocking
ch3 <-2     // Blocking

有缓冲和无缓冲的 channel 区别在哪

  • 无缓冲的 channel 支持同步操作,能够保证两个 Goroutine 之间一个接受数据另外一个发送数据

  • 有缓冲的 channel 不提供强同步操作

忘记字符串 Format 的副作用

Go 复制代码
type Customer struct {
    mutex sync.RWMutex // Uses a sync.RWMutex to protect concurrent accesses
    id    string
    age   int
}

func (c *Customer) UpdateAge(age int) error {
    c.mutex.Lock() // Locks and defers unlock as we update Customer
    defer c.mutex.Unlock()

    if age < 0 { // Returns an error if age is negative
        return fmt.Errorf("age should be positive for customer %v", c)
    }

    c.age = age
    return nil
}

func (c *Customer) String() string {
    c.mutex.RLock() // Locks and defers unlock as we read Customer
    defer c.mutex.RUnlock()
    return fmt.Sprintf("id %s, age %d", c.id, c.age)
}

仔细查看上述代码,bug在于。如果 UpdateAge 填入的是一个负数,则由于返回的 error 会进行 format("%v") 这会调用结构体的 String() 方法,这个方法又会对 mutex 加一次锁,导致加锁两次直接死锁

修改方法可如下

Go 复制代码
// 将 age 的判断提前出来
func (c *Customer) UpdateAge(age int) error {
    if age < 0 {
        return fmt.Errorf("age should be positive for customer %v", c)
    }

    c.mutex.Lock()
    defer c.mutex.Unlock()

    c.age = age
    return nil
}

// 不使用 %v, 而是使用 %s 即可
func (c *Customer) UpdateAge(age int) error {
    c.mutex.Lock()
    defer c.mutex.Unlock()

    if age < 0 {
        return fmt.Errorf("age should be positive for customer id %s", c.id)
    }

    c.age = age
    return nil
}

对 slices 和 maps 不正确的使用

Go 复制代码
type Cache struct {
    mu       sync.RWMutex
    balances map[string]float64
}

func (c *Cache) AddBalance(id string, balance float64) {
    c.mu.Lock()
    c.balances[id] = balance
    c.mu.Unlock()
}

func (c *Cache) AverageBalance() float64 {
    c.mu.RLock()
    balances := c.balances // Creates a copy of the balances map
    c.mu.RUnlock()

    sum := 0.
    for _, balance := range balances { // Iterates over the copy, outside of the critical section
        sum += balance
    }
    return sum / float64(len(balances))
}

仔细看上面的代码,发现 bug 了嘛

单 Goroutine 运行这段代码可能不会出现问题,但是如果存在多个 Goroutine,一个用来计算 AddBalance,一个用来计算 AverageBalance,就可能存在数据竞态

我们使用 balances := c.balances 其实是想拷贝一个副本,但是 map 和 slice 是一个引用类型,所以实际上多个 Goroutine 操作的还是同一个 map

修改方式如下

Go 复制代码
// 使用一个互斥锁
func (c *Cache) AverageBalance() float64 {
    c.mu.RLock()
    defer c.mu.RUnlock() // Unlocks when the function returns

    sum := 0.
    for _, balance := range c.balances {
        sum += balance
    }
    return sum / float64(len(c.balances))
}

// 手动复制一个map,用其副本做对应的操作即可
func (c *Cache) AverageBalance() float64 {
    c.mu.RLock()
    m := make(map[string]float64, len(c.balances)) // Copies the map
    for k, v := range c.balances {
        m[k] = v
    }
    c.mu.RUnlock()

    sum := 0.
    for _, balance := range m {
        sum += balance
    }
    return sum / float64(len(m))
}

错误使用 WaitGroup

要准确使用 sync.WaitGroup,请在启动 Goroutine 之前调用 Add 方法

Go 复制代码
// 错误一:
func listing1() {
    wg := sync.WaitGroup{}
    var v uint64

    for i := 0; i < 3; i++ {
        go func() {
            wg.Add(1)
            atomic.AddUint64(&v, 1)
            wg.Done()
        }()
    }

    wg.Wait()
    fmt.Println(v)
}

// 错误二:
func listing2() {
    wg := sync.WaitGroup{}
    var v uint64

    wg.Add(3)
    for i := 0; i < 3; i++ {
        go func() {
            atomic.AddUint64(&v, 1)
            wg.Done()
        }()
    }

    wg.Wait()
    fmt.Println(v)
}

// 正确使用
func listing3() {
    wg := sync.WaitGroup{}
    var v uint64

    for i := 0; i < 3; i++ {
        wg.Add(1)
        go func() {
            atomic.AddUint64(&v, 1)
            wg.Done()
        }()
    }

    wg.Wait()
    fmt.Println(v)
}

忘记 sync.Cond

可以使用 sync.Cond 向多个 Goroutine 发送重复的通知

使用 errgroup

可以使用 errgroup 同步一组 Goroutine 并处理错误和上下文

禁止复制 sync 类型

Sync 类型不能被复制

标准库

提供错误的时间格式

Go 复制代码
func listing1() {
    ticker := time.NewTicker(1000)    // 不要这么写(1000不知道单位是啥)
    for {
        select {
        case <-ticker.C:
            fmt.Println("tick")
        }
    }
}

func listing2() {
ticker := time.NewTicker(time.Microsecond)
    for {
        select {
        case <-ticker.C:
            fmt.Println("tick")
        }
    }
}

time.After 和内存泄漏

Go 复制代码
func consumer(ch <-chan Event) {
    for {
        select {
        case event := <-ch:
            handle(event)
        case <-time.After(time.Hour):
            log.Println("warning: no messages received")
        }
    }
}

查看 time.After 的源代码,可以发现其每次都返回的是一个只读的 channel

Go 复制代码
func After(d Duration) <-chan Time {
    return NewTimer(d).C
}

当 time.After 用来 loop 循环或者重复的 context 中时,每次迭代都会创建一个新的 channel.

如果这些新创建的 channel 没有被 close 掉或者其相关的 timers 没有停止掉,则会导致内存泄漏

The resources associated with each timer and channel are only released when the timer expires or the channel is closed.

优化方案如下:

Go 复制代码
// 使用 context's timeout
func consumer(ch <-chan Event) {
    for {
        ctx, cancel := context.WithTimeout(context.Background(), time.Hour)
        select {
        case event := <-ch:
            cancel()
            handle(event)
        case <-ctx.Done():
            log.Println("warning: no messages received")
        }
    }
}

// 使用 time.NewTimer
func consumer(ch <-chan Event) {
    timerDuration := 1 * time.Hour
    timer := time.NewTimer(timerDuration)

    for {
        timer.Reset(timerDuration)
        select {
        case event := <-ch:
            handle(event)
        case <-timer.C:
            log.Println("warning: no messages received")
        }
    }
}

未关闭临时资源

close HTTP body

Go 复制代码
func (h handler) getStatusCode2(body io.Reader) (int, error) {
    resp, err := h.client.Post(h.url, "application/json", body)
    if err != nil {
        return 0, err
    }

    defer func() {
        err := resp.Body.Close()
        if err != nil {
            log.Printf("failed to close response: %v\n", err)
        }
    }()

    _, _ = io.Copy(io.Discard, resp.Body)

    return resp.StatusCode, nil
}

读写文件

Go 复制代码
func writeToFile2(filename string, content []byte) (err error) {
    f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, os.ModeAppend)
    if err != nil {
        return err
    }

    defer func() {
        _ = f.Close()
    }()

    _, err = f.Write(content)
    if err != nil {
        return err
    }

    return f.Sync()
}

处理完HTTP请求之后忘记 return

Go 复制代码
func handler(w http.ResponseWriter, req *http.Request) {
    err := foo(req)
    if err != nil {
        http.Error(w, "foo", http.StatusInternalServerError)
        // 这里记得带上return 
    }

    _, _ = w.Write([]byte("all good"))
    w.WriteHeader(http.StatusCreated)
}

上面的问题在于 err != nil 之后,使用 http.Error 处理完错误但是却没有返回,导致程序还在向 response 里面写入

另外需要注意:生产环境不要使用 defualt HTTP client 和 server,因为其未配置超时等其他参数

Go 复制代码
// DefaultClient is the default [Client] and is used by [Get], [Head], and [Post].
var DefaultClient = &Client{}

可以看到源码里面其实现压根就什么参数都未配置

单元测试

必要情况下可对测试进行分类

使用 build 参数、环境变量或者短模式对测试进行分类可以使测试过程更加高效

Go 复制代码
func TestInsert2(t *testing.T) {
    if os.Getenv("INTEGRATION") != "true" {
        t.Skip("skipping integration test")
    }

    // ...
}

func TestLongRunning(t *testing.T) {
    if testing.Short() {
        t.Skip("skipping long-running test")
    }
    // ...
}

不使用 table-driven 测试样式

业务函数如下

Go 复制代码
func removeNewLineSuffixes(s string) string {
    if s == "" {
        return s
    }
    if strings.HasSuffix(s, "\r\n") {
        return removeNewLineSuffixes(s[:len(s)-2])
    }
    if strings.HasSuffix(s, "\n") {
        return removeNewLineSuffixes(s[:len(s)-1])
    }
    return s
}

需要对上面的函数进行单元测试

写法一:每个测试集都使用一个测试函数

Go 复制代码
func TestRemoveNewLineSuffix_Empty(t *testing.T) {
    got := removeNewLineSuffixes("")
    expected := ""
    if got != expected {
        t.Errorf("got: %s", got)
    }
}

func TestRemoveNewLineSuffix_EndingWithCarriageReturnNewLine(t *testing.T) {
    got := removeNewLineSuffixes("a\r\n")
    expected := "a"
    if got != expected {
        t.Errorf("got: %s", got)
    }
}

func TestRemoveNewLineSuffix_EndingWithNewLine(t *testing.T) {
    got := removeNewLineSuffixes("a\n")
    expected := "a"
    if got != expected {
        t.Errorf("got: %s", got)
    }
}

上面的写法虽然可行,但是每次都引入了重复的代码,这样写不太好

写法二:使用 table-driven 样式;减少了重复冗余的代码,只需要关心自己需要关注的测试集即可

Go 复制代码
func TestRemoveNewLineSuffix(t *testing.T) {
    tests := map[string]struct {
        input    string
        expected string
    }{
        `empty`: {
            input:    "",
            expected: "",
        },
        `ending with \r\n`: {
            input:    "a\r\n",
            expected: "a",
        },
        `ending with \n`: {
            input:    "a\n",
            expected: "a",
        },
        `ending with multiple \n`: {
            input:    "a\n\n\n",
            expected: "a",
        },
        `ending without newline`: {
            input:    "a",
            expected: "a",
        },
    }
    for name, tt := range tests {
        tt := tt
        t.Run(name, func(t *testing.T) {
            t.Parallel()
            got := removeNewLineSuffixes(tt.input)
            if got != tt.expected {
                t.Errorf("got: %s, expected: %s", got, tt.expected)
            }
        })
    }
}
相关推荐
凡人的AI工具箱4 小时前
AI教你学Python 第11天 : 局部变量与全局变量
开发语言·人工智能·后端·python
是店小二呀4 小时前
【C++】C++ STL探索:Priority Queue与仿函数的深入解析
开发语言·c++·后端
canonical_entropy4 小时前
金蝶云苍穹的Extension与Nop平台的Delta的区别
后端·低代码·架构
我叫啥都行5 小时前
计算机基础知识复习9.7
运维·服务器·网络·笔记·后端
无名指的等待7125 小时前
SpringBoot中使用ElasticSearch
java·spring boot·后端
.生产的驴6 小时前
SpringBoot 消息队列RabbitMQ 消费者确认机制 失败重试机制
java·spring boot·分布式·后端·rabbitmq·java-rabbitmq
AskHarries6 小时前
Spring Boot利用dag加速Spring beans初始化
java·spring boot·后端
苹果酱05677 小时前
一文读懂SpringCLoud
java·开发语言·spring boot·后端·中间件
掐指一算乀缺钱7 小时前
SpringBoot 数据库表结构文档生成
java·数据库·spring boot·后端·spring
计算机学姐9 小时前
基于python+django+vue的影视推荐系统
开发语言·vue.js·后端·python·mysql·django·intellij-idea