最近 Phuong Le 大佬针对日常开发 Go 项目时,总结了一些好用的 Go 小技巧。
看了后,感觉对于刚入门 Go 的同学有一定的学习价值。可以挑好的学。应用到自己项目里。以下内容分享给大家。
在开发 Go 生产项目时,我发现自己经常重复编写代码和使用某些技术,直到后来回顾自己的工作时才意识到这一点。
下面是从总结经验中挑选的一些有用的代码片段,希望对大家有所帮助。
1. 计时技巧
如果你对跟踪函数的执行时间感兴趣,或者在排查问题时需要使用。
可以在 Go 中可以使用 defer
关键字,只需一行代码即可实现一个非常简单、高效的技巧。
你只需要一个 TrackTime 函数:
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()) // <-- 就是这里
time.Sleep(500 * time.Millisecond)
}
// elapsed: 501.11125ms
1.5 两阶段 Defer
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)
}
2. 预先分配切片
我们在编写程序时,可以有意识的预先分配或映射切片,可以显著提高我们的 Go 程序的性能。
如下例子:
// 而不是这样
a := make([]int, 10)
a[0] = 1
// 这样使用
b := make([]int, 0, 10)
b = append(b, 1)
3. 链式调用
链式调用技术可以应用于函数(指针)接收者。
我们考虑一个具有两个函数 AddAge 和 Rename 的 Person 结构体,这两个函数可以用来修改 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: 35}
p.AddAge()
p.Rename("煎鱼")
}
或者,我们可以修改 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("脑子进煎鱼了")
4. Go 1.20 支持将切片解析为数组或数组指针
当我们需要将切片转换为固定大小的数组时,我们不能像这样直接赋值:
a := []int{0, 1, 2, 3, 4, 5}
var b [3]int = a[0:3]
// cannot use a[0:3] (value of type []int) as [3]int value in variable
// declaration compiler(IncompatibleAssign)
为了将切片转换为数组,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]
}
5. 使用 _ import 进行包初始化
在库中,你可能会看到像这样带有下划线 _
的 import 语句:
import (
_ "google.golang.org/genproto/googleapis/api/annotations"
)
这将会执行包的初始化代码(init 函数),不会为它创建包的名称引用。
功能上来讲,这允许你在运行代码之前初始化包,注册连接并执行其他任务。
这是一个例子,以便于我们更好地理解它的工作原理:
// 下划线包
package underscore
func init() {
// 初始化代码
}
这种方式允许我们在不直接使用包的情况下,执行包的初始化代码。
这在需要进行一些设置或注册操作时非常有用。
6. 使用点 . 操作符导入包
点(.)操作符可以用来使导入包的导出标识符在不必指定包名的情况下可用,这对懒惰的开发者来说是一个有用的捷径。
这在处理项目中的长包名称(例如 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. 编译时检查接口的技巧
假设有一个名为 Buffer
的接口,其中包含一个 Write()
函数。此外,还有一个名为 StringBuffer
的结构体实现了这个接口。
但是,如果你打错了字,写的是 Writeee()
,而不是 Write()
呢?
type Buffer interface {
Write(p []byte) (n int, err error)
}
type StringBuffer struct{}
func (s *StringBuffer) Writeee(p []byte) (n int, err error) {
return 0, nil
}
在运行之前,您无法检查 StringBuffer
是否正确实现了 Buffer
接口。
通过使用下面这个技巧,编译器会通过 IDE
错误信息提醒您:
var _ Buffer = (*StringBuffer)(nil)
// cannot use (*StringBuffer)(nil) (value of type *StringBuffer)
// as Buffer value in variable declaration: *StringBuffer
// does not implement Buffer (missing method Write)
9. 三元运算符
Go 不像许多其他编程语言那样有内置对三元运算符的支持。
Python:
min = a if a < b else b
C#:
min = x < y ? x : y
Go 在 1.18 中引入了泛型功能,现在我们可以创建一个实用工具,只需一行代码即可实现类似于三元表达式的功能:
// our utility
func Ter[T any](cond bool, a, b T "T any") T {
if cond {
return a
}
return b
}
func main() {
fmt.Println(Ter(true, 1, 2)) // 1
fmt.Println(Ter(false, 1, 2)) // 2
}
10. 验证接口是否真的为 nil 的方法
即使接口的值为 nil,也不一定意味着接口本身就是 nil。这可能会导致 Go 程序中出现意想不到的错误。
知道如何检查接口是否为 nil 是很重要的。
func main() {
var x interface{}
var y *int = nil
x = y
if x != nil {
fmt.Println("x != nil") // <-- actual
} else {
fmt.Println("x == nil")
}
fmt.Println(x)
}
// x != nil
// <nil>
我们如何确定 interface{}
值是否为空?
通过下述方法可以实现这一个诉求:
func IsNil(x interface{}) bool {
if x == nil {
return true
}
return reflect.ValueOf(x).IsNil()
}
总结
这些开发技巧不限具体的分类,对于大家在日常开发中能有一些 tips 的作用。
平时我经常看到有同学为了统计函数执行时间,就一条条打日志,打开始和结束时间。显得比较繁琐。
大家可以结合起来,在平时开发时,也可以及时总结这类方法论。会比较有帮助!
推荐阅读
关注和加煎鱼微信,
一手消息和知识,拉你进技术交流群👇
你好,我是煎鱼,出版过 Go 畅销书《Go 语言编程之旅》,再到获得 GOP(Go 领域最有观点专家)荣誉,点击蓝字查看我的出书之路。
日常分享高质量文章,输出 Go 面试、工作经验、架构设计,加微信拉读者交流群,和大家交流!