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

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 10个令人惊叹的Go语言技巧,让你的代码更加优雅

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


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


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


. 追踪执行时间的技巧


如果你想追踪 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 结构,它有两个函数 AddAgeRename,用于对其进行修改。


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


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 ." 进行导入


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


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


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


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

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


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


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

相关文章
|
1天前
|
Java 编译器 Go
探索Go语言的性能优化技巧
在本文中,我们将深入探讨Go语言的底层机制,以及如何通过代码层面的优化来提升程序性能。我们将讨论内存管理、并发控制以及编译器优化等关键领域,为你提供一系列实用的技巧和最佳实践。
|
1天前
|
Cloud Native Go API
Go语言在微服务架构中的创新应用与实践
本文深入探讨了Go语言在构建高效、可扩展的微服务架构中的应用。Go语言以其轻量级协程(goroutine)和强大的并发处理能力,成为微服务开发的首选语言之一。通过实际案例分析,本文展示了如何利用Go语言的特性优化微服务的设计与实现,提高系统的响应速度和稳定性。文章还讨论了Go语言在微服务生态中的角色,以及面临的挑战和未来发展趋势。
|
1天前
|
安全 Go 调度
探索Go语言的并发模式:协程与通道的协同作用
Go语言以其并发能力闻名于世,而协程(goroutine)和通道(channel)是实现并发的两大利器。本文将深入了解Go语言中协程的轻量级特性,探讨如何利用通道进行协程间的安全通信,并通过实际案例演示如何将这两者结合起来,构建高效且可靠的并发系统。
|
1天前
|
安全 Go 开发者
破译Go语言中的并发模式:从入门到精通
在这篇技术性文章中,我们将跳过常规的摘要模式,直接带你进入Go语言的并发世界。你将不会看到枯燥的介绍,而是一段代码的旅程,从Go的并发基础构建块(goroutine和channel)开始,到高级模式的实践应用,我们共同探索如何高效地使用Go来处理并发任务。准备好,让Go带你飞。
|
2天前
|
运维 Go 开发者
Go语言在微服务架构中的应用与优势
本文深入探讨了Go语言在构建微服务架构中的独特优势和实际应用。通过分析Go语言的核心特性,如简洁的语法、高效的并发处理能力以及强大的标准库支持,我们揭示了为何Go成为开发高性能微服务的首选语言。文章还详细介绍了Go语言在微服务架构中的几个关键应用场景,包括服务间通信、容器化部署和自动化运维等,旨在为读者提供实用的技术指导和启发。
|
2天前
|
安全 Go 调度
探索Go语言的并发之美:goroutine与channel
在这个快节奏的技术时代,Go语言以其简洁的语法和强大的并发能力脱颖而出。本文将带你深入Go语言的并发机制,探索goroutine的轻量级特性和channel的同步通信能力,让你在高并发场景下也能游刃有余。
|
3天前
|
Go 开发者
Go语言中的并发编程:从基础到实践
在当今的软件开发中,并发编程已经成为了一项不可或缺的技能。Go语言以其简洁的语法和强大的并发支持,成为了开发者们的首选。本文将带你深入了解Go语言中的并发编程,从基础概念到实际应用,帮助你掌握这一重要的编程技能。
|
4天前
|
Go
使用go语言将A助手加入项目中
使用go语言将A助手加入项目中
13 2
|
6天前
|
负载均衡 Go API
探索Go语言在微服务架构中的应用与优势
在这篇技术性文章中,我们将深入探讨Go语言(又称为Golang)在构建微服务架构时的独特优势。文章将通过对比分析Go语言与其他主流编程语言,展示Go在并发处理、性能优化、以及开发效率上的优势。同时,我们将通过一个实际的微服务案例,详细说明如何利用Go语言构建高效、可扩展的微服务系统。
|
4天前
|
Go 数据处理 调度
Go语言中的并发模型:解锁高效并行编程的秘诀
本文将探讨Go语言中独特的并发模型及其在现代软件开发中的应用。通过深入分析 Goroutines 和 Channels,我们将揭示这一模型如何简化并行编程,提升应用性能,并改变开发者处理并发任务的方式。不同于传统多线程编程,Go的并发方法以其简洁性和高效性脱颖而出,为开发者提供了一种全新的编程范式。