Golang高级微调技术

本文分享了一些小技巧,可以帮助我们写出更简化、高效的Golang代码,从而获得更好的开发体验。原文: Fine-Tuning Golang: Advanced Techniques for Code Optimization

本文是Golang代码优化技术的综合指南,帮助我们释放 Golang 应用程序的全部潜能,提高性能、简化开发,获得更高效、更强大的编码体验。

在项目开发过程中,我发现经常会有重复代码,有时还会忽略某些技术,直到进行工作回顾的时候才会发现。

为了解决这个问题,我开发了一个解决方案。这对我自己很有帮助,相信对其他人也会有用。

以下是从我的实用工具库中随机挑选的一些有用的多功能代码片段,没有具体分类,也没有针对特定系统的技巧:

跟踪执行时间的技术

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

golang 复制代码
// Utility
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
两阶段延迟执行

在 Go 中,defer 不仅用于清理任务,还可以用于准备任务。请看下面的例子:

golang 复制代码
func setupTeardown() func() {
    fmt.Println("Run initialization")
    return func() {
        fmt.Println("Run cleanup")
    }
}

func main() {
    defer setupTeardown()() // <--------
    fmt.Println("Main function called")
}

// Run initialization
// Main function called
// Run cleanup

这种模式的妙处在于,只需一行代码,就能完成以下任务:

  • 打开数据库连接,然后关闭。
  • 建立模拟环境,然后拆除。
  • 获取分布式锁,然后释放。
  • ...

这看起来很聪明,但在现实中有什么实际应用呢?

还记得跟踪执行时间的技巧吗?也可以这样做:

golang 复制代码
func TrackTime() func() {
  pre := time.Now()
  return func() {
    elapsed := time.Since(pre)
    fmt.Println("elapsed:", elapsed)
  }
}

func main() {
  defer TrackTime()()

  time.Sleep(500 * time.Millisecond)
}

注意!连接数据库时出现错误怎么办?

事实上,像 defer TrackTime()defer ConnectDB() 这样的模式可能无法正确处理错误。

这种技术最适合在测试或愿意承担潜在致命错误风险的情况下使用。请考虑以下面向测试的方法:

golang 复制代码
func TestSomething(t *testing.T) {
  defer handleDBConnection(t)()
  // ...
}

func handleDBConnection(t *testing.T) func() {
  conn, err := connectDB()
  if err != nil {
    t.Fatal(err)
  }

  return func() {
    fmt.Println("Closing connection", conn)
  }
}

这样就可以在测试过程中处理数据库连接中的错误。

预先分配切片

预先分配切片或映射可以显著提高Go程序性能。

不过,值得注意的是,如果我们不小心使用追加而不是索引(如 a[i]),这种方法有时会出错。

可以在不指定数组长度(零长度)的情况下使用预分配的切片,这样就可以像使用 append 一样使用切片。

golang 复制代码
// Not Recommended
a := make([]int, 10)
a[0] = 1

// Recommended
b := make([]int, 0, 10)
b = append(b, 1)
链式调用

链式调用技术可应用于带有接收器的函数(指针)。为了说明这一点,让我们考虑一个带有 AddAgeRename 两个函数的 Person 结构,以对其进行修改。

golang 复制代码
type Person struct {
  Name string
  Age  int
}

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

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

如果想增加一个人的年龄,然后重新命名,传统的方法是

golang 复制代码
func main() {
  p := Person{Name: "Aiden", Age: 30}

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

另一方面,也可以修改 AddAgeRename 函数的接收器,使其返回修改后的对象本身,尽管它们通常不会返回任何内容:

golang 复制代码
func (p *Person) AddAge() *Person {
  p.Age++
  return p
}

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

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

golang 复制代码
p = p.AddAge().Rename("Aiden 2")
从 Go 1.20 开始,可以将切片转换为数组或数组指针

例如,当我们需要将切片转换为固定大小的数组时,是不能直接赋值的:

golang 复制代码
a := []int{0, 1, 2, 3, 4, 5}
var b [3]int = a[0:3]

在变量声明中,将 a[0:3][]int 类型的值)赋值给 [3]int 类型的变量是不兼容、不允许的。

Go 团队在 Go 1.17 中更新了将切片转换为数组的功能。随着 Go 1.20 的发布以及更多方便的字面量的加入,转换过程变得更加简单:

golang 复制代码
// Go 1.20
func Test(t *testing.T) {
   a := []int{0, 1, 2, 3, 4, 5}
   b := [3]int(a[0:3])

  fmt.Println(b) // [0 1 2]
}

