本篇博客的目的,更多是为快速翻阅与回忆使用。 若需文档本身:Go项目开发文档,结合翻阅,效果更佳
接口本身就是 - 引用类型 - (底层存类型+数据指针),完全不用定义 “指向接口的指针”,这毫无意义。
都是为了验证合理性罢了。都是为了验证合
// 用于触发编译期的接口的合理性检查机制
// 如果 Handler 没有实现 http.Handler,会在编译期报错
var _ http.Handler = (*Handler)(nil)
type LogHandler struct {
h http.Handler
log *zap.Logger
}
var _ http.Handler = LogHandler{}
func (h LogHandler) ServeHTTP(
w http.ResponseWriter,
r *http.Request,
) {
// ...
}
所以,我个人认为,能用指针,尽量用指针。
大致意思是,不需要new
mu := new(sync.Mutex)
mu.Lock()
直接就可以使用
var mu sync.Mutex
mu.Lock()
结构体里放 Mutex 时,别直接嵌入(会暴露 Lock/Unlock 方法),用命名字段(如musync.Mutex),把锁的操作藏在结构体内部方法里,不让外部知道实现细节。
切片和映射含底层数据指针,传递或返回时需在边界(接收 / 返回处)拷贝(用 make+copy 或循环复制),避免外部修改影响内部数据。
用 defer 释放资源(如锁、文件),确保无论多少 return 分支都能释放,提升可读性,且开销小,推荐使用。
Defer 的开销非常小,只有在您可以证明函数执行时间处于纳秒级的程度时,才应避免这样做。
" Channel 不建议随便设大值,优先无缓冲(size0)或 size1 ",核心是回到 Go Channel 的设计初衷 —— 用于 goroutine 间的 - 同步与通信 - ,而非数据缓存容器。 并且大尺寸无法解决 “阻塞问题”,只会延迟并隐蔽风险
枚举:用 “自定义类型 + iota” 实现,通常从 1 开始(因为变量值默认是0) 仅当 0 是合理默认时才从 0 开始;
其实这部分规范的核心就一个目的:避免你在处理时间时 “想当然” 出错—— 因为时间比你以为的复杂(比如有夏令时、闰年、时区差),所以必须用 Go 自带的time
包,按固定套路来。
特殊例子: 你可能觉得 “一天 24 小时、一年 365 天” 是常识,但实际不是:
所以规范第一句话就强调:别自己假设时间规则,必须用time
包—— 它已经帮你处理了这些复杂情况。
我对3.命名有规矩的解释:
var (
// 导出错误变量:Err开头,首字母大写,外部可通过 utils.ErrInvalidParam 访问
ErrInvalidParam = errors.New("invalid parameter")
ErrTimeout = errors.New("operation timeout")
// 未导出错误变量:err开头,首字母小写,仅包内可用
errInternalCalc = errors.New("internal calculation failed") // 包内小函数用
errTempFile = errors.New("temp file missing") // 包内临时文件处理用
)
对 4.处理只一次 的解释 很多人会犯 “又打日志又返回错误” 的错,导致上层再打日志,日志里全是重复的错误信息(比如 “get user failed” 出现 3 次)。规范强调:每个错误只处理一次,处理方式分 4 种 处理方式什么时候用?例子包装后返回你处理不了,让上层处理
return fmt.Errorf("get user %q: %w", id, err)
日志 + 降级(不返回)错误不影响主流程,比如发 metrics 失败if err := emitMetrics(); err != nil { log.Printf("emit metrics: %v", err) }
匹配错误 + 针对性处理知道怎么处理这个错误,比如用户没找到就用 UTC 时区if errors.Is(err, ErrUserNotFound) { tz = time.UTC } else { return ... }
直接返回原始错误没上下文可加,底层错误已经很清楚if err != nil { return err }
拓展:
fmt.Errorf("fail %w", errors.New("test"))
的直接输出(字符串形式)是 "fail test",且会保留原始错误的关联,支持后续通过errors.Is()等工具追溯底层错误。 而%v 只是一个普通的占位符。
对接口变量做类型断言时,永远用 “逗号 ok” 习语(t,ok := i.(类型)),因为:
t, ok := i.(string)
if !ok {
// 优雅地处理错误
}
本质是:把 “错误处理的主动权” 交给调用方,而非用 panic 强行终止程序,这是 Go 错误处理的核心思想之一。
测试代码:为什么用 t.Fatal 而非 panic? --但截至到目前,我还没用过。所以对这个体悟还不是很深。
测试代码中,我们需要的是 “标记测试失败”,而不是 “让测试程序崩溃”。t.Fatal 比 panic 更合适,原因有两点:
t.Fatal
会停止当前测试用例,但不会影响其他测试用例(如果用 panic,可能会导致整个测试套件中断)。对比例子:
go.uber.org/atomic 是 Uber 公司开源的一个 Go 语言第三方库,专门用于简化并发场景下的原子操作,解决标准库 sync/atomic 容易用错的问题。
在多线程(Go 里是 goroutine)并发时,如果多个 goroutine 同时读写同一个变量,可能出现 “数据竞争”(比如一个 goroutine 写了一半,另一个就读了,导致数据错乱)。 “原子操作” 就是一种 “不可分割” 的操作 —— 要么做完,要么没做,中间不会被其他 goroutine 打断,确保并发安全。
避免全局变量,可改用依赖倒置, 这样既能测试起来方便,又能避免全局污染,倒置不安全。
其实这个我在面向对象设计的七大原则中,提到过的 “组合/聚合复用原则” 。
点击 "查看具体"
嵌套就是其中的组合。 我们要使用的是聚合。
核心观点 | 具体说明 |
---|---|
为什么要避免公共结构嵌入类型? | 1. 泄漏内部实现细节(用户能看到依赖的类型); 2. 限制类型演化(改依赖 / 改方法都会导致破坏性改变); 3. 模糊文档(用户需跳转查看嵌入类型的方法)。 |
正确做法是什么? | 1. 用 私有字段(首字母小写)持有内部依赖(结构体或接口); 2. 手动写 委托方法:公共结构体自己暴露方法,内部调用私有字段的对应方法。 |
权衡点 | 手动写委托方法确实比嵌入 “麻烦”(多写几行代码),但换来的是 更好的封装性、更强的可维护性、无兼容性风险—— 对于公共结构(比如库、框架对外暴露的类型),这种 “麻烦” 是值得的。 |
一句话概括:公共结构的核心是 “对外暴露功能,隐藏实现”,而类型嵌入会打破这种平衡;用 “私有字段 + 手动委托”,才能在代码复用和封装性之间找到最优解。
简单说: 不要“抢用”Go的“专用名字”,用自己的名称(如 `err`、`msg`、`num`),才能保证代码无冲突、易维护。
其实,主要就是不可预测性!
因为
init
函数会带来三个主要问题:
init
的执行顺序虽然规则明确(按包导入顺序和文件字典序),但难以一目了然。这使得代码的流程变得不透明,调试和追踪问题困难。
init
函数会自动执行,无法在测试中绕过或模拟其行为。如果它执行了诸如连接数据库、设置全局变量等操作,会给单元测试带来麻烦和耦合。
init
函数没有返回值,如果初始化失败,只能通过 panic 来中止程序,这非常不优雅,剥夺了调用者处理错误的机会。
核心思想: 避免“魔法”,提倡显式优于隐式。通过显式的初始化函数(如 Initialize()
)来让调用者控制流程和处理错误,代码会更清晰、更健壮、更易维护。
是这样:
for n := 0; n < b.N; n++ {
data := make([]int, 0, size)
for k := 0; k < size; k++{
data = append(data, k)
}
}
而非这样:
for n := 0; n < b.N; n++ {
data := make([]int, 0)
for k := 0; k < size; k++{
data = append(data, k)
}
}
这样可以减少切片重新分配容量的次数。
除了 main
函数,其他任何函数都不要调用 os.Exit 或 log.Fatal 。它们应该只返回错误,把“退出”这个重大决定留给程序的最高领导者。 这样你的程序会更安全、更健壮、也更容易测试。
其实就是在文件关闭的时候,还能追踪到完整的 错误的返回的流程 。
type Stock struct {
Price int `json:"price"`
Name string `json:"name"`
// Safe to rename Name to Symbol.
}
bytes, err := json.Marshal(Stock{
Price: 137,
Name: "UBER",
})
其实,就是这种,加字段标记。
Goroutines 是轻量级的,但它们不是免费的: 至少,它们会为堆栈和 CPU 的调度消耗内存。 虽然这些成本对于 Goroutines 的使用来说很小,但当它们在没有受控生命周期的情况下大量生成时会导致严重的性能问题。
所以我们的目的就是,能够控制并明确 协程 的退出。
可以通过 sync.WaitGroup 进行控制
var wg sync.WaitGroup // 创建一个计数器
for i := 0; i < 10; i++ {
wg.Add(1) // 计数器+1(表示要等待一条新 goroutine)
go func() {
defer wg.Done() // 函数结束时,计数器-1
// ... 执行任务 ...
}()
}
// 阻塞等待,直到计数器归零(所有 goroutine 都调用了 Done())
wg.Wait()
在 “基本类型(如整数、浮点数)和字符串之间互相转换” 时,用
strconv
包比fmt
包性能更好。
其实说白了,就是运用了我平时写算法时的思想,预处理。
避免在循环或频繁调用的代码中,反复将同一个字符串转换为字节切片(
[]byte
),因为每次转换都会创建新的内存副本,造成不必要的性能开销。 应该在循环外部提前转换一次,然后在循环内部复用转换后的结果。
跟上方的优先指定切片容量,是一个道理。
这些规范的目的都是为了写出整洁、一致、易于他人阅读和维护的代码。它们关注的不是“代码能不能运行”,而是“代码好不好”,这是个人项目与大型、可持续协作的专业项目之间的重要区别。
代码不是写给自己看的,要考虑队友的可读性。 一行代码太长(建议超过99个字符),需要读者横向滚动屏幕才能看完,这非常影响阅读体验和效率。
在一个项目甚至一个公司内,统一的代码风格远比争论“哪种风格最好”更重要。
把同类事物放在一起,让代码更整洁、更有组织性,就像把同一类的文件放进同一个文件夹。 如:将多个 import、const、var、type 声明分别用括号 () 分组在一起。
让导入的库来源一目了然。标准库和第三方库分开,结构更清晰。 用空行将导入分为两组:第一组是标准库(如"fmt"、"os"),第二组是第三方库(如"go.uber.org/atomic")。 大多数编辑器用 goimports 工具会自动帮你完成这个分组。
import (
"fmt"
"os"
"go.uber.org/atomic"
"golang.org/x/sync/errgroup"
)
类型 | 命名规则 | 例子 | 例外/特殊情况 |
---|---|---|---|
包名 | 全小写,无下划线,不用复数,不用通用名 | package user, package http | 无 |
文件名 | 全小写,可使用下划线 _ 分隔单词(也可用驼峰命名法) | user_model.go, http_server.go | _test.go, _unix.go, _windows.go 等 |
蛇形、驼峰、尽量和项目一致
对了(大驼峰就是公开导出GetUser、小驼峰就是私有的getUser)
如果程序包名称与导入路径的最后一个元素不匹配,则必须使用导入别名 一般可以用来解决包名冲突使用。
目的是让任何人打开一个文件,都能很快抓住重点(这个包提供了什么类型和功能),然后按需阅读细节,而不是在混乱的代码中迷失。
type something struct{ ... }
func newSomething() *something {
return &something{}
}
func (s *something) Cost() {
return calcCost(s.weights)
}
func (s *something) Stop() {...}
func calcCost(n []int) int {...}
return
或 continue
。这能让你减少一层 else
嵌套。
else
:很多时候,如果 if
条件里已经 return
了,接下来的代码自然就是在条件不成立的情况下运行的,根本不需要 else
。
for _, v := range data {
if v.F1 != 1 {
log.Printf("Invalid v: %v", v)
continue
}
v = process(v)
if err := v.Call(); err != nil {
return err
}
v.Send()
}
像高速公路,遇到障碍(错误)立刻下高速,否则就一路畅通直达目的地。
信任编译器的类型推断。不要写不必要的类型声明,让代码更简洁。
对这个,我理解不够深刻。
顶级变量和常量具有包范围作用域。使用通用名称可能很容易在其他文件中意外使用错误的值。
// foo.go
const (
_defaultPort = 8080
_defaultUser = "user"
)
嵌入式类型(例如 mutex)应位于结构体内的字段列表的顶部,并且必须有一个空行将嵌入式字段与常规字段分隔开。
type Client struct {
http.Client
version int
}
内嵌应该提供切实的好处,比如以语义上合适的方式添加或增强功能。 它应该在对用户没有任何不利影响的情况下使用。 其中 Mutex 极度不建议直接嵌入。
如果将变量明确设置为某个值,则应使用短变量声明形式 (
:=
)
是
s := "foo"
而非
var s = "foo"
最好能使用规范啦,但如下这种就可以不用使用:
更优:
var filtered []int
非更优: filtered := []int{}
nil
是一个有效的长度为 0 的 slice,这意味着!!如下几点: 1、您不应明确返回长度为零的切片。应该返回 nil 来代替 2、要检查切片是否为空,请始终使用len(s)==0,而非nil。(错误:s==nil) 3、零值切片可即刻使用,无需make创建。
如果有可能,尽量缩小变量作用范围。
函数调用中的
意义不明确的参数
可能会损害可读性。当参数名称的含义不明显时,请为参数添加 C 样式注释 (/* ... */)
用``,防转义。
wantError := `unknown error:"test"`
var
声明零值 -> 为了表明意图。
&T{}
初始化指针 -> 为了一致性和简洁。
对于空 map 请使用 make(...) 初始化,
在尽可能的情况下,请在初始化时提供 map 容量大小,
另外,如果 map 包含固定的元素列表,则使用 map literals(map 初始化列表) 初始化映射。
如:
m := map[T1]T2{
k1: v1,
k2: v2,
k3: v3,
}