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
+151
View File
@@ -0,0 +1,151 @@
---
name: go-interfaces
description: Use when defining or implementing Go interfaces, designing abstractions, creating mockable boundaries for testing, or composing types through embedding. Also use when deciding whether to accept an interface or return a concrete type, or using type assertions or type switches, even if the user doesn't explicitly mention interfaces. Does not cover generics-based polymorphism (see go-generics).
license: Apache-2.0
metadata:
sources: "Effective Go, Google Style Guide, Uber Style Guide"
allowed-tools: Bash(bash:*)
---
# Go 接口与组合
## 可用脚本
- **`scripts/check-interface-compliance.sh`**——查找缺少编译时合规性检查(`var _ I = (*T)(nil)`)的导出接口。运行 `bash scripts/check-interface-compliance.sh --help` 查看选项。
---
## 接受接口,返回具体类型
接口属于**消费**值的包,而不是**实现**值的包。从构造函数返回具体类型(通常是指针或结构体),这样可以在不重构的情况下添加新方法。
```go
// 好:消费者定义自己需要的接口
package consumer
type Thinger interface { Thing() bool }
func Foo(t Thinger) string { ... }
```
```go
// 好:生产者返回具体类型
package producer
type Thinger struct{ ... }
func (t Thinger) Thing() bool { ... }
func NewThinger() Thinger { return Thinger{ ... } }
```
```go
// 不好:生产者定义并返回自己的接口
package producer
type Thinger interface { Thing() bool }
type defaultThinger struct{ ... }
func NewThinger() Thinger { return defaultThinger{ ... } }
```
**不要在接口被使用之前定义它。** 如果没有现实的使用示例,很难判断接口是否真的有必要。
---
## 通用性:隐藏实现,暴露接口
如果一个类型仅用于实现某个接口,且没有该接口之外的导出方法,则从构造函数返回接口以隐藏实现:
```go
func NewHash() hash.Hash32 {
return &myHash{} // 未导出的类型
}
```
好处:实现可以在不影响调用者的情况下更改,替换算法只需更改构造函数调用。
---
## 类型断言:Comma-Ok 模式
不进行检查的话,失败的断言会导致运行时 panic。始终使用 comma-ok 模式进行安全测试:
```go
str, ok := value.(string)
if ok {
fmt.Printf("string value is: %q\n", str)
}
```
检查值是否实现了某个接口:
```go
if _, ok := val.(json.Marshaler); ok {
fmt.Printf("value %v implements json.Marshaler\n", val)
}
```
---
## 类型切换
重用变量名是惯用做法(`t := t.(type)`)——变量在每个 case 分支中拥有正确的类型。当 case 列出多个类型(`case int, int64:`)时,变量拥有接口类型。
---
## 嵌入
避免在公开结构体中嵌入类型——内部类型的完整方法集将成为你公开 API 的一部分。改用未导出的字段。
> 在使用结构体嵌入进行组合、重写嵌入方法、解决名称冲突、应用 HandlerFunc 适配器模式或决定是否在公开 API 类型中使用嵌入时,阅读 [references/EMBEDDING.md](references/EMBEDDING.md)。
---
## 接口满足检查
使用空标识符赋值在编译时验证类型是否实现了接口:
```go
var _ json.Marshaler = (*RawMessage)(nil)
```
如果 `*RawMessage` 没有实现 `json.Marshaler`,这会导致编译错误。
在以下情况下使用此模式:
- 没有能自动验证接口的静态转换
- 类型必须满足接口才能正确运行(例如自定义 JSON 序列化)
- 接口更改应该导致编译失败,而不是静默降级
**不要**为每个接口都添加这些检查——仅在没有其他静态转换能捕获错误时才使用。
> **验证**:在定义接口或实现后,运行 `bash scripts/check-interface-compliance.sh` 验证所有具体类型都有编译时的 `var _ I = (*T)(nil)` 检查。
---
## 接收者类型
如果不确定,使用指针接收者。不要在单个类型上混合接收者类型——如果任何方法需要指针,则所有方法都使用指针。仅在小型不可变类型(`Point`、`time.Time`)或基本类型上使用值接收者。
> 在为新类型决定使用指针接收者还是值接收者时,特别是对于包含 sync 原语或大型结构体的类型,阅读 [references/RECEIVER-TYPE.md](references/RECEIVER-TYPE.md)。
---
## 快速参考
| 概念 | 模式 | 说明 |
|------|------|------|
| 消费者拥有接口 | 在使用处定义接口 | 不在实现包中 |
| 安全类型断言 | `v, ok := x.(Type)` | 返回零值 + false |
| 类型切换 | `switch v := x.(type)` | 变量在每个 case 中拥有正确类型 |
| 接口嵌入 | `type RW interface { Reader; Writer }` | 方法的并集 |
| 结构体嵌入 | `type S struct { *T }` | 提升 T 的方法 |
| 接口检查 | `var _ I = (*T)(nil)` | 编译时验证 |
| 通用性 | 从构造函数返回接口 | 隐藏实现 |
---
## 相关技能
- **接口命名**:在为接口命名(`-er` 后缀约定)或选择接收者名称时,参见 [go-naming](../go-naming/SKILL.md)
- **错误类型**:在实现 `error` 接口、自定义错误类型或 `errors.As` 匹配时,参见 [go-error-handling](../go-error-handling/SKILL.md)
- **泛型 vs 接口**:在决定是否需要泛型或接口是否已足够时,参见 [go-generics](../go-generics/SKILL.md)
- **函数选项**:在使用基于接口的 Option 模式实现灵活构造函数时,参见 [go-functional-options](../go-functional-options/SKILL.md)
- **编译时检查**:在 API 边界添加 `var _ I = (*T)(nil)` 满足检查时,参见 [go-defensive](../go-defensive/SKILL.md)
@@ -0,0 +1,138 @@
# Go 中的嵌入模式
> **来源**:Effective Go、Uber 风格指南
Go 使用嵌入来实现组合而非继承。嵌入将内部类型的方法提升到外部类型,自动满足接口。
## 接口嵌入
通过嵌入来组合接口:
```go
type ReadWriter interface {
Reader
Writer
}
```
`ReadWriter` 既能做 `Reader` 能做的事,*也能*做 `Writer` 能做的事。接口中只能嵌入接口。
## 结构体嵌入
嵌入将内部类型的方法提升到外部类型,无需显式转发。
```go
type ReadWriter struct {
*Reader // *bufio.Reader
*Writer // *bufio.Writer
}
```
通过嵌入,`bufio.ReadWriter` 自动满足 `io.Reader`、`io.Writer` 和 `io.ReadWriter`。
混合使用嵌入字段和命名字段:
```go
type Job struct {
Command string
*log.Logger
}
job.Println("starting now...")
job.Logger.SetPrefix("Job: ")
```
## 方法重写
在外部类型上定义方法以重写提升的方法:
```go
func (job *Job) Printf(format string, args ...any) {
job.Logger.Printf("%q: %s", job.Command, fmt.Sprintf(format, args...))
}
```
外部方法优先——对 `job.Printf(...)` 的调用会调用外部方法,而嵌入方法仍可通过 `job.Logger.Printf(...)` 访问。
## 嵌入 vs 子类化
当调用嵌入方法时,接收者是**内部**类型,而非外部类型。嵌入类型不知道自己被嵌入——不存在类似于 `this` 或 `super` 的引用指向包含它的类型。
```go
type Base struct{}
func (b *Base) Name() string { return "Base" }
type Derived struct{ Base }
d := Derived{}
d.Name() // 返回 "Base",而非 "Derived"
```
## 名称冲突解决
1. **外部隐藏内部**——外部类型上的字段或方法会遮蔽嵌入类型在同名位置提升的字段或方法
2. **同级冲突是错误**——如果两个同深度的嵌入类型提升了相同的名称,则为编译错误(除非该名称从未被访问)
```go
type A struct{}
func (A) Hello() string { return "A" }
type B struct{}
func (B) Hello() string { return "B" }
type C struct {
A
B
}
// c.Hello() // 编译错误:选择器不明确
c.A.Hello() // 可以:显式消歧
```
## 不要在公开结构体中嵌入
嵌入将内部类型的完整方法集暴露为你的公开 API 的一部分。这带来了维护负担:嵌入类型方法的更改会破坏 API 的兼容性保证。
**不好**
```go
type SMap struct {
sync.Mutex // Lock 和 Unlock 现在是 SMap API 的一部分
data map[string]string
}
```
**好**
```go
type SMap struct {
mu sync.Mutex // 未导出的字段——实现细节
data map[string]string
}
func (m *SMap) Get(k string) string {
m.mu.Lock()
defer m.mu.Unlock()
return m.data[k]
}
```
例外:在测试类型和 API 稳定性无关紧要的内部结构体中,嵌入是可以接受的。
## HandlerFunc 适配器模式
方法可以在任何命名类型上定义,不仅仅是结构体。`http.HandlerFunc` 模式将普通函数转换为接口实现:
```go
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, req *Request) {
f(w, req)
}
```
任何具有正确签名的函数都可以成为 HTTP 处理器:
```go
http.Handle("/args", http.HandlerFunc(ArgServer))
```
这种适配器模式在需要让独立函数满足单方法接口时非常有用。
@@ -0,0 +1,68 @@
# 接收者类型:指针 vs 值
> **建议**:Go Wiki CodeReviewComments
选择在方法上使用值接收者还是指针接收者可能很困难。**如果不确定,使用指针**,但有时值接收者也是合理的。
## 何时使用指针接收者
- **方法修改接收者**:接收者必须是指针
- **接收者包含 sync.Mutex 或类似类型**:必须使用指针以避免复制
- **大型结构体或数组**:指针接收者更高效。如果将所有元素作为参数传递感觉太大,那对值接收者来说也太大了
- **并发或被调方法可能修改**:如果更改必须对原始接收者可见,则必须使用指针
- **元素是指向可变内容的指针**:优先使用指针接收者使意图更清晰
## 何时使用值接收者
- **小型不变的结构体或基本类型**:值接收者以提高效率
- **Map、func 或 chan**:不要对它们使用指针
- **不重新切片/重新分配的切片**:如果方法不重新切片或重新分配切片,不要使用指针
- **没有可变字段的小型值类型**:像 `time.Time` 这样没有可变字段且没有指针的类型适合作为值接收者
- **简单基本类型**:`int`、`string` 等
```go
// 值接收者:小型、不可变类型
type Point struct {
X, Y float64
}
func (p Point) Distance(q Point) float64 {
return math.Hypot(q.X-p.X, q.Y-p.Y)
}
// 指针接收者:方法修改接收者
func (p *Point) ScaleBy(factor float64) {
p.X *= factor
p.Y *= factor
}
// 指针接收者:包含 sync.Mutex
type Counter struct {
mu sync.Mutex
count int
}
func (c *Counter) Increment() {
c.mu.Lock()
c.count++
c.mu.Unlock()
}
```
## 一致性规则
**不要混合接收者类型**。为类型上所有可用的方法统一选择指针或结构体类型。如果任何方法需要指针接收者,则所有方法都使用指针接收者。
```go
// 好:一致的指针接收者
type Buffer struct {
data []byte
}
func (b *Buffer) Write(p []byte) (int, error) { /* ... */ }
func (b *Buffer) Read(p []byte) (int, error) { /* ... */ }
func (b *Buffer) Len() int { return len(b.data) }
// 不好:混合接收者类型
func (b Buffer) Len() int { return len(b.data) } // 不一致
```
@@ -0,0 +1,224 @@
#!/usr/bin/env bash
set -euo pipefail
VERSION="1.0.0"
SCRIPT_NAME="$(basename "$0")"
usage() {
cat <<EOF
$SCRIPT_NAME v$VERSION — Check for missing compile-time interface compliance verifications
USAGE
bash $SCRIPT_NAME [options] [path]
DESCRIPTION
Scans Go files for exported interface definitions and checks whether each
has a corresponding compile-time compliance assertion like:
var _ MyInterface = (*MyImpl)(nil)
var _ MyInterface = MyImpl{}
Reports interfaces that lack such compile-time checks. This helps catch
interface drift at compile time instead of runtime.
Exits 0 if all interfaces are verified, 1 if missing checks found, 2 on error.
OPTIONS
-h, --help Show this help message
-v, --version Show version
--json Output results as JSON
--include-test Also scan _test.go files for compliance checks
--limit N Show at most N results (default: all)
ARGUMENTS
path Directory to scan (default: current directory)
EXAMPLES
bash $SCRIPT_NAME
bash $SCRIPT_NAME ./pkg/storage
bash $SCRIPT_NAME --json .
bash $SCRIPT_NAME --include-test ./internal
EOF
}
JSON_OUTPUT=false
INCLUDE_TEST=false
LIMIT=0
TARGET=""
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help) usage; exit 0 ;;
-v|--version) echo "$SCRIPT_NAME v$VERSION"; exit 0 ;;
--json) JSON_OUTPUT=true; shift ;;
--include-test) INCLUDE_TEST=true; shift ;;
--limit) LIMIT="${2:?error: --limit requires a number}"; shift 2 ;;
-*) echo "error: unknown option: $1" >&2; usage >&2; exit 2 ;;
*) TARGET="$1"; shift ;;
esac
done
TARGET="${TARGET:-.}"
if [[ ! -d "$TARGET" && ! -f "$TARGET" ]]; then
# Handle ./... patterns
dir="${TARGET%%/...}"
dir="${dir:-.}"
if [[ ! -d "$dir" ]]; then
echo "error: path not found: $TARGET" >&2
exit 2
fi
TARGET="$dir"
fi
json_escape() {
local s="$1"
s="${s//\\/\\\\}"
s="${s//\"/\\\"}"
s="${s//$'\t'/\\t}"
s="${s//$'\r'/}"
s="${s//$'\n'/\\n}"
printf '%s' "$s"
}
# Collect all Go source files
find_go_files() {
local t="$1"
if $INCLUDE_TEST; then
find "$t" -name '*.go' ! -path '*/vendor/*' ! -path '*/.git/*' 2>/dev/null
else
find "$t" -name '*.go' ! -name '*_test.go' ! -path '*/vendor/*' ! -path '*/.git/*' 2>/dev/null
fi
}
# Collect all Go files (including tests) for checking compliance vars
find_all_go_files() {
find "$1" -name '*.go' ! -path '*/vendor/*' ! -path '*/.git/*' 2>/dev/null
}
# Step 1: Find all exported interface definitions
IFACE_NAMES=()
IFACE_LOCATIONS=()
while IFS= read -r file; do
[[ -n "$file" ]] || continue
line_num=0
while IFS= read -r line; do
line_num=$((line_num + 1))
# Match: type ExportedName interface {
pat='^[[:space:]]*type[[:space:]]+([A-Z][a-zA-Z0-9]*)[[:space:]]+interface[[:space:]]*\{'
if [[ "$line" =~ $pat ]]; then
iface_name="${BASH_REMATCH[1]}"
IFACE_NAMES+=("$iface_name")
IFACE_LOCATIONS+=("$file:$line_num")
fi
done < "$file"
done < <(find_go_files "$TARGET")
if [[ ${#IFACE_NAMES[@]} -eq 0 ]]; then
if $JSON_OUTPUT; then
echo '{"interfaces":[],"missing":[],"count_interfaces":0,"count_missing":0}'
else
echo "No exported interfaces found in: $TARGET"
fi
exit 0
fi
# Step 2: Scan all Go files (including tests) for compliance checks
# Pattern: var _ InterfaceName = ...
ALL_GO_FILES=()
while IFS= read -r f; do
[[ -n "$f" ]] && ALL_GO_FILES+=("$f")
done < <(find_all_go_files "$TARGET")
MISSING=()
for ((i=0; i<${#IFACE_NAMES[@]}; i++)); do
iface_name="${IFACE_NAMES[$i]}"
location="${IFACE_LOCATIONS[$i]}"
# Look for: var _ InterfaceName = (various patterns)
if ! grep -qlE "var[[:space:]]+_[[:space:]]+${iface_name}[[:space:]]*=" \
"${ALL_GO_FILES[@]}" 2>/dev/null; then
MISSING+=("${iface_name}|${location}")
fi
done
# Sort for stable output
IFS=$'\n' MISSING=($(sort <<<"${MISSING[*]}")); unset IFS
# Truncation
TOTAL=${#MISSING[@]}
TRUNCATED=false
if [[ $LIMIT -gt 0 && $TOTAL -gt $LIMIT ]]; then
MISSING=("${MISSING[@]:0:$LIMIT}")
TRUNCATED=true
fi
# Output results
if $JSON_OUTPUT; then
echo "{"
echo ' "interfaces": ['
first=true
SORTED_INDICES=()
for ((i=0; i<${#IFACE_NAMES[@]}; i++)); do
SORTED_INDICES+=("$i|${IFACE_NAMES[$i]}")
done
IFS=$'\n' SORTED_INDICES=($(sort -t'|' -k2 <<<"${SORTED_INDICES[*]}")); unset IFS
for entry in "${SORTED_INDICES[@]}"; do
i="${entry%%|*}"
iface_name="${IFACE_NAMES[$i]}"
location="${IFACE_LOCATIONS[$i]}"
file="${location%%:*}"
line="${location#*:}"
$first || echo ","
first=false
printf ' {"name":"%s","file":"%s","line":%s}' "$(json_escape "$iface_name")" "$(json_escape "$file")" "$line"
done
echo ""
echo " ],"
echo ' "missing": ['
first=true
for entry in "${MISSING[@]+"${MISSING[@]}"}"; do
IFS='|' read -r name location <<< "$entry"
file="${location%%:*}"
line="${location#*:}"
$first || echo ","
first=false
printf ' {"name":"%s","file":"%s","line":%s}' "$(json_escape "$name")" "$(json_escape "$file")" "$line"
done
echo ""
echo " ],"
printf ' "count_interfaces": %d,\n' "${#IFACE_NAMES[@]}"
printf ' "count_missing": %d,\n' "$TOTAL"
printf ' "truncated": %s\n' "$TRUNCATED"
echo "}"
else
echo "Exported interfaces found: ${#IFACE_NAMES[@]}"
echo ""
if [[ $TOTAL -eq 0 ]]; then
echo "All interfaces have compile-time compliance checks."
exit 0
fi
echo "Missing compile-time compliance checks:"
echo ""
for entry in "${MISSING[@]}"; do
IFS='|' read -r name location <<< "$entry"
printf " %s interface '%s' has no 'var _ %s = ...' assertion\n" "$location" "$name" "$name"
done
if $TRUNCATED; then
echo " ... and $((TOTAL - LIMIT)) more (use --limit to adjust)"
fi
echo ""
echo "Add compile-time checks like:"
echo " var _ MyInterface = (*MyImpl)(nil)"
echo ""
echo "Total: $TOTAL interface(s) missing verification"
fi
if [[ $TOTAL -gt 0 ]]; then
exit 1
fi
exit 0