🎬 故事的开始:我发现自己一直在"重复造轮子"
说实话,有段时间我陷入了一个怪圈:
我:(写代码)
我:(review代码)
我:"等等...这段代码我好像写过?"
我:"这个技巧我上个月也用过了!"
那一刻我意识到:我一直在用一些"独门技巧",但从来没系统整理过。
于是,我决定从自己的工具库里掏出12个最实用的Go技巧,分享给同样在Go世界里探索的你!
💡 温馨提示:这些技巧没有特定分类,全是实战中摸爬滚打出来的"野路子",但...真香!
1️⃣ 计时小技巧:一行代码搞定性能分析
我的痛点
有次老板问我:"这个接口为啥这么慢?"
我:"让我加个日志看看..."
(然后手动在函数开头结尾加时间戳,算差值...累!)
解决方案
// 我的秘密武器
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,初始化+清理一把梭
这个技巧是我从Teiva Harsanyi那里偷师的,简直绝了!
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)
}
⚠️ 但有个坑:如果数据库连接失败怎么办?
解法:在测试里用t.Fatal()处理错误
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️⃣ 切片预分配:既要性能,又要安全
我的发现
看了一篇性能优化文章后,我兴奋地把所有切片都预分配了:
// 我以为这样很快
a := make([]int, 10)
a[0] = 1
a[1] = 2
// ...
结果:某天我不小心用了append,数据全乱了!😱
正确姿势
// 这样既快又安全!
b := make([]int, 0, 10) // 长度0,容量10
b = append(b, 1) // 放心append
b = append(b, 2)
好处:
- ✅ 预分配容量,避免频繁扩容
- ✅ 用append,不会覆盖数据
- ✅ 性能+安全,我全都要!
3️⃣ 链式调用:让代码像搭积木
以前的我
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") // 改名
// 要写两行,烦!
}
开窍后的我
// 改一下返回值
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}
// Go 1.17之前,要这样写(看着就恶心)
b := *(*[3]int)(a[0:3])
// 那一堆*是啥?谁记得住啊!
Go 1.20的救赎
// Go 1.20,终于像人话了!
a := []int{0, 1, 2, 3, 4, 5}
b := [3]int(a[0:3])
fmt.Println(b) // [0 1 2]
感受:Go团队终于听到了我们的呐喊!🙏
小贴士:
a[:3]和a[0:3]是一样的,我写0只是为了更清楚。
5️⃣ 下划线导入:让包"自动初始化"
我第一次见到的懵逼
import (
_ "google.golang.org/genproto/googleapis/api/annotations"
)
我:"这啥意思?导入不用?"
真相
// underscore包
package underscore
func init() {
fmt.Println("init called from underscore package")
}
// main包
package main
import (
_ "lab/underscore" // 👈 下划线导入
)
func main() {}
// 输出:init called from underscore package
作用:执行包的init()函数,但不创建引用。
实战场景:
- 📦 注册数据库驱动
- 🔌 初始化插件
- 🎯 触发副作用(side effects)
6️⃣ 点导入:懒人的福音
场景
// 不用点导入
fmt.Println(math.Pi)
fmt.Println(math.Sin(math.Pi / 2))
// 用点导入
import (
"fmt"
. "math" // 👈 点导入
)
fmt.Println(Pi) // 不用写math.
fmt.Println(Sin(Pi / 2)) // 爽!
适用场景:
- 📏 包名太长(比如
externalmodel) - 🎨 想让代码更简洁
- 😴 纯粹就是懒
⚠️ 警告:别滥用!导入太多包会命名冲突
7️⃣ Go 1.20:错误合并,终于不用手动拼了
以前的我
var errors []error
if err1 != nil {
errors = append(errors, err1)
}
if err2 != nil {
errors = append(errors, err2)
}
// 手动拼,累!
Go 1.20的我
err1 := errors.New("Error 1st")
err2 := errors.New("Error 2nd")
err := errors.Join(err1, err2)
fmt.Println(errors.Is(err, err1)) // true
fmt.Println(errors.Is(err, err2)) // true
效果:多个错误,一键打包!📦
8️⃣ 编译时检查接口实现:别让bug溜到生产
我的血泪史
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
}
结果:编译通过,运行时才报错!😭
救星来了
// 加这一行
var _ Buffer = (*StringBuffer)(nil)
// 编译器立刻报错:
// cannot use (*StringBuffer)(nil) as Buffer value
// *StringBuffer does not implement Buffer (missing method Write)
好处:IDE直接标红,bug无处遁形!🔴
9️⃣ 泛型实现三元运算符(但别用!)
诱惑
// Go没有三元运算符,可惜
// min = a < b ? a : b // 这种语法Go不支持
// 但用泛型可以模拟!
func Ter[T any](cond bool, a, b T) T {
if cond {
return a
}
return b
}
func main() {
fmt.Println(Ter(true, 1, 2)) // 1
fmt.Println(Ter(false, 1, 2)) // 2
}
我的建议
别用! ❌
原因:
- 📖 可读性差(
if-else更清楚) - 调试困难
- 团队其他成员可能看不懂
💡 结论:知道有这个技巧就行,生产环境慎用!
🔟 避免裸参数:让代码自己说话
问题代码
printInfo("foo", true, true)
// 我:(看代码)"这俩true是啥意思?"
// 我:(翻函数定义)"哦,第一个是isLocal,第二个是done..."
// 我:(心累)
解决方案
// 加注释!
printInfo("foo", true /* isLocal */, true /* done */)
// 或者用命名参数(如果IDE支持)
效果:代码自己解释自己,爽!
1️⃣1️⃣ 接口判空:nil不等于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) // <nil>
}
// 输出:
// x != nil
// <nil>
我:"啥?x打印出来是nil,但x != nil??"
正确姿势
func IsNil(x interface{}) bool {
if x == nil {
return true
}
return reflect.ValueOf(x).IsNil()
}
// 现在可以正确判断了!
💡 原理:接口包含类型+值,即使值是nil,接口本身也不一定是nil
1️⃣2️⃣ JSON解析time.Duration:告别9个0
曾经的噩梦
// 解析JSON里的"1s"
// 结果要写:1000000000(9个0!)
// 我:(数0)"1、2、3...9,对了吗?"
我的解决方案
type Duration time.Duration
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
}
// 现在可以直接解析"1s"、"20h5m"了!
效果:JSON里的"1s"自动变成正确的duration,爽!
🎯 总结:我的私藏武器库
| 技巧 | 实用指数 | 推荐指数 | 难度 |
|---|---|---|---|
| 计时defer | ⭐⭐⭐⭐⭐ | 必用 | 简单 |
| 两阶段defer | ⭐⭐⭐⭐⭐ | 强烈推荐 | 中等 |
| 切片预分配 | ⭐⭐⭐⭐ | 性能优化 | 简单 |
| 链式调用 | ⭐⭐⭐⭐ | 看场景 | 简单 |
| Go 1.20数组转换 | ⭐⭐⭐ | 特定场景 | 简单 |
| 下划线导入 | ⭐⭐⭐⭐ | 框架开发 | 简单 |
| 点导入 | ⭐⭐ | 慎用 | 简单 |
| 错误合并 | ⭐⭐⭐⭐⭐ | Go 1.20+必用 | 简单 |
| 接口检查 | ⭐⭐⭐⭐⭐ | 强烈推荐 | 简单 |
| 泛型三元 | ⭐ | 别用! | 中等 |
| 注释参数 | ⭐⭐⭐⭐ | 团队规范 | 简单 |
| 接口判空 | ⭐⭐⭐⭐⭐ | 必须知道 | 中等 |
| Duration解析 | ⭐⭐⭐⭐ | 配置场景 | 中等 |
💬 最后的话
这些技巧都是我在实战中一点点摸索出来的,有些可能你早就知道,有些可能让你眼前一亮。
我的建议:
-
✅ 挑适合你的用
-
✅ 别为了炫技而炫技
-
✅ 代码可读性永远是第一位的