mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-12 10:16:37 +08:00
wavelet init
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
# 在 API 边界处复制切片和 Map
|
||||
|
||||
> **来源**:Uber 风格指南
|
||||
|
||||
切片和 map 包含对其底层数据的引用。在 API 边界处复制它们,以防止调用者修改内部状态(反之亦然)。
|
||||
|
||||
## 接收切片和 Map
|
||||
|
||||
当函数存储调用者传入的切片或 map 时,始终进行防御性复制。调用者保留原始引用,可以在函数返回后修改它。
|
||||
|
||||
### 切片
|
||||
|
||||
**不好**
|
||||
```go
|
||||
func (d *Driver) SetTrips(trips []Trip) {
|
||||
d.trips = trips // 调用者仍然可以修改 d.trips
|
||||
}
|
||||
```
|
||||
|
||||
**好**
|
||||
```go
|
||||
func (d *Driver) SetTrips(trips []Trip) {
|
||||
d.trips = make([]Trip, len(trips))
|
||||
copy(d.trips, trips)
|
||||
}
|
||||
```
|
||||
|
||||
### Map
|
||||
|
||||
**不好**
|
||||
```go
|
||||
func (s *Server) SetConfig(cfg map[string]string) {
|
||||
s.config = cfg // 调用者仍然可以修改 s.config
|
||||
}
|
||||
```
|
||||
|
||||
**好**
|
||||
```go
|
||||
func (s *Server) SetConfig(cfg map[string]string) {
|
||||
s.config = make(map[string]string, len(cfg))
|
||||
for k, v := range cfg {
|
||||
s.config[k] = v
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 返回切片和 Map
|
||||
|
||||
返回内部切片或 map 时,返回副本以防止调用者修改你的内部状态。
|
||||
|
||||
### 返回 Map
|
||||
|
||||
**不好**
|
||||
```go
|
||||
func (s *Stats) Snapshot() map[string]int {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.counters // 暴露了内部状态!
|
||||
}
|
||||
```
|
||||
|
||||
**好**
|
||||
```go
|
||||
func (s *Stats) Snapshot() map[string]int {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
result := make(map[string]int, len(s.counters))
|
||||
for k, v := range s.counters {
|
||||
result[k] = v
|
||||
}
|
||||
return result
|
||||
}
|
||||
```
|
||||
|
||||
### 返回切片
|
||||
|
||||
**不好**
|
||||
```go
|
||||
func (q *Queue) Items() []Item {
|
||||
return q.items // 调用者可以追加、修改或重新切片
|
||||
}
|
||||
```
|
||||
|
||||
**好**
|
||||
```go
|
||||
func (q *Queue) Items() []Item {
|
||||
result := make([]Item, len(q.items))
|
||||
copy(result, q.items)
|
||||
return result
|
||||
}
|
||||
```
|
||||
|
||||
## 何时不需要复制
|
||||
|
||||
防御性复制有开销。在以下情况下可以跳过:
|
||||
|
||||
- 数据**按约定是不可变的**,并且有清晰的文档说明
|
||||
- 切片/map 是**为调用者新创建的**(不在内部存储)
|
||||
- 性能分析表明复制在热路径中是瓶颈
|
||||
|
||||
如有疑问,就复制。与共享引用导致的 bug 相比,开销通常可以忽略不计。
|
||||
@@ -0,0 +1,144 @@
|
||||
# 全局状态模式
|
||||
|
||||
> **来源**:Google 风格指南, Effective Go
|
||||
|
||||
全局状态使程序更难以测试、推理和维护。依赖注入是首选替代方案,但某些全局状态在谨慎使用时是可以接受的。
|
||||
|
||||
## 何时可以接受全局状态
|
||||
|
||||
并非所有包级变量都有害。当全局状态是**真正进程级别的**且**不值得注入**时,它是合适的:
|
||||
|
||||
- **默认实例**——`http.DefaultClient`、`log.Default()`、`flag.CommandLine`
|
||||
- **一次编译的值**——包级别的 `regexp.MustCompile(...)`
|
||||
- **注册表**——`database/sql.Register`、`image.RegisterFormat`
|
||||
- **单例基础设施**——进程级别的指标收集器或追踪导出器
|
||||
|
||||
## 全局变量的试金石测试
|
||||
|
||||
在添加包级变量之前,请问自己:
|
||||
|
||||
1. **它是否真正是进程级别的?** 如果两个 goroutine 或测试可能需要不同的值,它不应该是全局的
|
||||
2. **它是否妨碍了测试?** 如果测试必须保存/恢复变量,或因此无法并行运行,应改为注入
|
||||
3. **它可以是常量吗?** 如果值在初始化后永远不会改变,优先使用 `const` 或未导出的只初始化一次的 `var`
|
||||
4. **它是否携带可变状态?** 可变全局变量是最危险的——仅在有完善文档、并发安全的单例情况下才可接受
|
||||
|
||||
## 包状态 API 模式:New() + Default()
|
||||
|
||||
标准库模式同时提供可定制的构造器和便捷的默认值。这使调用者可以在简单场景下使用默认值,在测试或特殊行为需求下注入自定义实例。
|
||||
|
||||
**好**
|
||||
```go
|
||||
package mylog
|
||||
|
||||
type Logger struct {
|
||||
prefix string
|
||||
out io.Writer
|
||||
}
|
||||
|
||||
func New(prefix string, out io.Writer) *Logger {
|
||||
return &Logger{prefix: prefix, out: out}
|
||||
}
|
||||
|
||||
var defaultLogger = New("", os.Stderr)
|
||||
|
||||
func Default() *Logger { return defaultLogger }
|
||||
|
||||
func (l *Logger) Info(msg string) {
|
||||
fmt.Fprintf(l.out, "%s%s\n", l.prefix, msg)
|
||||
}
|
||||
|
||||
// 包级便捷函数委托给默认实例。
|
||||
func Info(msg string) { defaultLogger.Info(msg) }
|
||||
```
|
||||
|
||||
```go
|
||||
// 调用者在简单场景下使用默认值
|
||||
mylog.Info("starting server")
|
||||
|
||||
// 测试或特殊代码创建自定义实例
|
||||
logger := mylog.New("[test] ", &buf)
|
||||
logger.Info("test message")
|
||||
```
|
||||
|
||||
此模式的标准库示例:
|
||||
- `log.New()` + `log.Default()` + `log.Println()`
|
||||
- `http.NewServeMux()` + `http.DefaultServeMux`
|
||||
- `flag.NewFlagSet()` + `flag.CommandLine`
|
||||
|
||||
## 依赖注入作为首选替代方案
|
||||
|
||||
当代码需要可配置行为时,通过构造器参数或结构体字段接受依赖,而非读取包级变量。
|
||||
|
||||
**不好**
|
||||
```go
|
||||
var db *sql.DB
|
||||
|
||||
func GetUser(id int) (*User, error) {
|
||||
return db.QueryRow("SELECT ...", id) // 依赖全局变量
|
||||
}
|
||||
```
|
||||
|
||||
**好**
|
||||
```go
|
||||
type UserStore struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewUserStore(db *sql.DB) *UserStore {
|
||||
return &UserStore{db: db}
|
||||
}
|
||||
|
||||
func (s *UserStore) GetUser(id int) (*User, error) {
|
||||
return s.db.QueryRow("SELECT ...", id)
|
||||
}
|
||||
```
|
||||
|
||||
注入的好处:
|
||||
- 测试可以提供 mock 或内存实现
|
||||
- 多个实例可以共存(例如,只读副本与主库)
|
||||
- 依赖在构造器签名中是显式的
|
||||
|
||||
## 注入时间
|
||||
|
||||
一个常见场景:替换 `time.Now` 以实现确定性测试。
|
||||
|
||||
**不好**
|
||||
```go
|
||||
func IsExpired(expiry time.Time) bool {
|
||||
return time.Now().After(expiry) // 不可测试
|
||||
}
|
||||
```
|
||||
|
||||
**好**
|
||||
```go
|
||||
type Checker struct {
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
func NewChecker() *Checker {
|
||||
return &Checker{now: time.Now}
|
||||
}
|
||||
|
||||
func (c *Checker) IsExpired(expiry time.Time) bool {
|
||||
return c.now().After(expiry)
|
||||
}
|
||||
```
|
||||
|
||||
测试用固定函数替换 `now`:
|
||||
|
||||
```go
|
||||
c := &Checker{now: func() time.Time {
|
||||
return time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
}}
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
| 场景 | 方法 |
|
||||
|------|------|
|
||||
| 进程级单例(日志、指标) | 默认实例 + `New()` 构造器 |
|
||||
| 一次编译的正则或模板 | 包级 `var` 配合 `MustCompile` |
|
||||
| 注册表(数据库驱动、编解码器) | 包级 `Register()` 函数 |
|
||||
| 可配置行为 | 通过构造器进行依赖注入 |
|
||||
| 时间相关逻辑 | 注入 `func() time.Time` |
|
||||
| 测试需要变化的任何东西 | 不要使用全局状态 |
|
||||
@@ -0,0 +1,92 @@
|
||||
# Must 函数
|
||||
|
||||
> **来源**:Uber 风格指南, Go 标准库约定
|
||||
|
||||
`Must` 函数包装一个可能失败的函数,在出错时 panic。**仅**在程序初始化阶段使用,因为失败意味着程序无法运行。
|
||||
|
||||
## 标准库示例
|
||||
|
||||
```go
|
||||
// regexp.MustCompile 在模式无效时 panic
|
||||
var validID = regexp.MustCompile(`^[a-z][a-z0-9-]{0,62}$`)
|
||||
|
||||
// template.Must 在模板解析失败时 panic
|
||||
var tmpl = template.Must(template.ParseFiles("index.html"))
|
||||
```
|
||||
|
||||
这些是安全的,因为它们在包初始化时运行——如果失败,程序无法正确运行。
|
||||
|
||||
## 何时使用 Must
|
||||
|
||||
```
|
||||
这是在程序初始化期间调用的吗(包级 var、init、main 设置)?
|
||||
├─ 是 → 失败是否不可恢复(配置、正则、模板)?
|
||||
│ ├─ 是 → 使用 Must 是合适的
|
||||
│ └─ 否 → 改为返回 error
|
||||
└─ 否 → 绝不使用 Must——返回 error
|
||||
```
|
||||
|
||||
### 适当的使用场景
|
||||
|
||||
- **包级 `var`**:编译正则表达式、解析模板、加载必需的配置
|
||||
- **`init()` 或 `main()` 早期**:设置程序运行所必需的资源
|
||||
- **测试辅助函数**:测试中优先使用 `t.Fatal`,但 Must 在测试 fixture 中是可以接受的
|
||||
|
||||
### 绝不使用 Must 的场景
|
||||
|
||||
- 运行时请求处理
|
||||
- 用户提供的输入
|
||||
- 可能合理失败的网络或文件操作
|
||||
- 程序启动后调用的任何代码
|
||||
|
||||
## 编写 Must 函数
|
||||
|
||||
遵循命名约定 `MustX`,其中 `X` 是可能失败的函数名:
|
||||
|
||||
```go
|
||||
func MustParseConfig(path string) *Config {
|
||||
cfg, err := ParseConfig(path)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("parsing config %s: %v", path, err))
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
```
|
||||
|
||||
### 指南
|
||||
|
||||
- **命名**:`Must` 前缀 + 可能失败的函数名(例如 `MustParse`、`MustNew`、`MustCompile`)
|
||||
- **Panic 消息**:包含输入和错误信息以便调试
|
||||
- **文档**:始终记录函数在出错时会 panic
|
||||
|
||||
```go
|
||||
// MustParseConfig 解析路径处的配置文件。
|
||||
// 如果文件无法读取或包含无效配置,则会 panic。
|
||||
func MustParseConfig(path string) *Config { ... }
|
||||
```
|
||||
|
||||
### 泛型 Must 辅助函数
|
||||
|
||||
对于一次性使用,泛型 Must 辅助函数可以避免样板代码:
|
||||
|
||||
```go
|
||||
func Must[T any](v T, err error) T {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// 在包级别使用
|
||||
var cfg = Must(ParseConfig("app.yaml"))
|
||||
```
|
||||
|
||||
## 与 Panic/Recover 的关系
|
||||
|
||||
Must 函数是对 `panic` 的受控使用。它们应该:
|
||||
|
||||
- 仅在初始化期间运行(因此不需要 recover)
|
||||
- 产生清晰、可操作的 panic 消息
|
||||
- 绝不在可以返回 error 的场景中使用
|
||||
|
||||
完整的 panic/recover 模式请参见 [PANIC-RECOVER.md](PANIC-RECOVER.md)。
|
||||
@@ -0,0 +1,161 @@
|
||||
# Panic 与 Recover 模式
|
||||
|
||||
> **来源**:Effective Go
|
||||
|
||||
## Panic 指南
|
||||
|
||||
`panic` 创建一个运行时错误来停止程序。仅在真正不可恢复的情况下使用。
|
||||
|
||||
### 何时 Panic
|
||||
|
||||
真正的库函数应**避免 panic**。如果问题可以被掩盖或绕过,让程序继续运行,而不是让整个程序崩溃。
|
||||
|
||||
```go
|
||||
// 可接受:真正不可能的情况
|
||||
func CubeRoot(x float64) float64 {
|
||||
z := x/3
|
||||
for i := 0; i < 1e6; i++ {
|
||||
prevz := z
|
||||
z -= (z*z*z-x) / (3*z*z)
|
||||
if veryClose(z, prevz) {
|
||||
return z
|
||||
}
|
||||
}
|
||||
// 百万次迭代仍未收敛;出了问题。
|
||||
panic(fmt.Sprintf("CubeRoot(%g) did not converge", x))
|
||||
}
|
||||
```
|
||||
|
||||
### 初始化中的 Panic
|
||||
|
||||
例外:如果库在 `init()` 期间确实无法完成初始化,panic 可能是合理的:
|
||||
|
||||
```go
|
||||
var user = os.Getenv("USER")
|
||||
|
||||
func init() {
|
||||
if user == "" {
|
||||
panic("no value for $USER")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 何时 Panic 是可接受的
|
||||
|
||||
除了初始化之外,panic 在以下窄泛场景中是可接受的:
|
||||
|
||||
1. **API 误用**——类似于核心语言对越界访问的 panic。`reflect` 包使用了这种方法。
|
||||
2. **带有匹配 `recover` 的内部实现细节**在包边界处。Panic 简化了深层嵌套的控制流,而公共 API 仍然返回 error(下方的 Parse/parseInt 模式)。
|
||||
3. **`panic("unreachable")`** 在 `log.Fatal` 之后,当编译器无法检测到不可达代码时。
|
||||
|
||||
#### Parse/parseInt 模式
|
||||
|
||||
在内部使用 panic 来回退复杂的递归,但始终在包边界处转换为 error:
|
||||
|
||||
```go
|
||||
func parseInt(in string) int {
|
||||
n, err := strconv.Atoi(in)
|
||||
if err != nil {
|
||||
panic(&syntaxError{"not a valid integer"})
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func Parse(in string) (_ *Node, err error) {
|
||||
defer func() {
|
||||
if p := recover(); p != nil {
|
||||
sErr, ok := p.(*syntaxError)
|
||||
if !ok {
|
||||
panic(p) // 不是我们的——重新 panic
|
||||
}
|
||||
err = fmt.Errorf("syntax error: %v", sErr.msg)
|
||||
}
|
||||
}()
|
||||
// ... 内部调用 parseInt
|
||||
}
|
||||
```
|
||||
|
||||
**关键**:类型检查 `p.(*syntaxError)` 确保只捕获*我们的* panic。意外的 panic(nil 指针等)正常传播。
|
||||
|
||||
---
|
||||
|
||||
## Recover 模式
|
||||
|
||||
`recover` 重新获得对正在 panic 的 goroutine 的控制。它只在延迟函数中有效。
|
||||
|
||||
### 基本恢复模式
|
||||
|
||||
```go
|
||||
func safelyDo(work *Work) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Println("work failed:", err)
|
||||
}
|
||||
}()
|
||||
do(work)
|
||||
}
|
||||
```
|
||||
|
||||
### 服务器 Goroutine 保护
|
||||
|
||||
在服务器中将 panic 隔离到各个 goroutine:
|
||||
|
||||
```go
|
||||
func server(workChan <-chan *Work) {
|
||||
for work := range workChan {
|
||||
go safelyDo(work) // 每个 worker 都受保护
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
如果 `do(work)` panic,结果会被记录,goroutine 干净退出而不影响其他 goroutine。
|
||||
|
||||
### 包内部的 Panic/Recover
|
||||
|
||||
在内部使用 panic 但在 API 边界处转换为 error:
|
||||
|
||||
```go
|
||||
// Error 是一个解析错误类型
|
||||
type Error string
|
||||
func (e Error) Error() string { return string(e) }
|
||||
|
||||
// 内部:使用 Error 类型 panic
|
||||
func (regexp *Regexp) error(err string) {
|
||||
panic(Error(err))
|
||||
}
|
||||
|
||||
// 外部 API:将 panic 转换为 error 返回
|
||||
func Compile(str string) (regexp *Regexp, err error) {
|
||||
regexp = new(Regexp)
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
regexp = nil
|
||||
err = e.(Error) // 如果不是我们的 Error 类型则重新 panic
|
||||
}
|
||||
}()
|
||||
return regexp.doParse(str), nil
|
||||
}
|
||||
```
|
||||
|
||||
**要点:**
|
||||
|
||||
- 延迟函数可以修改命名返回值
|
||||
- 类型断言 `e.(Error)` 对意外错误类型重新 panic
|
||||
- 绝不向客户端暴露 panic——始终在 API 边界处转换
|
||||
|
||||
---
|
||||
|
||||
## 快速参考
|
||||
|
||||
| 模式 | 描述 |
|
||||
|------|------|
|
||||
| 基本恢复 | `defer func() { if err := recover(); err != nil { ... } }()` |
|
||||
| 服务器保护 | 将每个 goroutine 处理器包装在 safelyDo 中 |
|
||||
| 包内部 | 内部 panic,在 API 边界处 recover 并返回 error |
|
||||
| 类型安全恢复 | 使用类型断言对意外错误重新 panic |
|
||||
|
||||
## 何时使用
|
||||
|
||||
- **Panic**:仅用于真正不可恢复的情况或初始化失败
|
||||
- **Recover**:服务器处理器、包内部错误简化
|
||||
- **绝不**:跨包边界暴露 panic——始终转换为 error
|
||||
@@ -0,0 +1,111 @@
|
||||
# 时间、结构体标签和嵌入模式
|
||||
|
||||
## 使用 time.Time 和 time.Duration
|
||||
|
||||
始终使用 `time` 包。避免使用原始 `int` 表示时间值。
|
||||
|
||||
### 时间点
|
||||
|
||||
**不好**
|
||||
```go
|
||||
func isActive(now, start, stop int) bool {
|
||||
return start <= now && now < stop
|
||||
}
|
||||
```
|
||||
|
||||
**好**
|
||||
```go
|
||||
func isActive(now, start, stop time.Time) bool {
|
||||
return (start.Before(now) || start.Equal(now)) && now.Before(stop)
|
||||
}
|
||||
```
|
||||
|
||||
### 时长
|
||||
|
||||
**不好**
|
||||
```go
|
||||
func poll(delay int) {
|
||||
time.Sleep(time.Duration(delay) * time.Millisecond)
|
||||
}
|
||||
poll(10) // 秒?毫秒?
|
||||
```
|
||||
|
||||
**好**
|
||||
```go
|
||||
func poll(delay time.Duration) {
|
||||
time.Sleep(delay)
|
||||
}
|
||||
poll(10 * time.Second)
|
||||
```
|
||||
|
||||
### JSON 字段
|
||||
|
||||
当无法使用 `time.Duration` 时,在字段名中包含单位:
|
||||
|
||||
**不好**
|
||||
```go
|
||||
type Config struct {
|
||||
Interval int `json:"interval"`
|
||||
}
|
||||
```
|
||||
|
||||
**好**
|
||||
```go
|
||||
type Config struct {
|
||||
IntervalMillis int `json:"intervalMillis"`
|
||||
}
|
||||
```
|
||||
|
||||
## 避免在公共结构体中嵌入类型
|
||||
|
||||
嵌入类型会泄露实现细节并阻碍类型演进。
|
||||
|
||||
**不好**
|
||||
```go
|
||||
type ConcreteList struct {
|
||||
*AbstractList
|
||||
}
|
||||
```
|
||||
|
||||
**好**
|
||||
```go
|
||||
type ConcreteList struct {
|
||||
list *AbstractList
|
||||
}
|
||||
|
||||
func (l *ConcreteList) Add(e Entity) {
|
||||
l.list.Add(e)
|
||||
}
|
||||
|
||||
func (l *ConcreteList) Remove(e Entity) {
|
||||
l.list.Remove(e)
|
||||
}
|
||||
```
|
||||
|
||||
嵌入的问题:
|
||||
- 向嵌入接口添加方法是破坏性变更
|
||||
- 从嵌入结构体移除方法是破坏性变更
|
||||
- 替换嵌入类型是破坏性变更
|
||||
|
||||
## 在序列化结构体中使用字段标签
|
||||
|
||||
始终为 JSON、YAML 等使用显式字段标签。
|
||||
|
||||
**不好**
|
||||
```go
|
||||
type Stock struct {
|
||||
Price int
|
||||
Name string
|
||||
}
|
||||
```
|
||||
|
||||
**好**
|
||||
```go
|
||||
type Stock struct {
|
||||
Price int `json:"price"`
|
||||
Name string `json:"name"`
|
||||
// 可以安全地将 Name 重命名为 Symbol
|
||||
}
|
||||
```
|
||||
|
||||
标签使序列化契约显式化,并可以安全地进行重构。
|
||||
Reference in New Issue
Block a user