写点什么

Golang 高级微调技术

作者:俞凡
  • 2024-02-24
    上海
  • 本文字数:3781 字

    阅读完需:约 12 分钟

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


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


Aaron Thomas @Unsplash


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


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


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

跟踪执行时间的技术

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


// Utilityfunc 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 不仅用于清理任务,还可以用于准备任务。请看下面的例子:


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


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


  • 打开数据库连接,然后关闭。

  • 建立模拟环境,然后拆除。

  • 获取分布式锁,然后释放。

  • ...


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


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


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() 这样的模式可能无法正确处理错误。


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


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 一样使用切片。


// Not Recommendeda := make([]int, 10)a[0] = 1
// Recommendedb := make([]int, 0, 10)b = append(b, 1)
复制代码
链式调用

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


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")
复制代码
从 Go 1.20 开始,可以将切片转换为数组或数组指针

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


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 的发布以及更多方便的字面量的加入,转换过程变得更加简单:


// Go 1.20func 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.17func 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 _

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


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


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


package underscore
func init() { fmt.Println("init called from underscore package")}// mainpackage main
import ( _ "lab/underscore")
func main() {}// Output:init called from underscore package
复制代码
通过import .导入

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


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


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


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的新增功能之一,我们将在下文中详细讨论:


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 至关重要。


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?幸运的是,有一个简单的工具可以帮助我们做到这一点:


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


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

避免裸参数

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


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


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


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


// func printInfo(name string, isLocal, done bool)
printInfo("foo", true /* isLocal */, true /* done */)
复制代码




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

发布于: 刚刚阅读数: 4
用户头像

俞凡

关注

公众号:DeepNoMind 2017-10-18 加入

俞凡,Mavenir Systems研发总监,关注高可用架构、高性能服务、5G、人工智能、区块链、DevOps、Agile等。公众号:DeepNoMind

评论

发布
暂无评论
Golang高级微调技术_golang_俞凡_InfoQ写作社区