您当前的位置:首页 > 电脑百科 > 程序开发 > 语言 > Go语言

十个令人惊叹的Go语言技巧,让你的代码更加优雅

时间:2023-11-20 15:15:19  来源:微信公众号  作者:爱发白日梦的后端

在开发生产项目的过程中,我注意到经常会发现自己在重复编写代码,使用某些技巧时没有意识到,直到后来回顾工作时才意识到。

为了解决这个问题,我开发了一种解决方案,对我来说非常有帮助,我觉得对其他人也可能有用。

以下是一些从我的实用程序库中随机挑选的有用且多功能的代码片段,没有特定的分类或特定于系统的技巧。

1. 追踪执行时间的技巧

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

// 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

1.5. 两阶段延迟执行

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)
  }
}

这样,在测试期间可以处理数据库连接的错误。

2. 预分配切片

根据文章《Go 性能提升技巧》中的见解,预分配切片或映射可以显著提高 Go 程序的性能。

但是值得注意的是,如果我们不小心使用 Append 而不是索引(如 a[i]),这种方法有时可能导致错误。你知道吗,我们可以在不指定数组长度(为零)的情况下使用预分配的切片,就像在上述文章中解释的那样?这使我们可以像使用 append 一样使用预分配的切片:

// 与其
a := make([]int, 10)
a[0] = 1

// 不如这样使用
b := make([]int, 0, 10)
b = append(b, 1)

3. 链式调用

链式调用技术可以应用于函数(指针)接收器。为了说明这一点,让我们考虑一个 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")
}

或者,我们可以修改 AddAge 和 Rename 函数接收器,使其返回修改后的对象本身,即使它们通常不返回任何内容。

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")

4. 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.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]。我提到这一点是为了更清晰地说明。

5. 使用 "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() {}
// 输出:init called from underscore package

6. 使用 "import ." 进行导入

在了解了如何使用下划线进行导入后,让我们看看如何更常见地使用点 (.) 运算符。

作为开发者,点 (.) 运算符可用于在不必指定包名的情况下使用导入包的导出标识符,这对于懒惰的开发者来说是一个有用的快捷方式。

很酷,对吧?这在处理项目中的长包名时特别有用,比如 externalmodel 或 doingsomethinglonglib。

为了演示,这里有一个简单的例子:

package main

import (
  "fmt"
  . "math"
)

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

7. Go 1.20 允许将多个错误合并为单个错误

Go 1.20 引入了对错误包的新功能,包括对多个错误的支持以及对 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 函数而不是手动管理数组。这简化了错误处理过程。

8. 检查接口是否为真正的 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)
}

// 输出:
// x != nil
// <nil>

我们如何确定 interface{} 值是否为 nil 呢?幸运的是,有一个简单的工具可以帮助我们实现这一点:

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

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

9. 在 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' 不应为 nil,否则可能会导致编组错误。或者,你还可以在函数开头对 'd' 进行检查。

10. 避免裸参数

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

printInfo("foo", true, true)

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

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

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

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

有些 IDE 也支持这个功能,可以在函数调用建议中显示注释,但可能需要在设置中启用。

以上是我分享的一些实用技巧,但我不想让文章过长,难以跟进,因为这些技巧与特定主题无关,涵盖了各种类别。

如果你觉得这些技巧有用,或有自己的见解要分享,请随时留言。我重视你的反馈,并乐于在回应此文章时点赞或推荐你的想法。