// Go 1.17
func TestM2e(t *testing.T) {
  a := []int{0, 1, 2, 3, 4, 5}
  b := *(*[3]int)(a[0:3])

  fmt.Println(b) // [0 1 2]
}

提醒一下:可以用 a[:3] 代替 a[0:3]

软件包初始化,import _

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

golang 复制代码
import (
  _ "google.golang.org/genproto/googleapis/api/annotations"
)

这将执行软件包的初始化代码(init 函数),而不会为其创建命名引用。通过它,可以在运行代码前初始化软件包、注册连接并执行其他任务。

golang 复制代码
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
通过import .导入

在了解了下划线 (_) 在导入中的使用方法后,让我们来看看更常用的点 (.) 操作符。

作为开发人员,可以使用点(.)操作符从软件包导入导出标识符,而无需明确指定软件包名称。对于懒惰的开发人员来说,这是一种方便的快捷方式。

很酷吧?在处理项目中较长的软件包名称(如 externalmodeldoingsomethinglonglib)时,这一点尤其有用。

golang 复制代码
package main

import (
  "fmt"
  . "math"
)

func main() {
  fmt.Println(Pi) // 3.141592653589793
  fmt.Println(Sin(Pi / 2)) // 1
}
将多个错误合并为一个错误

Go 1.20 为 errors 包引入了新功能,包括支持多重错误以及对 errors.Iserrors.As 的修改。

Joinserrors的新增功能之一,我们将在下文中详细讨论:

golang 复制代码
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 函数代替手动管理数组,从而简化错误处理流程。

检查接口是否真正为Nil

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

golang 复制代码
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
// <nil>

如何判断 interface{} 值是否为 nil?幸运的是,有一个简单的工具可以帮助我们做到这一点:

golang 复制代码
func IsNil(x interface{}) bool {
  if x == nil {
    return true
  }

  return reflect.ValueOf(x).IsNil()
}
解析 JSON 中的 time.Duration

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

golang 复制代码
type Duration time.Duration

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

golang 复制代码
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
}

不过,需要注意的是,变量 d 不能为零,否则会导致 marshaling 错误。另外,也可以在函数开始时对 d 进行检查。

避免裸参数

在处理具有多个参数的函数时,仅通过阅读每个参数的用法来理解其含义可能会造成混乱。请看下面的例子:

golang 复制代码
printInfo("foo", true, true)

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

不过,可以通过注释来提高代码可读性。例如:

golang 复制代码
// func printInfo(name string, isLocal, done bool)

printInfo("foo", true /* isLocal */, true /* done */)

你好,我是俞凡,在Motorola做过研发,现在在Mavenir做技术工作,对通信、网络、后端架构、云原生、DevOps、CICD、区块链、AI等技术始终保持着浓厚的兴趣,平时喜欢阅读、思考,相信持续学习、终身成长,欢迎一起交流学习。为了方便大家以后能第一时间看到文章,请朋友们关注公众号"DeepNoMind",并设个星标吧,如果能一键三连(转发、点赞、在看),则能给我带来更多的支持和动力,激励我持续写下去,和大家共同成长进步!

本文由mdnice多平台发布

相关推荐
研究司马懿1 小时前
【云原生】Gateway API高级功能
云原生·go·gateway·k8s·gateway api
梦想很大很大15 小时前
使用 Go + Gin + Fx 构建工程化后端服务模板(gin-app 实践)
前端·后端·go
lekami_兰20 小时前
MySQL 长事务:藏在业务里的性能 “隐形杀手”
数据库·mysql·go·长事务
却尘1 天前
一篇小白也能看懂的 Go 字符串拼接 & Builder & cap 全家桶
后端·go
ん贤1 天前
一次批量删除引发的死锁,最终我选择不加锁
数据库·安全·go·死锁
mtngt112 天前
AI DDD重构实践
go
Grassto3 天前
12 go.sum 是如何保证依赖安全的?校验机制源码解析
安全·golang·go·哈希算法·go module
Grassto5 天前
11 Go Module 缓存机制详解
开发语言·缓存·golang·go·go module
程序设计实验室6 天前
2025年的最后一天,分享我使用go语言开发的电子书转换工具网站
go
我的golang之路果然有问题6 天前
使用 Hugo + GitHub Pages + PaperMod 主题 + Obsidian 搭建开发博客
golang·go·github·博客·个人开发·个人博客·hugo