第一章:Go基础组件概览与测试困境本质
Go语言的核心优势在于其简洁的运行时、内置并发模型(goroutine + channel)以及标准化的工具链,包括go build、go run、go test和go mod。这些组件共同构成轻量但强约束的开发闭环——没有虚拟机,无运行时反射依赖,编译产物为静态链接的单二进制文件。
然而,正是这种“简洁性”放大了测试阶段的结构性挑战:
- 标准库对I/O、时间、随机数等外部依赖高度封装,导致纯函数式隔离测试困难;
init()函数隐式执行、包级变量全局可变,使测试间状态污染难以规避;go test默认并行执行(-p=runtime.NumCPU()),而未加同步的包级变量或临时文件操作极易引发竞态。
例如,以下代码在并发测试中会非确定性失败:
// counter.go
var count int // 包级变量,无同步保护
func Inc() { count++ } // 非原子操作
func Get() int { return count }
若直接运行 go test -race,会立即报告写-写竞态。修复需显式同步:
// 修正方案:使用sync.Mutex或sync/atomic
import "sync"
var mu sync.Mutex
var count int
func Inc() {
mu.Lock()
count++
mu.Unlock()
}
常见测试困境类型如下表所示:
| 困境类别 | 典型诱因 | 推荐缓解方式 |
|---|---|---|
| 状态污染 | 包级变量、全局配置修改 | TestMain中重置+defer恢复 |
| 时间依赖 | time.Now()、time.Sleep() |
使用github.com/benbjohnson/clock等可注入时钟接口 |
| 文件系统干扰 | os.CreateTemp未清理 |
统一使用t.TempDir()并确保defer os.RemoveAll() |
真正的测试困境不在于语法复杂度,而源于Go对“显式优于隐式”的哲学坚持——它拒绝为测试提供魔法钩子,要求开发者将依赖抽象为接口,并通过构造函数或选项模式注入,从而让测试边界清晰可塑。
第二章:httptest.Server深度剖析与典型误用场景
2.1 httptest.Server的生命周期管理与goroutine泄漏风险
httptest.Server 是 Go 测试 HTTP 服务的核心工具,但其生命周期需手动管理——启动后不会自动关闭,易引发 goroutine 泄漏。
启动与显式关闭模式
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
}))
defer srv.Close() // ✅ 必须显式调用
srv.Close() 终止监听、关闭 listener,并等待所有活跃连接完成;若遗漏,底层 http.Server 的 Serve() goroutine 持续运行,且 srv.URL 对应的监听端口仍被占用。
常见泄漏场景对比
| 场景 | 是否触发泄漏 | 原因 |
|---|---|---|
defer srv.Close() 在测试函数末尾 |
否 | 正常清理 |
srv.Close() 被 panic 跳过 |
是 | defer 未执行 |
多次 srv.Close() |
否(幂等) | 内部通过 sync.Once 保障 |
goroutine 状态流转(关键路径)
graph TD
A[NewServer] --> B[启动 goroutine 执行 Serve]
B --> C{是否调用 Close?}
C -->|是| D[关闭 listener + WaitGroup.Done]
C -->|否| E[goroutine 永驻内存]
2.2 基于httptest.Server的端到端测试边界识别与mock粒度控制
httptest.Server 是 Go 标准库中模拟 HTTP 服务的核心工具,其本质是启动一个真实监听的本地服务器,但完全运行在内存中,无网络开销。
测试边界判定原则
- ✅ 应 mock:外部依赖(数据库、第三方 API、消息队列)
- ⚠️ 慎 mock:本服务内部 HTTP handler 链路(如
/api/v1/users→/api/v1/profiles) - ❌ 不可 mock:被测 handler 自身逻辑与路由注册行为
Mock 粒度对照表
| 粒度层级 | 示例 | 适用场景 |
|---|---|---|
| 全服务替换 | httptest.NewServer(mux) |
端到端集成验证 |
| Handler 替换 | httptest.NewUnstartedServer(handler) |
控制中间件/错误注入 |
| 依赖注入替换 | server := &Server{DB: mockDB} |
单 handler 单元隔离 |
// 启动仅含核心 handler 的轻量 server,跳过日志/认证中间件
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
})
server := httptest.NewUnstartedServer(handler)
server.Start() // 延迟启动,便于注入依赖
defer server.Close()
该写法绕过 NewServer 的自动启动与默认中间件栈,使测试精准锚定 handler 行为本身;NewUnstartedServer 返回可修改的 *httptest.Server 实例,支持在 Start() 前替换 Handler 或注入 Client 超时等参数。
2.3 TLS配置缺失导致的本地集成测试证书验证失败实战复现
现象复现
本地运行 Spring Boot 集成测试时,RestTemplate 调用 HTTPS 接口报错:
javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
根本原因
JVM 默认信任系统/Java 信任库(cacerts)中的 CA 证书,而本地自签名服务端证书未导入。
快速验证脚本
# 检查目标服务证书(假设运行在 localhost:8443)
openssl s_client -connect localhost:8443 -servername localhost 2>/dev/null | openssl x509 -noout -subject -issuer
逻辑分析:
s_client建立 TLS 握手并输出证书链;x509 -noout提取关键字段。若返回subject=CN=localhost且issuer=CN=localhost,即为自签名证书——JVM 默认拒绝信任。
修复方案对比
| 方案 | 安全性 | 适用场景 | 持久性 |
|---|---|---|---|
导入证书到 $JAVA_HOME/jre/lib/security/cacerts |
✅ 高 | 生产预置、CI 环境 | ⚙️ 需权限,影响全局 JVM |
| 测试代码中禁用 SSL 验证(仅限开发) | ❌ 低 | 本地快速验证 | 📦 仅限 @Test 类生效 |
推荐测试级临时绕过(含注释)
// 创建信任所有证书的 SSLContext(⚠️ 仅测试使用!)
SSLContext sslContext = SSLContextBuilder.create()
.loadTrustMaterial((chain, authType) -> true) // 总是信任
.build();
CloseableHttpClient client = HttpClients.custom()
.setSSLContext(sslContext)
.setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE) // 关闭域名匹配
.build();
参数说明:
loadTrustMaterial(...)替代默认信任管理器;NoopHostnameVerifier跳过CN/SAN域名校验——二者缺一不可,否则仍会触发SSLHandshakeException。
2.4 httptest.Server与真实HTTP/2握手不兼容引发的协议协商失败案例
httptest.Server 默认仅支持 HTTP/1.1,不启动 ALPN 协商,而真实客户端(如 curl -k --http2 或 Go http.Client 启用 HTTP/2)会发送 TLS 扩展中的 ALPN: h2。服务端若未响应匹配协议,连接将被重置。
核心差异对比
| 特性 | httptest.Server |
真实 HTTP/2 服务(如 net/http.Server + TLS) |
|---|---|---|
| ALPN 支持 | ❌ 不实现 TLS 层 | ✅ 响应 h2 或 http/1.1 |
SETTINGS 帧交换 |
❌ 无 HTTP/2 帧处理逻辑 | ✅ 握手后立即发送初始 SETTINGS |
PRI * HTTP/2.0\r\n 预检 |
❌ 不识别该伪请求 | ✅ 明确拒绝或跳过(依赖 TLS ALPN 结果) |
复现代码片段
// 错误示范:期望 HTTP/2 但使用纯内存 server
srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
}))
srv.StartTLS() // 仍为 HTTP/1.1 over TLS —— 无 ALPN!
client := &http.Client{Transport: &http.Transport{
TLSClientConfig: &tls.Config{NextProtos: []string{"h2"}}, // 客户端主动协商 h2
}}
_, err := client.Get(srv.URL) // panic: http: server closed idle connection
逻辑分析:
httptest.Server.StartTLS()仅包裹http.Server并启用 TLS 加密,但不注入http2.ConfigureServer,故srv.TLSConfig.NextProtos为空,TLS 层无法通告h2,导致客户端 ALPN 协商失败,后续帧解析中断。
协议协商失败路径(mermaid)
graph TD
A[Client: TLS ClientHello with ALPN=h2] --> B{httptest.Server TLS layer}
B -->|无 NextProtos 配置| C[Server returns ALPN=empty]
C --> D[Client aborts handshake]
D --> E[Connection closed before HTTP/2 frame exchange]
2.5 多实例并发启动时端口竞争与资源隔离失效的调试实践
当多个微服务实例并行启动,常因未配置动态端口或命名空间隔离,触发 Address already in use 异常。
根本原因定位
- 启动脚本硬编码
server.port=8080 - Docker Compose 默认共享
bridge网络,未启用network_mode: "container:..."或--isolation=process(Windows WSL2) - Kubernetes Pod 未设置
hostPort冲突防护或podAntiAffinity
动态端口分配示例(Spring Boot)
# application.yml
server:
port: ${PORT:0} # 0 表示随机空闲端口
management:
server:
port: 0 # 管理端点也随机化
port: 0触发内核级端口探测,Spring Boot 启动后通过local.server.port曝光实际绑定值,避免竞态;若设为固定值(如8080),多实例在毫秒级启动窗口内必然冲突。
验证工具链对比
| 工具 | 检测维度 | 实时性 | 是否需 root |
|---|---|---|---|
lsof -i :8080 |
进程级端口占用 | 高 | 是 |
ss -tuln \| grep :8080 |
套接字状态 | 极高 | 否 |
netstat -an \| findstr :8080 |
Windows 兼容 | 中 | 否 |
资源隔离修复路径
graph TD
A[并发启动请求] --> B{是否启用端口发现}
B -->|否| C[端口冲突]
B -->|是| D[分配唯一port+namespace]
D --> E[写入etcd/Consul注册中心]
E --> F[健康探针校验隔离有效性]
第三章:net/http/httputil.ReverseProxy核心机制解构
3.1 ReverseProxy请求转发链路中的上下文传递断裂与超时继承失效
当 http.ReverseProxy 转发请求时,若未显式注入父 context.Context,下游服务将丢失上游的 Deadline 与取消信号。
上下文断裂典型场景
ReverseProxy.Transport默认使用http.DefaultTransport,其RoundTrip不继承传入req.Context()- 中间件中
r = r.WithContext(newCtx)后未同步更新req.Header中的超时元信息
超时继承失效验证
proxy := httputil.NewSingleHostReverseProxy(u)
proxy.Transport = &http.Transport{
// ❌ 缺失 context 感知能力
}
该配置导致:req.Context().Done() 不触发底层连接中断;Timeout 字段不透传至 http.Transport.DialContext。
| 环节 | 是否继承 Deadline | 原因 |
|---|---|---|
ReverseProxy.ServeHTTP |
否 | req.Clone(ctx) 未被调用 |
Transport.RoundTrip |
否 | 默认 DialContext 未绑定原始 ctx |
修复路径(关键代码)
proxy.Director = func(req *http.Request) {
// ✅ 显式注入上下文并保留超时
req = req.Clone(req.Context()) // 继承 cancel/timeout
req.Header.Set("X-Forwarded-For", req.RemoteAddr)
}
req.Clone() 是上下文透传的唯一安全方式——它复制 Context、URL、Header 等全部语义字段,避免浅拷贝引发的竞态。
3.2 Director函数中Host头篡改引发的后端服务路由错乱实测分析
Director 函数在边缘网关中常用于动态重写请求头,但不当的 Host 头覆写会直接干扰后端基于域名的虚拟主机路由(如 Nginx 的 server_name 或 Envoy 的 virtual_host 匹配)。
复现关键代码片段
// Director.js 中存在隐患逻辑
export function director(req) {
req.headers.set('Host', 'api.internal'); // ⚠️ 硬编码覆盖,未校验来源域
return req;
}
该代码强制将所有请求 Host 改为 api.internal,导致原本发往 admin.example.com 的管理流量被后端误判为内部 API 流量,触发错误路由分发。
影响路径示意
graph TD
A[Client: Host: admin.example.com] --> B[Director函数]
B --> C[Host → 'api.internal']
C --> D[Nginx server_name匹配失败]
D --> E[回退至 default_server 或 404]
实测对比表
| 请求原始 Host | Director 后 Host | 后端实际路由目标 | 结果 |
|---|---|---|---|
admin.example.com |
api.internal |
/api/v1/... |
权限拒绝 |
web.example.com |
api.internal |
/api/v1/... |
接口不存在 |
3.3 Transport配置缺失导致的连接池复用冲突与DNS缓存污染问题
当HTTP客户端(如OkHttp、Apache HttpClient)未显式配置Transport层参数时,底层连接池默认启用长连接复用,而DNS解析结果被无期限缓存于JVM级InetAddress缓存中。
DNS缓存污染表现
- JVM默认永久缓存正向解析结果(TTL=0 → 永不过期)
- 服务端IP变更后,客户端持续复用旧地址,引发
Connection refused或5xx错误
连接池复用冲突根源
// ❌ 危险配置:未禁用DNS缓存且未定制ConnectionPool
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.build(); // 默认复用connection pool + 永久DNS缓存
此配置下,
Dns.SYSTEM被直接使用,且Address对象复用导致InetSocketAddress绑定过期IP;ConnectionPool对同一Host+Port复用RealConnection,无视DNS刷新。
推荐加固方案
| 配置项 | 推荐值 | 说明 |
|---|---|---|
address.dns |
自定义Dns实现 |
支持TTL感知解析 |
connectionPool |
设置maxIdleConnections=5, keepAliveDuration=5, MINUTES |
避免长连接僵死 |
system property |
-Dnetworkaddress.cache.ttl=30 |
限制JVM DNS缓存生命周期 |
graph TD
A[发起HTTP请求] --> B{Transport层是否配置Dns/Pool?}
B -->|否| C[使用Dns.SYSTEM + 全局ConnectionPool]
C --> D[复用含过期IP的RealConnection]
D --> E[连接失败或路由错位]
B -->|是| F[按TTL刷新DNS + 连接空闲驱逐]
第四章:httptest.Server与ReverseProxy协同测试模式构建
4.1 构建可插拔式测试代理中间件:拦截/修改/断言HTTP流量
核心设计原则
- 职责分离:拦截(Intercept)、修改(Rewrite)、断言(Assert)三阶段解耦,支持动态注册插件
- 非侵入式集成:基于标准 HTTP 代理协议,兼容 curl、Postman、前端 fetch 等任意客户端
请求处理流水线
def handle_request(flow: http.HTTPFlow):
# 拦截:匹配规则并触发插件链
for plugin in plugins.match(flow.request.url, "request"):
plugin.on_request(flow) # 可修改 flow.request.headers / .content
# 断言:仅在测试模式下执行(env == "test")
if os.getenv("TEST_MODE"):
assert_status_code(flow, 200)
assert_json_schema(flow.response.content, "user_v1.json")
逻辑说明:
flow是 mitmproxy 的核心数据结构;plugins.match()基于 URL 模式与请求方法路由;on_request()允许同步篡改请求体,如注入X-Test-ID头用于追踪;断言模块在响应返回前校验,失败则抛出AssertionError并记录完整 flow 快照。
插件能力矩阵
| 能力 | 拦截 | 修改 | 断言 | 示例用途 |
|---|---|---|---|---|
| Header 操作 | ✅ | ✅ | ❌ | 注入调试头、移除认证令牌 |
| Body 重写 | ✅ | ✅ | ❌ | 模拟错误响应体 |
| 响应 Schema 验证 | ❌ | ❌ | ✅ | 确保 API 返回符合 OpenAPI 定义 |
graph TD
A[Client Request] --> B{Proxy Middleware}
B --> C[Interceptor Chain]
C --> D[Rewrite Plugins]
C --> E[Assert Plugins]
D --> F[Upstream Server]
F --> G[Response Flow]
G --> E
E --> H[Pass/Fail Report]
4.2 模拟真实网关链路:在测试中复现X-Forwarded-*头注入异常
现代微服务架构中,API网关常自动添加 X-Forwarded-For、X-Forwarded-Proto 等头字段。若下游服务盲目信任这些头,将导致身份伪造、协议降级等安全风险。
复现场景构造
使用 curl 模拟恶意客户端与网关共存的双重转发:
# 模拟攻击者在请求中注入伪造头,经网关二次添加后形成歧义
curl -H "X-Forwarded-For: 192.168.1.100, 10.0.0.5" \
-H "X-Forwarded-Proto: http" \
http://localhost:8080/api/user
逻辑分析:网关默认追加自身IP至
X-Forwarded-For(如变为192.168.1.100, 10.0.0.5, 172.20.1.20),但业务代码若取首个IP(192.168.1.100)做访问控制,则绕过内网校验。参数X-Forwarded-Proto被篡改为http可触发HTTPS重定向失效。
常见解析策略对比
| 策略 | 安全性 | 适用场景 |
|---|---|---|
| 取第一个IP | ⚠️ 低 | 无可信边界网关 |
| 取倒数第二个IP | ✅ 中 | 单层网关部署 |
| 白名单校验末N段 | ✅✅ 高 | 多级网关+IP白名单 |
graph TD
A[客户端] -->|XFF: 1.1.1.1| B[边缘网关]
B -->|XFF: 1.1.1.1, 10.10.10.10| C[内部网关]
C -->|XFF: 1.1.1.1, 10.10.10.10, 192.168.5.5| D[业务服务]
4.3 跨组件状态一致性保障:共享TestContext与自定义RoundTripper联动
在集成测试中,多个测试组件(如 HTTP 客户端、服务模拟器、断言引擎)需共享同一测试生命周期上下文,避免状态漂移。
数据同步机制
TestContext 作为不可变快照容器,通过 sync.Once 初始化并全局复用:
var testCtx *TestContext
var once sync.Once
func GetTestContext() *TestContext {
once.Do(func() {
testCtx = &TestContext{
RequestID: uuid.New().String(), // 全局唯一追踪标识
StartTime: time.Now(),
Flags: map[string]bool{"enable-mock": true},
}
})
return testCtx
}
此设计确保所有组件读取同一
RequestID和Flags,为日志关联与行为开关提供一致依据。
RoundTripper 协同策略
自定义 RoundTripper 注入 TestContext 元数据到请求头:
| Header Key | Value Source | 用途 |
|---|---|---|
X-Test-Request-ID |
testCtx.RequestID |
请求链路追踪 |
X-Test-Mode |
testCtx.Flags["enable-mock"] |
动态启用 mock 响应逻辑 |
graph TD
A[HTTP Client] -->|Inject ctx| B[Custom RoundTripper]
B -->|Add headers| C[Mock Server / Real API]
C -->|Response + ctx-aware logging| D[Test Assertion Engine]
4.4 基于httptest.NewUnstartedServer实现ReverseProxy零端口绑定测试
httptest.NewUnstartedServer 是 net/http/httptest 中的关键工具,它创建一个未启动的 HTTP 服务实例,避免端口占用与竞争条件,特别适合 http.ReverseProxy 的集成测试。
为何需要“零端口绑定”?
- 避免测试间端口冲突(如
:8080被占用) - 支持并发运行多个隔离代理测试
- 完全绕过
ListenAndServe,由测试逻辑精确控制生命周期
核心代码示例
srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
w.Write([]byte("backend ok"))
}))
srv.Start() // 显式启动 → 获取真实监听地址 srv.URL
proxy := httputil.NewSingleHostReverseProxy(mustParseURL(srv.URL))
逻辑分析:
NewUnstartedServer返回*httptest.Server,其Handler被包装为内部http.Server;调用Start()后自动绑定随机空闲端口(如127.0.0.1:34921),srv.URL即可安全注入ReverseProxy。参数srv.URL是唯一必需的后端地址源,确保代理目标完全可控。
| 特性 | 传统 httptest.Server | NewUnstartedServer |
|---|---|---|
| 端口分配时机 | 构造即绑定 | Start() 时动态分配 |
| 并发安全性 | 低(需手动管理端口) | 高(每个实例独立端口) |
| 生命周期控制 | 弱(自动启停) | 强(可多次 Start/Close) |
graph TD
A[NewUnstartedServer] --> B[Handler 封装]
B --> C[Start: 绑定随机端口]
C --> D[生成 srv.URL]
D --> E[注入 ReverseProxy Director]
E --> F[发起 proxy.ServeHTTP]
第五章:从陷阱到范式:Go HTTP生态测试演进路径
常见的HTTP测试陷阱:mock滥用与真实行为脱节
许多团队早期采用 httpmock 或 gock 对所有外部依赖进行强拦截,却忽视了HTTP状态码流转、重试逻辑、超时传播等真实链路行为。例如,一个使用 net/http/httptest.NewServer 模拟下游服务的测试,在并发压测下暴露出 http.Client 的 Transport 未配置 MaxIdleConnsPerHost,导致连接耗尽——而纯 mock 完全无法复现该问题。某电商订单服务曾因该疏漏在线上出现 3.7% 的请求静默失败,日志中仅显示 EOF,根源却是 mock 层掩盖了 TLS 握手异常。
真实端到端测试:基于 Docker Compose 的可重现环境
我们为支付网关模块构建了轻量级集成测试套件,使用 testcontainers-go 启动真实依赖组件:
func TestPaymentFlowWithRealRedisAndPostgres(t *testing.T) {
ctx := context.Background()
redisC, _ := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: testcontainers.ContainerRequest{
Image: "redis:7.2-alpine",
ExposedPorts: []string{"6379/tcp"},
},
Started: true,
})
defer redisC.Terminate(ctx)
// 启动PostgreSQL、启动MockPayProvider(Go实现的轻量HTTP服务)
// ...省略初始化代码
}
该方案使关键路径测试覆盖率从单元测试的 62% 提升至端到端验证的 91%,且每次 CI 运行耗时稳定在 48±3 秒。
测试可观测性:注入 OpenTelemetry 并捕获 HTTP trace
在测试中启用 otelhttp 中间件,将 httptrace.ClientTrace 与 sdktrace.Tracer 绑定,自动记录 DNS 解析、TLS 握手、首字节延迟等指标。以下为某次失败测试的 trace 片段(Mermaid 可视化):
sequenceDiagram
participant T as Test
participant S as Service
participant D as DownstreamAPI
T->>S: POST /v1/charge (ctx with traceID)
S->>D: GET /status (with baggage: region=us-west-2)
D-->>S: 200 OK + traceparent
S-->>T: 201 Created + full trace context
从测试金字塔到测试蜂巢:分层策略演进
| 层级 | 占比 | 工具链 | 验证重点 |
|---|---|---|---|
| 单元测试 | 58% | testing, gomock |
Handler 输入输出、错误分支 |
| 接口契约测试 | 22% | go-swagger + spectest |
OpenAPI v3 schema 符合性 |
| 场景驱动测试 | 20% | cucumber-golang + gherkin |
跨服务业务流(如退款→库存回滚) |
某金融风控服务通过引入契约测试,提前拦截了 17 处下游接口变更引发的 JSON 字段类型不一致问题,避免上线后触发熔断。
生产就绪测试:Chaos Engineering 在 HTTP 层的落地
在 staging 环境部署 chaos-mesh,对 ingress-nginx 注入故障:随机返回 503 Service Unavailable(概率 5%)、强制 Connection: close、模拟 X-Forwarded-For 头污染。验证服务是否正确执行 retryablehttp 重试、是否降级至缓存策略、是否向监控系统上报 http_client_errors_total{code="503"} 指标。
测试数据工厂:动态生成符合 RFC 7231 的请求体
使用 gotest.tools/golden 管理请求样本,并结合 github.com/brianvoe/gofakeit/v6 构建语义合法的数据:
func TestCreateOrder_ValidatesRFC7231Headers(t *testing.T) {
req := httptest.NewRequest("POST", "/orders", strings.NewReader(
gofakeit.JSON(`{"amount": {{price 10 1000}}, "currency": "{{currency}}"}`),
))
req.Header.Set("Content-Type", "application/json; charset=utf-8")
req.Header.Set("Accept", "application/vnd.api+json; version=1.0")
// 断言Header解析逻辑是否遵循RFC 7231 Section 5.3.2
}
该机制发现并修复了 3 处 Accept 头解析缺陷,包括对 q 参数权重排序错误及版本协商失败时的默认 fallback 行为。