Tags:Go语言   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除。
▌相关推荐
十个令人惊叹的Go语言技巧,让你的代码更加优雅
在开发生产项目的过程中,我注意到经常会发现自己在重复编写代码,使用某些技巧时没有意识到,直到后来回顾工作时才意识到。为了解决这个问题,我开发了一种解决方案,对我来说非常有...【详细内容】
2023-11-20  Tags: Go语言  点击:(0)  评论:(0)  加入收藏
Go语言Context应用全攻略:异步编程利器
概述在 Go 语言中,Context(上下文)是一个非常重要的概念,特别是在处理请求时。允许在请求的整个生命周期内传递数据、控制请求的取消、处理超时等。本文将介绍 Go 语言中 Contex...【详细内容】
2023-11-06  Tags: Go语言  点击:(21)  评论:(0)  加入收藏
Go语言高级特性:Context深入解读
概述在 Go 语言中,context(上下文)是一个非常重要的概念。它主要用于在多个 goroutine 之间传递请求特定任务的截止日期、取消信号以及其他请求范围的值。3. Context 的取消与...【详细内容】
2023-11-01  Tags: Go语言  点击:(14)  评论:(0)  加入收藏
Go语言中如何实现JWT
什么JWTJWT(JSON Web Token)是一种开放标准(RFC 7519),定义了一种在各方之间安全传输信息的简洁方式。这些信息可以被验证和信任,因为它们是数字签名的。JWT由三部分组成,用.分隔。...【详细内容】
2023-09-11  Tags: Go语言  点击:(100)  评论:(0)  加入收藏
Go语言开发者的Apache Arrow使用指南:内存管理
如果你看了上一篇《Go语言开发者的Apache Arrow使用指南:数据类型》[1]中的诸多Go操作arrow的代码示例,你很可能会被代码中大量使用的Retain和Release方法搞晕。不光大家有这...【详细内容】
2023-09-11  Tags: Go语言  点击:(92)  评论:(0)  加入收藏
使用Go语言进行开发的互联网公司
1、谷歌(Google)谷歌使用Go语言进行开发的服务和产品包括: Google App Engine:Google App Engine是Google的一款云服务,支持Go语言开发。 Google Cloud:Google Cloud是Google...【详细内容】
2023-09-06  Tags: Go语言  点击:(109)  评论:(0)  加入收藏
Go语言实现延迟队列
简单Go语言延迟队列思路: package mainimport ( "fmt" "time")...【详细内容】
2023-08-29  Tags: Go语言  点击:(109)  评论:(0)  加入收藏
Go语言之垃圾回收机制
什么是三色标记法 三色标记法是一种用于垃圾回收的算法,被广泛应用于各类编程语言的垃圾回收器中,包括Go语言。这种算法将对象划分成三种颜色: 白色:表示对象是垃圾,可以被释放...【详细内容】
2023-08-28  Tags: Go语言  点击:(126)  评论:(0)  加入收藏
Sonic: Go语言的超级JSON库,解析与编码速度狂飙
Sonic是一款由字节跳动开发的一个全新的高性能、适用广泛的 JSON 库。在设计上借鉴了多款JSON库,同时为了实现对标准库的真正插拔式替换,Sonic使用了 JIT[1] (即时编译) 。介绍...【详细内容】
2023-08-28  Tags: Go语言  点击:(126)  评论:(0)  加入收藏
Go语言之通道篇
通道的定义 通道是Go用于协程之间进行通讯的工具。Go的通道(channel)是一种特殊的类型,可以让你发送类型化的数据,在两个Go协程(goroutine)之间进行同步、数据传递。它是并发安...【详细内容】
2023-08-27  Tags: Go语言  点击:(62)  评论:(0)  加入收藏
▌简易百科推荐
十个令人惊叹的Go语言技巧,让你的代码更加优雅
在开发生产项目的过程中,我注意到经常会发现自己在重复编写代码,使用某些技巧时没有意识到,直到后来回顾工作时才意识到。为了解决这个问题,我开发了一种解决方案,对我来说非常有...【详细内容】
2023-11-20  爱发白日梦的后端  微信公众号  Tags:Go语言   点击:(0)  评论:(0)  加入收藏
Go 透明文件夹的特性,有没有必要加?
在 Go 语言中,我们一般会用模块(Module)和包(Package)来组织我们的项目、库的目录和代码结构。这也是官方所推荐的。今天给大家分享一个面向包这块的新提案,看看是否合适加进 Go...【详细内容】
2023-11-07  脑子进煎鱼了  微信公众号  Tags:Go   点击:(9)  评论:(0)  加入收藏
Golang 中的 Bytes 包详解之 Bytes.Buffer
上篇文章详细讲解了一次性密码 OTP 相关的知识,基于时间的一次性密码 TOTP 是 OTP 的一种实现方式。这种方法的优点是不依赖网络,因此即使在没有网络的情况下,用户也可以生成密...【详细内容】
2023-11-07  路多辛  今日头条  Tags:Golang   点击:(13)  评论:(0)  加入收藏
Go的插件机制:动态加载与卸载
今天,我们要深入探讨Go语言的插件机制,特别是动态加载与卸载的相关技术。 Go语言的插件系统提供了一种将编译好的代码作为插件动态加载到Go程序中的能力,这为程序的扩展性和模...【详细内容】
2023-11-07  lincyang新自媒体    Tags:Go   点击:(20)  评论:(0)  加入收藏
提升您的 Go 应用性能的六种方法
优化您的 Go 应用程序1. 如果您的应用程序在 Kubernetes 中运行,请自动设置 GOMAXPROCS 以匹配 Linux 容器的 CPU 配额Go 调度器 可以具有与运行设备的核心数量一样多的线程...【详细内容】
2023-11-07  技术的游戏  微信公众号  Tags:Go   点击:(20)  评论:(0)  加入收藏
Go中的同步与异步处理
在开发过程中,当需要同时处理多个操作时,开发者经常面临同步和异步两种处理方式的选择。同步处理在同步处理方式中,任务按顺序一个接一个地执行。每个任务必须在下一个任务开始...【详细内容】
2023-11-06  爱发白日梦的后端    Tags:Go   点击:(18)  评论:(0)  加入收藏
Go语言Context应用全攻略:异步编程利器
概述在 Go 语言中,Context(上下文)是一个非常重要的概念,特别是在处理请求时。允许在请求的整个生命周期内传递数据、控制请求的取消、处理超时等。本文将介绍 Go 语言中 Contex...【详细内容】
2023-11-06  Go先锋  微信公众号  Tags:Go语言   点击:(21)  评论:(0)  加入收藏
聊聊Golang饱受争议的Error
一、error是什么?在C中,返回错误通过errno.h中的错误代码来表示,比如0代表No error,也就是没有错误;2代表No such file or directory,也就是找不到指定路径的文件或文件夹;5代表Inp...【详细内容】
2023-11-06  程序员升级打怪之旅  微信公众号  Tags:Golang   点击:(21)  评论:(0)  加入收藏
14条Go接口最佳实践,有些不一样
最近几个月,没事喜欢看看老外写的技术文章,发现他们的一些思考维度真的有些不太一样。当然,他们写的文章大多数没有国内的那么卷。今天这篇文章是关于Go语言中接口设计的一些最...【详细内容】
2023-11-03  程序新视界  微信公众号  Tags:Go   点击:(19)  评论:(0)  加入收藏
Go etcd 的依赖问题终于解决了......
大家好,我是煎鱼。前几年非常高频的接触到这一堆微服务相关组件:grpc + grpc-gateway + etcd + protobuf + protoc-gen-go,一开始都是相安无事,逐步跟进新版本。这不,幺蛾子就来...【详细内容】
2023-11-02  脑子进煎鱼了  微信公众号  Tags:Go   点击:(17)  评论:(0)  加入收藏
站内最新
站内热门
站内头条