首页 > 其他分享 >10个令人惊叹的Go语言技巧,让你的代码更加优雅

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

时间:2023-11-19 18:12:34浏览次数:26  
标签:10 err 令人惊叹 nil fmt Println func Go

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

原创 TimLiu 爱发白日梦的后端 2023-11-18 08:50 发表于广东 收录于合集#go95个 爱发白日梦的后端 专注 Go 语言领域的发展,学习成为更牛逼的架构师,日常分享 Go 语言、架构、软件工具的使用。 129篇原创内容 公众号

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

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

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

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 也支持这个功能,可以在函数调用建议中显示注释,但可能需要在设置中启用。

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

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

 

关注和添加微信

进技术交流群,带你徜徉在知识的海洋中

图片

 

 

TimLiu

赞赏二维码喜欢作者

收录于合集 #go  95个 上一篇写出高质量代码的秘诀:Golang中的测试驱动开发(TDD)   阅读 520 爱发白日梦的后端 ​ 分享此内容的人还喜欢   Go etcd 的依赖问题终于解决了。。。     爱发白日梦的后端 不看的原因   用 Go语言 在处理队列时,redis如何做生产与消费订单任务的。守护进程又是如何写?     Go语言圈 不看的原因   Go 使用环境变量     爱发白日梦的后端 不看的原因   关注公众号后可以给作者发消息              

人划线

标签:10,err,令人惊叹,nil,fmt,Println,func,Go
From: https://www.cnblogs.com/cheyunhua/p/17842353.html

相关文章

  • 20211104李宜时学习笔记10
    块设备I/O和缓冲区管理学习笔记1.块设备I/O缓冲区定义与作用:解释块设备I/O缓冲区的基本概念,及其在数据传输中的作用。工作原理:描述数据如何从应用程序通过缓冲区传输到块设备,反之亦然。2.UNIXI/O缓冲区管理算法基本算法:介绍UNIX系统中用于管理I/O缓冲区的常见算法。效......
  • 2023-2024-1 20231410刘珈岐 《计算机基础与程序设计》第8周学习总结
    2023-2024-120231410刘珈岐《计算机基础与程序设计》第8周学习总结作业信息这个作业属于哪个课程(https://edu.cnblogs.com/campus/besti/2023-2024-1-CFAP)这个作业要求在哪里(https://www.cnblogs.com/rocedu/p/9577842.html#WEEK08))这个作业的目标自学教材《......
  • 2023-2024-1 20231310《计算机基础与程序设计》第八周学习总结
    作业信息这个作业属于哪个课程<班级的链接>https://www.cnblogs.com/rocedu/p/9577842.html#FHML这个作业要求在哪里https://www.cnblogs.com/rocedu/p/9577842.html#WEEK08这个作业的目标自学计算机科学概论第9章,《C语言程序设计》第7章作业正文https://www.c......
  • 如何在Mac电脑上输入苹果logo图标?
    苹果标志AppleLogo最经典设计图案就是苹果咬一口图案,通常能够在iPhone、iPad、AppleWatch、MacBook或iMac等苹果产品上看见。苹果logo符号标志怎么打,这篇来教大家通过Mac电脑打出苹果Logo符号。一、输入快捷键在苹果mac键盘上,点按 Option+Shift+K 键,就能够快速打出苹......
  • Princeton Algorithms, Part I week3 Quick Sort
    QuickSort今天学习quicksort,quicksort的基本思想是有一个数组,先shuffle以后,保证数组的item位置是均匀分布的,选择一个item然后,把所有比这个item大的放在item右边,所有比这个item小的放在左右,然后递归的进行这个操作,如下图所示 这里面的partition部分如何实现呢?首先定义两个指......
  • 第十一周学习笔记(学习笔记10)
    〇、思维导图一、知识总结解释块设备I/O的原理和I/O缓冲的优点介绍Unix的缓冲区管理算法利用信号量设计新的缓冲区管理算法,以提高I/O缓冲区的缓存效率和性能介绍简单的PV算法及其特点基本概念读写普通文件的算法依赖于两个关键操作,即get_block和put_block,这两个操作将磁......
  • (10)页尾右下角显示第 [Page#]页,共[TotalPages#]页
    新建一个页尾Band拖一个文本对象Memo到界面双击Memo 并设置Memo的上边框为黑色2 最终效果 ......
  • 学习笔记10
    块设备I/O和缓冲区管理块设备I/O缓冲区I/O缓冲的基本原理非常简单。文件系统使用一系列I/O缓冲区作为块设备的缓存内存。当进程试图读取(dev,blk)标识的磁盘块时,它首先在缓冲区缓存中搜索分配给磁盘块的缓冲区。如果该缓冲区存在并且包含有效数据,那么它只需从缓冲区中读取数据,而无......
  • 2023-2024-1 20232310 《网络空间安全导论》第二周学习
    教材内容总结教材学习中的问题和解决过程问题1:学习抗量子密码是不了解其中提到的Hash函数解决方法:看了B站教程问题2:还有哪些常见的密码破译或攻击技术方法解决方法:询问了GPT社会工程学:攻击者可能会利用社会工程学技术,通过欺骗、诱导、或其他手段来获取密码,例如通过欺骗用......
  • go并发 - channel
    概述并发编程是利用多核心能力,提升程序性能,而多线程之间需要相互协作、共享资源、线程安全等。任何并发模型都要解决线程间通讯问题,毫不夸张的说线程通讯是并发编程的主要问题。go使用著名的CSP(CommunicatingSequentialProcess,通讯顺序进程)并发模型,从设计之初Go语言就注重如......