1. 追踪执行时间的技巧
如果你想追踪 Go 中函数的执行时间,有一个简单高效的技巧可以用一行代码实现,使用 defer 关键字即可。你只需要一个 TrackTime 函数:
go
代码解读
复制代码
// 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 不仅仅是用于清理任务,还可以用于准备任务,考虑以下示例:
go
代码解读
复制代码
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
这种模式的美妙之处在于,只需一行代码,你就可以完成诸如以下任务:
- 打开数据库连接,然后关闭它。
- 设置模拟环境,然后拆除它。
- 获取分布式锁,然后释放它。
- ...
"嗯,这似乎很聪明,但它在现实中有什么用处呢?"
还记得追踪执行时间的技巧吗?我们也可以这样做:
go
代码解读
复制代码
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() 这样的模式不会妥善处理错误。这种技巧最适合用于测试或者当你愿意冒着致命错误的风险时使用,参考下面这种面向测试的方法:
go
代码解读
复制代码
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 一样使用预分配的切片:
go
代码解读
复制代码
// 与其
a := make([]int, 10)
a[0] = 1
// 不如这样使用
b := make([]int, 0, 10)
b = append(b, 1)
3. 链式调用
链式调用技术可以应用于函数(指针)接收器。为了说明这一点,让我们考虑一个 Person 结构,它有两个函数 AddAge 和 Rename,用于对其进行修改。
go
代码解读
复制代码
type Person struct {
Name string
Age int
}
func (p *Person) AddAge() {
p.Age++
}
func (p *Person) Rename(name string) {
p.Name = name
}
如果你想给一个人增加年龄然后给他们改名字,常规的方法是:
go
代码解读
复制代码
func main() {
p := Person{Name: "Aiden", Age: 30}
p.AddAge()
p.Rename("Aiden 2")
}
或者,我们可以修改 AddAge 和 Rename 函数接收器,使其返回修改后的对象本身,即使它们通常不返回任何内容。
go
代码解读
复制代码
func (p *Person) AddAge() *Person {
p.Age++
return p
}
func (p *Person) Rename(name string) *Person {
p.Name = name
return p
}
通过返回修改后的对象本身,我们可以轻松地将多个函数接收器链在一起,而无需添加不必要的代码行:
go
代码解读
复制代码
p = p.AddAge().Rename("Aiden 2")
4. Go 1.20 允许将切片解析为数组或数组指针
当我们需要将切片转换为固定大小的数组时,不能直接赋值,例如:
go
代码解读
复制代码
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
代码解读
复制代码
// 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 _" 进行包初始化
有时,在库中,你可能会遇到结合下划线 (_) 的导入语句,如下所示:
go
代码解读
复制代码
import (
_ "google.golang.org/genproto/googleapis/api/annotations"
)
这将执行包的初始化代码(init 函数),而无需为其创建名称引用。这允许你在运行代码之前初始化包、注册连接和执行其他任务。