第一章:Go语言封装的本质与哲学
Go语言的封装并非单纯语法层面的“访问控制”,而是一种以简洁性、可组合性和明确责任边界为核心的工程哲学。它拒绝传统面向对象语言中复杂的继承体系与隐式多态,转而通过结构体字段可见性(首字母大小写)与接口契约来定义模块边界——这种设计让封装成为一种显式约定,而非编译器强加的黑盒。
封装的可见性规则
Go中唯一决定封装粒度的机制是标识符的首字母大小写:
- 首字母大写(如
Name,ServeHTTP) → 导出(public),可被其他包访问; - 首字母小写(如
name,serveHTTP) → 非导出(private),仅限本包内使用。
该规则不依赖关键字(如 private/protected),也不受文件或目录层级影响,彻底消除了访问修饰符的语义歧义。
接口驱动的抽象封装
Go鼓励用小而专注的接口实现松耦合封装。例如:
// 定义行为契约,不暴露实现细节
type Reader interface {
Read(p []byte) (n int, err error) // 仅声明能力,不指定如何读
}
// 具体类型只需实现接口方法,即可被任何期望Reader的地方使用
type FileReader struct{ path string }
func (f FileReader) Read(p []byte) (int, error) {
// 实际读取逻辑对调用方完全隐藏
return os.ReadFile(f.path) // 简化示意,实际需适配Read签名
}
此模式使调用方只依赖“能做什么”,而非“是什么”,天然支持测试替身(mock)与运行时替换。
封装即文档
在Go中,导出标识符的命名本身承担着接口文档职责。清晰的命名(如 UnmarshalJSON, Close)直接传达行为意图;非导出字段则暗示其为内部状态,不应被外部依赖。这种“代码即文档”的实践降低了理解成本,也强化了重构安全性——修改私有字段不会破坏API兼容性。
| 封装维度 | Go实践 | 效果 |
|---|---|---|
| 作用域控制 | 包级可见性规则 | 消除跨包意外依赖 |
| 行为抽象 | 接口定义 + 隐式实现 | 解耦实现与使用方 |
| 演化保障 | 私有字段 + 导出方法 | 内部结构可自由重构 |
第二章:基于结构体字段控制的封装范式
2.1 字段可见性规则与首字母大小写的语义契约(理论)+ 标准库sync.Mutex字段设计逆向分析(实践)
Go 语言通过首字母大小写严格定义标识符的导出性(exported)与非导出性(unexported),构成隐式但强约束的“语义契约”:
- 首字母大写 → 导出 → 可被其他包访问;
- 首字母小写 → 非导出 → 仅限包内使用,实现封装边界。
数据同步机制
sync.Mutex 的字段设计是该契约的典范实践:
// src/sync/mutex.go(简化)
type Mutex struct {
state int32 // 非导出:内部状态位(mutexLocked/mutexWoken等)
sema uint32 // 非导出:信号量,由runtime.sem*函数直接操作
}
state和sema均为小写开头 → 禁止外部读写,确保锁状态只能通过Lock()/Unlock()方法原子变更;- 方法
Lock()是唯一合法入口,调用runtime_SemacquireMutex(&m.sema, ...), 依赖运行时深度协同。
可见性契约对照表
| 字段名 | 首字母 | 可见性 | 作用域限制 | 安全意义 |
|---|---|---|---|---|
state |
s |
包内私有 | sync 包内 |
防止竞态修改状态位 |
Lock |
L |
导出 | 全局可调用 | 提供受控同步入口 |
graph TD
A[Client Code] -->|调用| B[Mutex.Lock]
B --> C{检查 state}
C -->|未锁| D[设置 mutexLocked]
C -->|已锁| E[阻塞于 sema]
D & E --> F[runtime 协同调度]
2.2 匿名字段嵌入与组合式封装边界(理论)+ net/http.Header底层字段封装策略源码验证(实践)
Go 语言通过匿名字段实现“隐式继承”式的组合,但本质是编译期字段提升与方法代理,不引入类型继承关系。
Header 的封装设计哲学
net/http.Header 定义为 map[string][]string 的类型别名,未使用结构体嵌入,而是直接暴露底层 map——这打破了“封装即隐藏”的常规认知,实则是对高频读写场景的性能让渡。
// src/net/http/header.go
type Header map[string][]string
func (h Header) Get(key string) string {
if h == nil {
return ""
}
v := h[key]
if len(v) == 0 {
return ""
}
return v[0]
}
逻辑分析:
Get方法先做 nil 安全检查(因 Header 可为 nil),再取首个值。参数key区分大小写,但 HTTP 规范要求字段名不区分大小写——实际由调用方标准化(如http.CanonicalHeaderKey)。
封装边界的权衡矩阵
| 维度 | 选择 type Header map[…] |
若改用 struct{ header map[…] } |
|---|---|---|
| 内存开销 | 零额外字段 | +8~16 字节(map header 指针) |
| 方法调用性能 | 直接 map 访问 | 需解引用结构体字段 |
| 扩展性 | 无法添加私有状态 | 可内聚元数据(如修改标记、缓存) |
组合式封装的本质
Header 的设计印证了 Go 的核心信条:接口定义行为,组合表达关系,而封装粒度由场景契约决定。
2.3 struct tag驱动的运行时封装约束(理论)+ encoding/json.Marshaler接口与tag协同封装机制剖析(实践)
Go 语言中,struct tag 是编译期不可见、运行时可反射提取的元数据载体,其语义完全由使用者约定。encoding/json 包正是通过解析 json:"..." tag 控制字段序列化行为。
tag 的底层解析逻辑
反射调用 reflect.StructTag.Get("json") 返回原始字符串(如 "name,omitempty"),再经内部 parseTag 拆解为名称、选项(omitempty, string)等。
Marshaler 接口优先级高于 tag
当类型实现 json.Marshaler,json.Marshal 会跳过所有 tag 解析,直接调用 MarshalJSON() 方法:
type User struct {
Name string `json:"full_name"`
ID int `json:"user_id"`
}
func (u User) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]interface{}{
"uid": u.ID, // 忽略 tag 中的 "user_id"
"n": u.Name,
})
}
此处
User实例序列化结果为{"uid":123,"n":"Alice"}——jsontag 完全失效,体现接口优先级高于结构体元数据。
协同封装设计模式
| 场景 | 推荐策略 |
|---|---|
| 字段级格式控制 | 使用 json:"field,option" |
| 全字段逻辑重写 | 实现 MarshalJSON() |
| 混合策略(部分字段) | 在 MarshalJSON() 中手动调用 json.Marshal(&struct{...}) 并注入 tag 行为 |
graph TD
A[json.Marshal] --> B{Has Marshaler?}
B -->|Yes| C[Call MarshalJSON]
B -->|No| D[Parse struct tag → build encoder]
C --> E[Return custom bytes]
D --> F[Apply omitempty/string rules]
2.4 不可变结构体封装模式与内存安全保证(理论)+ time.Time不可变设计及其在标准库time包中的封装一致性验证(实践)
不可变性的核心价值
不可变结构体一旦创建,其字段无法被外部修改,天然规避竞态条件与意外突变,是 Go 内存安全的基石设计范式。
time.Time 的不可变契约
// src/time/time.go 中的核心定义(简化)
type Time struct {
wall uint64 // 墙钟时间位字段(含loc、sec等打包信息)
ext int64 // 扩展秒数(纳秒精度)
loc *Location // 指针,但只读访问
}
// 注意:所有字段均为小写(未导出),无公开 setter 方法
逻辑分析:wall 和 ext 通过位运算封装,loc 虽为指针但仅在构造时绑定;所有方法(如 Add, UTC)均返回新 Time 实例,不修改原值。参数 wall/ext 构成原子时间戳,确保跨 goroutine 安全读取。
标准库一致性验证要点
- 所有导出方法(
After,Before,Format)均不修改接收者 MarshalJSON/UnmarshalJSON生成新实例而非复用time.Now()每次调用返回独立副本
| 方法 | 是否修改原值 | 返回类型 |
|---|---|---|
t.Add(d) |
否 | Time |
t.In(loc) |
否 | Time |
t.Truncate(d) |
否 | Time |
graph TD
A[time.Now()] --> B[Time{wall,ext,loc}]
B --> C[t.Add(1h)]
B --> D[t.In(time.UTC)]
C --> E[New Time instance]
D --> F[New Time instance]
2.5 字段私有化与构造函数强制初始化范式(理论)+ strings.Builder零值不安全问题与NewBuilder封装实践(实践)
字段私有化与构造函数强制初始化
Go 中无 private 关键字,但首字母小写字段天然包级私有。强制初始化要求结构体无导出零值可用:
type Config struct {
endpoint string // 私有字段,无法外部直接赋值
timeout time.Duration
}
func NewConfig(endpoint string, timeout time.Duration) *Config {
if endpoint == "" {
panic("endpoint required")
}
return &Config{endpoint: endpoint, timeout: timeout}
}
逻辑分析:
NewConfig是唯一构造入口,拦截空 endpoint;endpoint字段不可导出,杜绝&Config{endpoint: ""}绕过校验。参数endpoint为必填业务标识,timeout提供默认容错空间。
strings.Builder 的零值陷阱与 NewBuilder 封装
strings.Builder{} 零值虽可调用 Write(),但若误用 Reset() 后未重新 Grow(),可能触发 panic。
| 场景 | 零值 Builder 行为 | 封装后 NewBuilder 行为 |
|---|---|---|
| 初始 Write | ✅ 安全 | ✅ 自动预分配 64B |
| Reset 后立即 Write | ❌ 可能 panic(cap=0) | ✅ Reset 后自动 Grow(64) |
func NewBuilder() *strings.Builder {
b := &strings.Builder{}
b.Grow(64)
return b
}
逻辑分析:
Grow(64)确保底层[]byte至少有 64 字节容量,消除Reset()后首次写入的扩容竞态风险;参数无,体现“零配置即安全”设计契约。
graph TD
A[NewBuilder] --> B[Builder{}]
B --> C[Grow 64]
C --> D[返回 ready-to-use 实例]
第三章:基于接口抽象的封装范式
3.1 接口即契约:隐式实现与封装边界的动态划定(理论)+ io.Reader/io.Writer在net.Conn中的封装解耦实证(实践)
Go 中接口不声明实现,仅定义行为契约——io.Reader 与 io.Writer 即典型范例。net.Conn 同时隐式满足二者,却不对底层 TCP 状态暴露任何字段:
type Conn interface {
Reader
Writer
// ...其他方法(Close, LocalAddr等)
}
该设计使 http.Transport 可直接复用 Conn 实例,无需感知其是否为 TLS、Unix Domain Socket 或 mock 连接。
封装边界如何动态划定?
- 调用方只依赖
Read(p []byte) (n int, err error)抽象签名 - 实现方自由选择缓冲策略(如
bufio.ReadWriter包装)、错误注入点或零拷贝路径 - 边界由编译期类型检查自动确立,无显式继承声明
实证对比:不同 Conn 的 Reader 行为一致性
| 实现类型 | Read 阻塞语义 | 错误传播方式 | 是否支持 io.Copy |
|---|---|---|---|
tcp.Conn |
按网络包到达 | io.EOF / net.OpError |
✅ |
tls.Conn |
加密层解包后 | 封装底层错误 | ✅ |
mockConn (test) |
可控字节流 | 自定义 error | ✅ |
graph TD
A[http.Server] -->|calls Read/Write| B[net.Conn]
B --> C[tcp.Conn]
B --> D[tls.Conn]
B --> E[mockConn]
C & D & E --> F[io.Reader/Writer 契约]
3.2 接口组合与封装层次演进(理论)+ context.Context接口组合在database/sql驱动层的封装收敛分析(实践)
Go 语言中,接口组合是构建可扩展抽象的核心机制——小而专的接口(如 io.Reader、io.Writer)通过嵌入形成高阶契约,避免“大接口”导致的实现负担。
context.Context 的组合定位
context.Context 本身不实现任何业务逻辑,而是作为跨层控制信号载体,被自然嵌入到 driver.Conn、driver.Stmt 等驱动接口方法签名中:
// driver/driver.go 片段
type Conn interface {
Prepare(query string) (Stmt, error)
Close() error
Begin() (Tx, error)
}
// database/sql 实际调用时注入 context(非 driver 接口原生定义,而是 sql 包封装层注入)
func (db *DB) QueryContext(ctx context.Context, query string, args ...any) (*Rows, error) {
// …… 获取 conn 后调用:conn.QueryContext(ctx, query, args...)
}
该设计体现封装收敛:database/sql 包统一将 context.Context 注入各驱动操作,驱动实现无需感知 context,但上层获得超时、取消等能力。
封装层次演进路径
- 基础层:
driver接口无 context(正交、稳定) - 中间层:
database/sql定义QueryContext/ExecContext等方法,桥接 context 与 driver - 应用层:业务代码直接使用带 context 的 API,零侵入适配
| 层级 | 责任 | context 可见性 |
|---|---|---|
driver |
数据库协议实现 | ❌ 不感知 |
database/sql |
连接池、重试、context 注入 | ✅ 封装枢纽 |
| 应用代码 | 业务逻辑 | ✅ 直接传递 |
graph TD
A[App: db.QueryContext(ctx, ...)] --> B[database/sql: 封装 ctx 并调度]
B --> C[driver.Conn.QueryContext?]
C -.-> D[实际由 sql 包包装 driver.Conn.Query 调用]
D --> E[底层 driver 实现无 context]
3.3 接口类型断言与封装透明性权衡(理论)+ reflect.Value.Interface()封装泄漏风险与标准库fmt包防护机制(实践)
类型断言的封装边界
Go 中 interface{} 是类型擦除的载体,类型断言 x.(T) 在运行时暴露底层具体类型——这既是灵活性来源,也是封装泄漏的起点。
reflect.Value.Interface() 的隐式解包风险
type secret struct{ token string }
func (s secret) String() string { return "[redacted]" }
v := reflect.ValueOf(secret{"abc123"})
raw := v.Interface() // ⚠️ 返回 concrete type secret,非 interface{}
fmt.Printf("%v\n", raw) // 输出:{abc123} —— 绕过 String() 方法!
v.Interface()强制还原为原始具体类型,跳过接口方法集约束;- 若该类型实现了
Stringer,但fmt未调用(因传入的是secret而非fmt.Stringer接口),则String()不被触发; - 标准库
fmt内部对reflect.Value有特殊处理:当值来自reflect且未导出字段时,fmt主动拒绝格式化敏感字段(见fmt/print.go中handleMethods分支)。
fmt 的防护策略对比
| 场景 | 输入值类型 | 是否调用 String() |
原因 |
|---|---|---|---|
fmt.Printf("%v", secret{}) |
concrete secret |
✅ | 满足 fmt.Stringer 接口 |
fmt.Printf("%v", v.Interface()) |
same, but via reflect |
❌ | fmt 检测到非接口直接值,降级为结构体打印 |
fmt.Printf("%v", interface{}(secret{})) |
interface{} holding secret |
✅ | 接口方法表可查 |
graph TD
A[reflect.Value] -->|Interface()| B[Concrete Type]
B --> C{fmt.Printf}
C -->|值是导出结构体| D[字段直显 → 泄漏]
C -->|值实现Stringer且为接口态| E[调用String → 安全]
第四章:基于方法集与接收者语义的封装范式
4.1 值接收者与指针接收者对封装粒度的决定性影响(理论)+ sort.Slice与sort.SliceStable中接收者选择对封装行为的差异验证(实践)
Go 中接收者类型直接决定方法能否修改原始状态、是否触发值拷贝,进而锚定封装边界——值接收者天然隔离内部状态,指针接收者则暴露可变契约。
封装粒度的本质差异
- 值接收者:每次调用复制整个结构体,不可修改原值,适合只读、无副作用操作
- 指针接收者:共享底层内存,可变更字段,但要求调用方提供地址,隐含“可变”契约
sort.Slice 的接收者语义验证
type Person struct{ Name string; Age int }
people := []Person{{"Alice", 30}, {"Bob", 25}}
// sort.Slice 接收 func(i, j int) bool —— 无接收者,仅依赖闭包捕获切片引用
sort.Slice(people, func(i, j int) bool {
return people[i].Age < people[j].Age // ✅ 直接修改原切片元素(底层数组可变)
})
此处
sort.Slice不声明接收者,而是通过函数参数间接获得切片引用;其内部逻辑按索引直接重排*[]T底层,因此能稳定修改原数据。而sort.SliceStable行为一致,二者封装粒度相同——均不约束调用方接收者类型,仅依赖切片可寻址性。
| 特性 | sort.Slice | sort.SliceStable |
|---|---|---|
| 稳定性 | 否 | 是 |
| 接收者类型约束 | 无(纯函数式) | 无(纯函数式) |
| 封装暴露面 | 切片地址 + 比较逻辑 | 同左 |
graph TD
A[调用 sort.Slice] --> B[传入切片引用]
B --> C[内部按索引交换底层数组元素]
C --> D[原切片内容被就地修改]
4.2 方法集封闭性与外部不可扩展原则(理论)+ sync.Pool.Put/Get方法集设计如何阻断非法状态篡改(实践)
方法集封闭性的本质
Go 语言中,sync.Pool 的公开方法仅限 Get() 和 Put(x interface{}),无 Reset()、Clear() 或字段直访接口。这种显式方法集裁剪强制所有状态变更经由可控路径,杜绝外部绕过内部状态机逻辑的篡改。
Put/Get 如何协同防御非法状态
Put(x)要求x必须为池中曾Get()出或零值对象(运行时不校验类型,但语义契约约束)Get()总返回“已初始化”或“已归零”对象,绝不暴露半初始化中间态
// sync.Pool 内部状态流转示意(简化)
var p = &sync.Pool{
New: func() interface{} { return &bytes.Buffer{} },
}
buf := p.Get().(*bytes.Buffer) // 获取已清空的 Buffer
buf.WriteString("hello") // 使用
p.Put(buf) // 归还:Pool 内部自动调用 buf.Reset()
逻辑分析:
Put并非简单入队,而是触发poolLocal.private或shared队列存储前的隐式归零(若对象实现Reset()则调用;否则依赖New构造函数保证初始态)。参数x若为非法类型(如nil或未归还过的自定义对象),虽不 panic,但破坏复用契约,导致后续Get()返回脏数据——这正体现了封闭方法集对非法输入的被动过滤能力。
状态机安全边界对比
| 操作 | 是否可绕过 Pool 状态管理 | 是否触发归零逻辑 |
|---|---|---|
p.Put(buf) |
否(唯一入口) | 是(内部保障) |
buf.Reset() |
是(直接调用) | 是(但脱离 Pool 控制) |
unsafe.Pointer 强转 |
是(完全越权) | 否(彻底失控) |
graph TD
A[Client calls p.Get()] --> B{Pool has ready object?}
B -->|Yes| C[Return zeroed object]
B -->|No| D[Call New() constructor]
C --> E[Use object]
E --> F[p.Put(obj)]
F --> G[Enqueue + implicit reset]
G --> B
4.3 封装内聚性:方法职责单一与状态变更原子性保障(理论)+ bytes.Buffer.Write方法如何封装缓冲区扩容与状态同步逻辑(实践)
方法职责单一性与状态原子性的内在统一
一个高内聚方法应仅承担一项语义明确的职责,同时确保其引发的所有状态变更(如 buf.len、buf.cap、buf.buf 指针)在执行中不可分割——即满足原子性。二者共同构成封装的坚实边界。
数据同步机制
bytes.Buffer.Write 将「写入数据」、「容量检查」、「底层数组扩容」、「长度更新」四步逻辑密闭于单次调用中:
func (b *Buffer) Write(p []byte) (n int, err error) {
if b.buf == nil {
b.reset() // 初始化空缓冲区
}
if len(p) == 0 {
return
}
if b.offs > 0 { // 处理未清空的前置偏移(如Read后Write)
b.grow(len(p)) // 原子扩容并移动有效数据
}
b.buf = append(b.buf[:b.len], p...) // 安全追加,隐式更新len
n = len(p)
return
}
逻辑分析:
append调用前b.buf[:b.len]精确截取当前有效字节;append内部自动处理容量不足时的make+copy,并返回新切片——b.len随之被len(b.buf)反向同步,避免手动维护导致的不一致。参数p []byte为只读输入,不修改其底层数组。
扩容策略对比
| 场景 | 是否触发扩容 | 状态同步方式 |
|---|---|---|
len(p) ≤ cap-b.len |
否 | 仅 b.len += len(p) |
len(p) > cap-b.len |
是 | append 重分配 + 全量拷贝 |
graph TD
A[Write(p)] --> B{len(p) ≤ remaining cap?}
B -->|Yes| C[直接append,len原子递增]
B -->|No| D[调用grow→扩容+copy→append]
D --> E[len/cap/buf三者同步更新]
4.4 接收者类型与nil安全封装契约(理论)+ errors.Is/errors.As在nil receiver场景下的封装鲁棒性源码验证(实践)
nil receiver并非语法错误,而是契约设计的分水岭
Go 中方法可被 nil 指针调用——前提是该方法不解引用 receiver。这是实现“零值可用”接口的关键前提。
errors.Is/errors.As 的 nil-resilient 设计
标准库 errors.Is 和 errors.As 显式允许 err == nil 输入,并返回 false 而非 panic:
// 源码精简示意($GOROOT/src/errors/wrap.go)
func Is(err, target error) bool {
if err == nil || target == nil {
return err == target // nil == nil → true;nil == non-nil → false
}
// ……后续链式展开逻辑
}
✅
Is(nil, nil)→true;Is(nil, io.EOF)→false;无 panic。
✅As(nil, &e)→ 返回false,且e不被写入(*e不解引用)。
封装鲁棒性对比表
| 场景 | errors.Is(err, target) | errors.As(err, &v) | 是否 panic |
|---|---|---|---|
err = nil |
false |
false |
❌ |
err = &MyErr{} |
依实现 | 若 v 非 nil 则尝试赋值 |
❌(若 MyErr.As 支持) |
err = (*MyErr)(nil) |
false(跳过 As 分支) |
false(不触达 v 解引用) |
❌ |
核心契约本质
nil 安全 ≠ 忽略 nil,而是将 nil 视为合法状态参与逻辑分支——这要求所有中间封装层(如自定义
Unwrap()、As())均遵循if err == nil { return ... }前置守卫。
第五章:封装范式的统一性、局限性与演进趋势
封装在主流语言中的共性实践
Java、C#、Rust 和 TypeScript 均通过访问修饰符(private/#/pub(crate))或模块边界实现数据隐藏。例如,Rust 的 struct 默认私有字段配合 impl 块提供受控接口:
pub struct BankAccount {
balance: f64,
}
impl BankAccount {
pub fn new(initial: f64) -> Self {
assert!(initial >= 0.0);
Self { balance: initial }
}
pub fn deposit(&mut self, amount: f64) {
self.balance += amount.max(0.0);
}
}
该模式屏蔽了余额直接赋值风险,强制走校验逻辑——这是跨语言封装的底层一致性体现。
微服务架构下的封装边界迁移
单体应用中“类级封装”让位于“服务级封装”。某电商系统将库存服务拆出后,原 InventoryManager.updateStock() 方法被替换为 HTTP 调用:
| 场景 | 封装粒度 | 暴露方式 | 安全保障 |
|---|---|---|---|
| 单体 Java 类 | 字段+方法 | public 方法 |
JVM 访问控制 |
| 库存微服务 | REST API | /v1/stock/reserve |
OAuth2 + 限流 + Schema 校验 |
此时,balance 不再是内存字段,而是数据库事务+幂等令牌+分布式锁协同维护的状态,封装从语法层跃迁至协议与治理层。
动态语言中封装的脆弱性实证
Python 的 _protected 和 __private 命名约定在实际运维中频繁被绕过。某金融风控平台曾因日志调试代码直接访问 __risk_score 导致缓存失效:
# 生产环境误用(真实事故日志片段)
logger.info(f"DEBUG: raw score = {user._User__risk_score}") # 触发 __getattribute__ 链路污染
该操作使 LRU 缓存键计算异常,引发下游 37% 的评分请求超时。最终通过 Pydantic v2 的 @computed_field + @field_validator 强制声明式约束才根治。
WASM 模块封装的新范式
Rust 编译为 WASM 后,导出函数自动成为唯一入口点,内存完全隔离:
flowchart LR
A[JS 主线程] -->|调用| B[WASM 实例]
B --> C[线性内存页]
C --> D[只读数据段]
C --> E[可写堆区]
D -.->|不可修改| F[预编译算法表]
E -->|仅通过 export 函数访问| G[动态状态]
某图像处理 SaaS 将 OpenCV 算法封装为 WASM 模块,前端无法越界读取原始像素缓冲区,规避了传统 WebAssembly 内存泄漏攻击面。
类型即契约:TypeScript 的编译期封装强化
通过 declare module 隐藏第三方库内部类型,配合 export type 显式暴露契约:
// @types/my-sdk/index.d.ts
declare module 'my-sdk' {
export class Client {
private constructor(); // 禁止 new
static create(config: Config): Client;
send<T>(req: Request): Promise<Response<T>>;
}
export type Config = { endpoint: string; timeoutMs: number };
}
该设计使 SDK 升级时内部重构不影响用户代码,npm 发布时仅需更新 .d.ts 文件即可完成封装契约演进。
封装已从保护字段演变为定义可信交互契约,其本质正从“阻止访问”转向“引导正确使用”。
