Go快速优化代码的几个小Tips

前言

本文将提供一些代码优化指南,希望能够帮助开发者增强其程序性能并简化开发,实现更高效和健壮的编码,解锁 Golang 应用程序的潜力。 下面是我从自己平时开发常用的实用库中随机选择的一些有用且通用的代码片段。

Tracking Execution Time

如果你想要跟踪 Go 中函数的执行时间,有一个简单而高效的技巧,只需使用 defer 关键字即可实现一行代码。你只需要一个 TrackTime 函数:

func TrackTime(pre time.Time) time.Duration {
  elapsed := time.Since(pre)
  fmt.Println("elapsed:", elapsed)

  return elapsed
}

func TestTrackTime(t *testing.T) {
  defer TrackTime(time.Now()) // <--- THIS

  time.Sleep(500 * time.Millisecond)
}

// elapsed: 501.11125ms

预分配切片

预分配切片或 Map 可以显著提高 Go 的性能。

我们可以使用预分配的零长度切片,而不指定数组长度,可以使用 append 来操作切片。

// 不推荐
a := make([]int, 10)
a[0] = 1

// 推荐用法
b := make([]int, 0, 10)
b = append(b, 1)

链式调用

链式调用的技巧可以应用于带有接收器的函数(指针)。为了说明这一点,先写一个 Person 结构体,其中有两个函数 AddAge 和 Rename,用于修改它。

type Person struct {
  Name string
  Age  int
}

func (p *Person) AddAge() {
  p.Age++
}

func (p *Person) Rename(name string) {
  p.Name = name
}

如果你想增加一个人的年龄然后修改他们的名字,传统的方法是:

func main() {
  p := Person{Name: "Aiden", Age: 30}

  p.AddAge()
  p.Rename("Aiden 2")
}

不过我们可以修改 AddAgeRename 函数的接收器,使其返回修改后的对象本身:

func (p *Person) AddAge() *Person {
  p.Age++
  return p
}

func (p *Person) Rename(name string) *Person {
  p.Name = name
  return p
}

通过返回修改后的对象本身,我们可以轻松地将多个函数接收器链接在一起,而不添加不必要的代码行:

p = p.AddAge().Rename("Aiden 2")

import _

有时可能会遇到包含下划线(_)的导入语句,就像这样:

import (
  _ "google.golang.org/genproto/googleapis/api/annotations"
)

这将执行包的初始化代码(init函数),而不创建一个命名引用。它允许你在运行代码之前初始化包,注册连接和执行其他任务。

package underscore

func init() {
  fmt.Println("init called from underscore package")
}
// main
package main

import (
  _ "lab/underscore"
)

func main() {}
// Output:init called from underscore package

Importing with import .

在理解了下划线(_)在导入中的用法之后,让我们看看点(.)运算符是如何更常用的。

作为开发者,点(.)运算符可以用来从包中导入公开标识符,而无需显式指定包名。这为懒惰的开发者提供了一个方便的快捷方式。

当处理项目中的长包名时,比如 externalmodel 或 doingsomethinglonglib 时,这特别有用。

package main

import (
  "fmt"
  . "math"
)

func main() {
  fmt.Println(Pi) // 3.141592653589793
  fmt.Println(Sin(Pi / 2)) // 1
}

多个 Error 的 Join

Go 1.20 引入了 errors 包的新功能,包括支持多个错误以及对 errors.Is 和 errors.As 的更改。

errors 包新增了一个名为 Join 的函数,我们将在下面详细说明:

var (
  err1 = errors.New("Error 1st")
  err2 = errors.New("Error 2nd")
)

func main() {
  err := err1
  err = errors.Join(err, err2)

  fmt.Println(errors.Is(err, err1)) // true
  fmt.Println(errors.Is(err, err2)) // true
}

如果多个任务导致错误,你可以使用 Join 函数而不是手动管理一个数组。这样就简化了错误处理过程。

检查 Interface 是否为 Nil

即使接口持有的值为 nil,这并不意味着接口本身为 nil。这可能会导致 Go 程序中的意外错误。因此,了解如何检查接口是否真正为 nil 就至关重要了。

func main() {
  var x interface{}
  var y *int = nil
  x = y

  if x != nil {
    fmt.Println("x != nil")
  } else {
    fmt.Println("x == nil")
  }

  fmt.Println(x)
}

// Output:
// x != nil
// 

我们如何确定一个interface{}值是否为nil呢?推荐一个简单的方法:

func IsNil(x interface{}) bool {
  if x == nil {
    return true
  }

  return reflect.ValueOf(x).IsNil()
}

解析 JSON 中的 time.Duration

在解析JSON时,使用 time.Duration 可能是一个繁琐的操作,因为它需要在秒后添加 9 个零(即1000000000)。为了简化这个过程,下面创建了一个名为 Duration 的新类型:

type Duration time.Duration

为了将字符串(如“1s”或“20h5m”)解析为int64类型的持续时间,我还为这种新类型实现了自定义解析逻辑:

func (d *Duration) UnmarshalJSON(b []byte) error {
  var s string
  if err := json.Unmarshal(b, &s); err != nil {
    return err
  }
  dur, err := time.ParseDuration(s)
  if err != nil {
    return err
  }
  *d = Duration(dur)
  return nil
}

NOTE: 需要注意的是变量 d 不应该为 nil,因为这可能会导致错误。可以在函数开始时对 d 进行检查。

函数参数注释

在处理具有多个参数的函数时,仅通过阅读其用法来理解每个参数的含义可能会令人困惑。请看以下示例:

printInfo("foo", true, true)

如果你不检查 printInfo 函数,第一个 'true' 和第二个 'true' 的含义是什么?当你有一个带有多个参数的函数时,仅通过阅读它们的用法来理解参数的含义可能会令人困惑。

我们可以使用注释来使代码更易读。例如:

// func printInfo(name string, isLocal, done bool)

printInfo("foo", true /* isLocal */, true /* done */)
关于我
loading