wavelet init

This commit is contained in:
ryan
2026-06-18 15:24:48 +08:00
parent d6a7011885
commit 99738bbc17
714 changed files with 139987 additions and 0 deletions
+107
View File
@@ -0,0 +1,107 @@
---
name: go-functions
description: Use when organizing functions within a Go file, formatting function signatures, designing return values, or following Printf-style naming conventions. Also use when a user is adding or refactoring any Go function, even if they don't mention function design or signature formatting. Does not cover functional options constructors (see go-functional-options).
license: Apache-2.0
metadata:
sources: "Effective Go, Google Style Guide, Uber Style Guide"
---
# Go 函数设计
> **本技能不适用的场景**:对于函数选项构造函数(`WithTimeout`、`WithLogger`),参见 [go-functional-options](../go-functional-options/SKILL.md)。对于错误返回约定,参见 [go-error-handling](../go-error-handling/SKILL.md)。对于函数和方法的命名,参见 [go-naming](../go-naming/SKILL.md)。
---
## 函数分组与排序
按以下规则组织文件中的函数:
1. 函数按**大致调用顺序**排序
2. 函数**按接收者分组**
3. **导出**函数排在最前面,位于 `struct`/`const`/`var` 定义之后
4. `NewXxx`/`newXxx` 构造函数紧跟在类型定义之后
5. 普通工具函数排在文件末尾
```go
type something struct{ ... }
func newSomething() *something { return &something{} }
func (s *something) Cost() int { return calcCost(s.weights) }
func (s *something) Stop() { ... }
func calcCost(n []int) int { ... }
```
---
## 函数签名
> 在格式化多行签名、包装返回值、缩短调用点或用自定义类型替换裸 bool 参数时,阅读 [references/SIGNATURES.md](references/SIGNATURES.md)。
尽量将签名保持在一行内。当必须换行时,将**所有参数放在各自的行上**并加尾随逗号:
```go
func (r *SomeType) SomeLongFunctionName(
foo1, foo2, foo3 string,
foo4, foo5, foo6 int,
) {
foo7 := bar(foo1)
}
```
为含义不明确的参数添加 `/* name */` 注释,或者更好的做法是用自定义类型替换裸 `bool` 参数。
---
## 接口指针
几乎不需要指向接口的指针。将接口作为值传递——底层数据仍然可以是指针。
```go
// 不好:接口指针
func process(r *io.Reader) { ... }
// 好:传递接口值
func process(r io.Reader) { ... }
```
---
## Printf 与 Stringer
> 在使用 %v/%s/%d 之外的 Printf 动词、实现 fmt.Stringer 或 fmt.GoStringer、编写自定义 Format() 方法或调试 String() 方法中的无限递归时,阅读 [references/PRINTF-STRINGER.md](references/PRINTF-STRINGER.md)。
### Printf 风格函数名
接受格式字符串的函数应以 `f` 结尾,以便 `go vet` 支持。在 `Printf` 调用之外使用格式字符串时,将其声明为 `const`。
在格式化日志或错误消息中的字符串时,优先使用 `%q` 而非手动加引号的 `%s`——它能安全地转义特殊字符并加上引号:
```go
return fmt.Errorf("unknown key %q", key) // 输出:unknown key "foo\nbar"
```
设计具有 3 个以上可选参数的构造函数时,参见 **go-functional-options**。
---
## 快速参考
| 主题 | 规则 |
|------|------|
| 文件排序 | 类型 -> 构造函数 -> 导出 -> 未导出 -> 工具函数 |
| 签名换行 | 所有参数各占一行,加尾随逗号 |
| 裸参数 | 添加 `/* name */` 注释或使用自定义类型 |
| 接口指针 | 几乎不需要;按值传递接口 |
| Printf 函数名 | 以 `f` 结尾以支持 `go vet` |
---
## 相关技能
- **错误返回**:在设计错误返回模式或在多返回值函数中包装错误时,参见 [go-error-handling](../go-error-handling/SKILL.md)
- **命名约定**:在为函数、方法命名或选择 getter/setter 模式时,参见 [go-naming](../go-naming/SKILL.md)
- **函数选项**:在设计具有 3 个以上可选参数的构造函数时,参见 [go-functional-options](../go-functional-options/SKILL.md)
- **格式化原则**:在决定行长度、裸返回或签名格式时,参见 [go-style-core](../go-style-core/SKILL.md)
@@ -0,0 +1,264 @@
# Printf、Stringer 与自定义格式化
Go 的 `fmt` 打印动词、`Stringer` 和 `GoStringer` 接口、自定义 `Format()` 方法以及常见陷阱的深度参考。
---
## Printf 动词
### 通用动词
| 动词 | 用途 |
|------|------|
| `%v` | 默认格式(结构体字段、切片元素) |
| `%+v` | 带字段名的结构体:`{Name:alice Age:30}` |
| `%#v` | Go 语法表示:`main.User{Name:"alice", Age:30}` |
| `%T` | 值的类型:`main.User` |
| `%%` | 字面百分号 |
### 字符串与字节动词
| 动词 | 用途 |
|------|------|
| `%s` | 纯字符串或字节切片 |
| `%q` | 带 Go 语法转义的引号字符串:`"hello\n"` |
| `%x` | 十六进制编码,小写:`68656c6c6f` |
| `%X` | 十六进制编码,大写:`68656C6C6F` |
### 整数动词
| 动词 | 用途 |
|------|------|
| `%d` | 十进制整数 |
| `%b` | 二进制 |
| `%o` | 八进制 |
| `%O` | 带 `0o` 前缀的八进制 |
| `%x` | 十六进制,小写 |
| `%X` | 十六进制,大写 |
### 浮点数动词
| 动词 | 用途 |
|------|------|
| `%f` | 小数点,无指数:`123.456` |
| `%e` | 科学计数法:`1.23456e+02` |
| `%g` | 紧凑格式:大指数用 `%e`,否则用 `%f` |
### 宽度与精度
```go
fmt.Sprintf("%10d", 42) // " 42" (宽度 10,右对齐)
fmt.Sprintf("%-10d", 42) // "42 " (宽度 10,左对齐)
fmt.Sprintf("%.2f", 3.14159) // "3.14" (2 位小数)
fmt.Sprintf("%010d", 42) // "0000000042" (零填充)
```
---
## 使用 `%q` 输出字符串
`%q` 动词在双引号内打印字符串,使空字符串和控制字符可见:
```go
fmt.Printf("value %q looks like English text", someText)
// 不好:手动添加引号
fmt.Printf("value \"%s\" looks like English text", someText)
```
在面向人类的输出中,如果值可能为空或包含控制字符,优先使用 `%q`。
---
## Printf 之外的格式字符串
在 `Printf` 风格调用之外声明格式字符串时,使用 `const`。这样 `go vet` 可以进行静态分析:
```go
// 不好:变量格式字符串——go vet 无法检查
msg := "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2)
// 好:常量格式字符串——go vet 可以验证
const msg = "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2)
```
---
## Printf 风格函数的命名
接受格式字符串的函数应以 `f` 结尾。这样 `go vet` 可以自动检查格式字符串:
```go
func Wrapf(err error, format string, args ...any) error
```
如果使用非标准名称,需要告知 `go vet`:
```bash
go vet -printfuncs=wrapf,statusf
```
---
## `fmt.Stringer` 接口
实现 `fmt.Stringer` 来控制类型在 `%v` 和 `%s` 下的显示方式:
```go
type fmt.Stringer interface {
String() string
}
```
```go
type Point struct{ X, Y int }
func (p Point) String() string {
return fmt.Sprintf("(%d, %d)", p.X, p.Y)
}
// fmt.Println(Point{1, 2}) → "(1, 2)"
// fmt.Sprintf("point: %v", p) → "point: (1, 2)"
// fmt.Sprintf("point: %s", p) → "point: (1, 2)"
```
### 何时实现 Stringer
- 类型将出现在日志消息或面向用户的输出中
- 默认的 `%v` 输出(仅字段值)不够有意义
- 需要一种区别于序列化的、对人类友好的表示
---
## `fmt.GoStringer` 接口
实现 `fmt.GoStringer` 来控制 `%#v` 输出。这对于默认 Go 语法表示具有误导性或过于冗长的类型很有用:
```go
type fmt.GoStringer interface {
GoString() string
}
```
```go
type Color struct{ R, G, B uint8 }
func (c Color) GoString() string {
return fmt.Sprintf("Color(%#02x, %#02x, %#02x)", c.R, c.G, c.B)
}
// fmt.Sprintf("%#v", Color{255, 128, 0})
// → "Color(0xff, 0x80, 0x00)" 而非 "main.Color{R:0xff, G:0x80, B:0x00}"
```
`GoString()` 的输出应该是有效的 Go 语法或接近有效语法——它用于调试,而非面向用户的显示。
---
## 使用 `fmt.Formatter` 自定义格式化
要完全控制所有格式动词,实现 `fmt.Formatter`:
```go
type fmt.Formatter interface {
Format(f fmt.State, verb rune)
}
```
```go
type Point struct{ X, Y int }
func (p Point) Format(f fmt.State, verb rune) {
switch verb {
case 'v':
if f.Flag('#') {
// %#v——Go 语法表示
fmt.Fprintf(f, "Point{X: %d, Y: %d}", p.X, p.Y)
return
}
if f.Flag('+') {
// %+v——带字段名的详细格式
fmt.Fprintf(f, "X:%d Y:%d", p.X, p.Y)
return
}
// %v——默认
fmt.Fprintf(f, "(%d, %d)", p.X, p.Y)
case 's':
fmt.Fprintf(f, "(%d, %d)", p.X, p.Y)
case 'q':
fmt.Fprintf(f, "%q", p.String())
default:
fmt.Fprintf(f, "%%!%c(Point=%d,%d)", verb, p.X, p.Y)
}
}
```
### `fmt.State` 方法
| 方法 | 返回值 |
|------|--------|
| `Flag(c int) bool` | 标志(`+`、`-`、`#`、`0`、` `)是否设置 |
| `Width() (int, bool)` | 宽度值以及是否指定了宽度 |
| `Precision() (int, bool)` | 精度值以及是否指定了精度 |
| `Write(b []byte) (int, error)` | 写入输出字节 |
仅在 `String()` 不够用时才实现 `fmt.Formatter`——很少需要这样做。常见原因:需要为 `%v`、`%+v`、`%#v` 提供不同输出,或者需要遵循宽度/精度标志。
---
## 无限递归陷阱
**在 `String()` 方法内部对接收者使用 `%s` 或 `%v` 调用 `fmt.Sprintf` 会导致无限递归:**
```go
type MyString string
// BUG:无限递归——Sprintf 调用 String(),String() 又调用 Sprintf...
func (m MyString) String() string {
return fmt.Sprintf("MyString: %s", m) // 崩溃:栈溢出
}
```
修复方法——将接收者转换为其底层类型以打破方法集:
```go
func (m MyString) String() string {
return fmt.Sprintf("MyString: %s", string(m)) // 安全:string 没有 String()
}
```
此陷阱还适用于:
- 底层类型为 string、[]byte 或另一个 Stringer 的类型
- 任何使用 `%s` 或 `%v` 格式化 `self` 的 `String()` 方法
- 使用 `%#v` 格式化 `self` 的 `GoString()` 方法
```go
type IPAddr [4]byte
// BUG:%v 调用 String(),无限递归
func (ip IPAddr) String() string {
return fmt.Sprintf("%v.%v.%v.%v", ip[0], ip[1], ip[2], ip[3])
// 这里安全——ip[0] 是 byte(uint8),没有 String() 方法。
// 但如果 ip 是一个包装了 Stringer 的命名类型,就会递归。
}
```
**经验法则**:在 `String()` 内部,永远不要将接收者(或重新转换为自身类型的接收者)传递给 `%s` 或 `%v` 动词。先转换为底层原始类型。
---
## 快速参考
| 主题 | 规则 |
|------|------|
| `%q` | 用于人类可读的字符串输出 |
| `%+v` | 带字段名的结构体 |
| `%#v` | Go 语法表示;通过 `GoStringer` 自定义 |
| 格式字符串存储 | 在 Printf 调用之外声明为 `const` |
| Printf 函数名 | 以 `f` 结尾以支持 `go vet` |
| `Stringer` | 实现 `String() string` 用于 `%v`/`%s` 输出 |
| `GoStringer` | 实现 `GoString() string` 用于 `%#v` 输出 |
| `Formatter` | 实现 `Format(fmt.State, rune)` 以完全控制动词 |
| 递归陷阱 | 永远不要在 `String()` 内部使用 `Sprintf("%s", receiver)`;转换为底层类型 |
@@ -0,0 +1,168 @@
# 函数签名
格式化 Go 函数签名、避免裸参数以及保持调用点可读性的详细规则。
---
## 单行 vs 多行
当签名能轻松放在一行时保持单行。当必须换行时,将**所有参数放在各自的行上**并加尾随逗号:
**不好**——部分换行使对齐变得脆弱:
```go
func (r *SomeType) SomeLongFunctionName(foo1, foo2, foo3 string,
foo4, foo5, foo6 int) {
foo7 := bar(foo1)
}
```
**好**——完全换行,尾随逗号:
```go
func (r *SomeType) SomeLongFunctionName(
foo1, foo2, foo3 string,
foo4, foo5, foo6 int,
) {
foo7 := bar(foo1)
}
```
### 返回值
当返回值也需要换行时,遵循相同的模式:
```go
func (r *SomeType) LongName(
foo1, foo2, foo3 string,
foo4, foo5, foo6 int,
) (
*Result,
error,
) {
// ...
}
```
对于更简单的情况,命名返回值可以与参数右括号在同一行:
```go
func (r *SomeType) LongName(
foo1, foo2, foo3 string,
) (result *Result, err error) {
// ...
}
```
---
## 缩短调用点
提取局部变量,而不是将函数调用拆分到多行:
```go
// 不好:过长的内联调用
result := foo.Call(
somePackage.ComplexFunction(arg1, arg2),
anotherPackage.Transform(data),
defaultOptions,
)
// 好:提取局部变量以提高清晰度
transformed := anotherPackage.Transform(data)
computed := somePackage.ComplexFunction(arg1, arg2)
result := foo.Call(computed, transformed, defaultOptions)
```
这提高了可读性,并使中间值可用于调试。
---
## 避免裸参数
函数调用中的裸参数会降低可读性。为含义不明确的参数添加 C 风格注释:
```go
// 不好:这些布尔值是什么意思?
printInfo("foo", true, true)
// 好:内联注释说明了意图
printInfo("foo", true /* isLocal */, true /* done */)
```
更好的做法是用自定义类型替换裸 `bool` 参数:
```go
type Region int
const (
UnknownRegion Region = iota
Local
)
type Status int
const (
Pending Status = iota
Done
)
func printInfo(name string, region Region, status Status)
```
### 何时使用每种方法
| 方法 | 时机 |
|------|------|
| C 风格注释 | 快速修复;调用点少;无法修改的第三方 API |
| 自定义类型 | 多个调用点;公开 API;多个 bool/int 参数 |
| 函数选项 | 3 个以上可选参数;参见 [go-functional-options](../../go-functional-options/SKILL.md) |
---
## 分组相关参数
当函数接受多个相同类型的参数时,将它们分组:
```go
// 可接受:将同类型参数分组
func Copy(dst, src string) error
// 可接受:尽管类型相同,但含义不同时分开声明
func Move(source string, destination string) error
```
当参数名称能清楚表明角色时使用分组;当不能清楚表明时使用分开声明。
---
## 方法接收者的位置
接收者放在函数名之前,格式类似于参数:
```go
// 短接收者——放在同一行
func (s *Server) Start(ctx context.Context) error { ... }
// 长接收者类型——如果整行过长则考虑换行
func (h *ComplicatedHandler) ServeHTTP(
w http.ResponseWriter,
r *http.Request,
) { ... }
```
参见 [go-naming](../../go-naming/SKILL.md) 了解接收者命名约定(简短的一到两个字母缩写)。
---
## 快速参考
| 主题 | 规则 |
|------|------|
| 单行 | 能放下时保持一行 |
| 多行 | 所有参数各占一行,尾随逗号 |
| 返回值换行 | 与参数相同的模式 |
| 调用点 | 提取局部变量而不是拆分调用 |
| 裸 bool | 添加 `/* name */` 注释或使用自定义类型 |
| 分组参数 | 当名称能清楚表明角色时将同类型分组 |
| 接收者 | 在函数名之前;简短缩写 |