mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1714 字
5 分钟
Go Context 深度解析:取消信号与请求作用域
2022-11-08

Go 的 context 包是控制并发和传递请求作用域数据的核心机制。无论是 HTTP 服务、gRPC 调用还是数据库操作,正确使用 Context 都是编写健壮 Go 代码的关键。

一、为什么需要 Context?#

1.1 问题场景#

// 场景:用户请求需要取消
func handleRequest(req Request) {
result := doSlowWork() // 耗时操作
// 问题:如果客户端断开连接,doSlowWork 还在继续执行
}
// 场景:请求需要超时控制
func fetchUserData(userID string) {
data := database.Query(userID) // 可能很慢
// 问题:没有超时控制,可能无限等待
}

1.2 Context 的作用#

// 解决方案:使用 Context
func handleRequest(ctx context.Context, req Request) error {
// ctx 可以被取消
result, err := doSlowWork(ctx) // 传递 ctx
if err != nil {
return err
}
return processResult(result)
}

二、Context 接口#

2.1 接口定义#

type Context interface {
// Deadline 返回 ctx 的截止时间
Deadline() (deadline time.Time, ok bool)
// Done 返回一个关闭的通道,当 ctx 被取消或超时时关闭
Done() <-chan struct{}
// Err 返回 ctx 被关闭的原因
Err() error
// Value 返回 ctx 中存储的值
Value(key any) any
}

2.2 内置 Context#

var (
// 不可取消,没有截止时间,没有值
// 通常用于 main 函数或顶级请求
background = contextImpl{}
// TODO: 语义未明确,用于临时占位
todo = contextImpl{}
)
func Background() Context { return background }
func TODO() Context { return todo }

三、创建子 Context#

3.1 WithCancel#

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
// 返回新的 ctx 和取消函数
// 调用 cancel() 会关闭 Done() 通道
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
time.Sleep(2 * time.Second)
cancel() // 取消操作
}()
select {
case <-ctx.Done():
fmt.Println("Cancelled:", ctx.Err())
}
}

3.2 WithTimeout#

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
// 返回带超时的 ctx
// 相当于 WithDeadline(parent, time.Now().Add(timeout))
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
result, err := fetchData(ctx)
if err != nil {
fmt.Println("Error:", err) // 3秒后会超时
}
}

3.3 WithDeadline#

func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {
// 返回在指定时间点过期的 ctx
}
func main() {
// 设置截止时间:明天 12:00
deadline := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
}

3.4 WithTimeout vs WithDeadline 时间线#

WithTimeoutWithDeadline 本质上是同一个机制,区别仅在于时间参数的表达方式。WithTimeout(parent, timeout) 等价于 WithDeadline(parent, time.Now().Add(timeout))

下面的时间线展示了二者的关系:

gantt title WithTimeout vs WithDeadline 时间线对比 dateFormat HH:mm:ss axisFormat %H:%M:%S section WithTimeout 请求开始 :a1, 00:00:00, 0s 超时 3 秒 :a2, 00:00:00, 3s section WithDeadline 请求开始 :b1, 00:00:00, 0s 截止时间 12:00:03 :milestone, 00:00:03, 0s section 效果 二者等效:到期自动取消 ctx :c1, 00:00:00, 3s

选择建议:

  • 使用 WithTimeout 当你关注”操作最长允许多久”
  • 使用 WithDeadline 当你有一个明确的绝对时间点(如配置中的截止时间)

3.5 WithValue#

func WithValue(parent Context, key, val any) Context {
// 返回携带值的 ctx
// key 应该是不可比较的类型或自定义的 key 类型
}
// 推荐:定义自定义 key 类型避免冲突
type key string
const userIDKey key = "userID"
const traceIDKey key = "traceID"
func WithUserID(ctx context.Context, userID string) Context {
return context.WithValue(ctx, userIDKey, userID)
}
func GetUserID(ctx context.Context) string {
if userID, ok := ctx.Value(userIDKey).(string); ok {
return userID
}
return ""
}

四、取消机制原理#

4.1 树状取消#

Context 构成树状结构,取消父节点会自动取消所有子节点。下图展示了一个典型的 Context 树形层级关系:

graph TD BG["Background()<br/>根 Context"] --> WC1["WithCancel<br/>HTTP 请求"] BG --> WT1["WithTimeout(3s)<br/>gRPC 调用"] WC1 --> WV1["WithValue(traceID)<br/>追踪信息"] WC1 --> WT2["WithTimeout(5s)<br/>数据库查询"] WT1 --> WC2["WithCancel<br/>流式处理"] WV1 --> WT3["WithTimeout(2s)<br/>缓存读取"] WT2 --> WC3["WithCancel<br/>结果处理"] style BG fill:#4CAF50,color:#fff style WC1 fill:#2196F3,color:#fff style WT1 fill:#FF9800,color:#fff style WV1 fill:#9C27B0,color:#fff style WT2 fill:#FF9800,color:#fff style WC2 fill:#2196F3,color:#fff style WT3 fill:#FF9800,color:#fff style WC3 fill:#2196F3,color:#fff

图解说明:

  • 绿色节点:根 Context(Background()),永不被取消
  • 蓝色节点:WithCancel 创建的可取消 Context
  • 橙色节点:WithTimeout 创建的带超时 Context
  • 紫色节点:WithValue 创建的携带值的 Context

取消传播方向为自上而下:当 HTTP 请求 Context 被取消时,其下的追踪信息、数据库查询、缓存读取等子 Context 都会被级联取消。

// Context 构成树状结构
// 取消父节点会自动取消所有子节点
func main() {
parent, cancel := context.WithCancel(context.Background())
defer cancel()
child1, cancel1 := context.WithCancel(parent)
child2, cancel2 := context.WithCancel(parent)
defer cancel1()
defer cancel2()
// 取消 parent 会同时取消 child1 和 child2
cancel()
}

4.2 取消传播#

下面的流程图展示了 HTTP 请求中 Context 取消信号的完整传播路径:

sequenceDiagram participant Client as 客户端 participant HTTP as HTTP Server participant Handler as Handler participant DB as 数据库 participant Cache as 缓存 Client->>HTTP: 发起请求 HTTP->>Handler: r.Context() Note over Handler: 创建子 ctx<br/>WithTimeout(ctx, 5s) Handler->>DB: QueryContext(ctx, SQL) Handler->>Cache: GetContext(ctx, key) Client-xHTTP: 断开连接 Note over HTTP: 检测到连接断开<br/>自动 cancel ctx HTTP-->>Handler: ctx.Done() 关闭 Handler-->>DB: ctx 取消,查询终止 Handler-->>Cache: ctx 取消,读取终止 Note over Handler: 所有子操作收到取消信号<br/>返回 context.Canceled
// HTTP 服务中的取消传播
func handleRequest(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() // 获取请求的 ctx
// 如果客户端断开连接,ctx 会自动取消
result, err := doWork(ctx)
if err != nil {
// 处理错误
}
}

4.3 避免泄露#

// 错误:忘记调用 cancel
func wrong() {
ctx, cancel := context.WithTimeout(context.Background(), time.Hour)
// 没有 defer cancel(),造成资源泄露
}
// 正确:确保 cancel 被调用
func correct() {
ctx, cancel := context.WithTimeout(context.Background(), time.Hour)
defer cancel() // 函数结束时取消
}

五、HTTP 服务中的应用#

5.1 标准用法#

func main() {
http.HandleFunc("/api/data", func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// 处理请求,传递 ctx
data, err := fetchData(ctx)
if err != nil {
// 检查是否是 ctx 取消导致的错误
if ctx.Err() == context.Canceled {
http.Error(w, "Client cancelled", http.StatusClientClosedRequest)
return
}
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(data)
})
log.Fatal(http.ListenAndServe(":8080", nil))
}

5.2 中间件模式#

// 追踪中间件
func tracingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// 添加追踪信息
traceID := generateTraceID()
ctx = context.WithValue(ctx, traceIDKey, traceID)
// 记录开始时间
start := time.Now()
// 包装 ResponseWriter 记录状态码
wrapped := &statusWriter{ResponseWriter: w, statusCode: 200}
next.ServeHTTP(wrapped, r.WithContext(ctx))
// 记录日志
log.Printf("traceID=%s status=%d duration=%v",
traceID, wrapped.statusCode, time.Since(start))
})
}
type statusWriter struct {
http.ResponseWriter
statusCode int
}
func (w *statusWriter) WriteHeader(code int) {
w.statusCode = code
w.ResponseWriter.WriteHeader(code)
}

5.3 数据库查询#

import "context"
// 使用 context 超时控制数据库查询
func queryWithTimeout(ctx context.Context, db *sql.DB) ([]Row, error) {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
rows, err := db.QueryContext(ctx, "SELECT * FROM users")
if err != nil {
return nil, err
}
defer rows.Close()
return scanRows(rows)
}

六、gRPC 中的 Context#

6.1 传递元数据#

import (
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)
func clientUnaryCall(ctx context.Context, client pb.UserServiceClient) {
// 添加 metadata
md := metadata.Pairs(
"authorization", "Bearer token",
"trace-id", traceID,
)
ctx = metadata.NewOutgoingContext(ctx, md)
// 调用
resp, err := client.GetUser(ctx, &pb.GetUserRequest{ID: userID})
}
func serverInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
// 从 context 提取 metadata
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Error(codes.Unauthenticated, "missing metadata")
}
token := md.Get("authorization")
if len(token) == 0 {
return nil, status.Error(codes.Unauthenticated, "missing token")
}
// 继续处理
return handler(ctx, req)
}

6.2 取消传播#

// 客户端取消传播到服务端
ctx, cancel := context.WithCancel(parentCtx)
defer cancel()
stream, err := client.StreamData(ctx, &pb.StreamRequest{})
// 如果 cancel() 被调用,服务端会收到 ctx.Canceled

七、值传递最佳实践#

7.1 Key 类型设计#

// 错误:使用字符串 key,容易冲突
ctx.Value("userID") // 可能返回其他包的同名值
// 正确:定义专属类型
type userIDKey struct{}
type traceIDKey struct{}
func (ctx context.Context) GetUserID() string {
if v := ctx.Value(userIDKey{}); v != nil {
return v.(string)
}
return ""
}
// 使用包级别的私有 key
var keyUserID = struct{}{}
var keyTraceID = struct{}{}

7.2 Context 的值不应该用于#

// 错误用法:
// 1. 传递可选参数(用函数选项模式)
// 2. 传递依赖注入(用 DI 容器)
// 3. 传递日志记录器(用结构化日志库)
// 正确用法:
// 1. 请求作用域的追踪 ID
// 2. 请求作用域的用户信息
// 3. 取消信号

八、常见陷阱#

8.1 Context 值是并发安全的#

// Context 的 Value 方法是并发安全的
// 多个 goroutine 可以同时读取

8.2 不要把 Context 放在结构体里#

// 错误:将 Context 存储在结构体中
type Request struct {
ctx context.Context // 不应该放在这里
Data string
}
func (r *Request) Process() error {
// ctx 的生命周期与 Request 绑定,难以追踪取消来源
return doWork(r.ctx)
}
// 正确:Context 作为函数的第一个参数传递
type Request struct {
Data string
}
func Process(ctx context.Context, req *Request) error {
// ctx 的生命周期由调用方控制,清晰且可追踪
return doWork(ctx)
}

8.3 超时设置要合理#

// 过长:失去控制作用
ctx, _ := context.WithTimeout(ctx, 24*time.Hour)
// 过短:还没完成就超时
ctx, _ := context.WithTimeout(ctx, 1*time.Millisecond)
// 合理:根据操作预估时间
ctx, _ := context.WithTimeout(ctx, 30*time.Second)

九、Go 1.20 Context 新特性#

Go 1.20 为 context 包引入了两个新 API:WithCancelCauseCause,用于在取消 Context 时携带原因信息,解决了传统 WithCancel 只能返回 context.Canceled 通用错误的局限。

9.1 WithCancelCause —— 携带原因的取消#

传统的 WithCancel 调用 cancel() 时,ctx.Err() 只能返回 context.Canceled,无法区分取消的具体原因。WithCancelCause 允许在取消时传入一个 error 作为原因。

// WithCancelCause 返回一个可携带取消原因的 Context
// 源码参考:https://github.com/golang/go/blob/go1.25.0/src/context/context.go
func WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc)
// CancelCauseFunc 是带原因的取消函数
type CancelCauseFunc func(cause error)
package main
import (
"context"
"errors"
"fmt"
"time"
)
// 定义具体的取消原因
var (
ErrUserCancelled = errors.New("user cancelled the request")
ErrTimeoutExceeded = errors.New("operation exceeded timeout")
ErrShuttingDown = errors.New("server is shutting down")
)
func main() {
ctx, cancel := context.WithCancelCause(context.Background())
go func() {
time.Sleep(2 * time.Second)
// 传入具体的取消原因
cancel(ErrUserCancelled)
}()
select {
case <-ctx.Done():
// ctx.Err() 仍然返回 context.Canceled
fmt.Println("Err:", ctx.Err())
// context.Cause(ctx) 返回具体的原因
fmt.Println("Cause:", context.Cause(ctx))
// 输出: Cause: user cancelled the request
}
}

9.2 Cause —— 获取取消原因#

context.Cause(ctx) 函数用于获取 Context 被取消的具体原因。

// Cause 返回导致 ctx 被取消的原始错误
// 如果 ctx 没有被取消,返回 nil
// 如果是通过 WithCancelCause 取消的,返回传入的 cause
// 如果是其他方式取消的,返回 ctx.Err()
func Cause(ctx Context) error
func handleRequest(ctx context.Context) {
<-ctx.Done()
// 区分不同的取消原因
switch cause := context.Cause(ctx); {
case errors.Is(cause, ErrUserCancelled):
fmt.Println("用户主动取消,清理部分结果")
cleanupPartialResults()
case errors.Is(cause, ErrTimeoutExceeded):
fmt.Println("操作超时,记录慢查询日志")
logSlowOperation()
case errors.Is(cause, ErrShuttingDown):
fmt.Println("服务关闭,持久化当前状态")
persistState()
default:
fmt.Println("未知取消原因:", cause)
}
}

9.3 WithCancelCause 与 WithCancel 的对比#

特性WithCancelWithCancelCause
取消函数签名func()func(error)
ctx.Err() 返回context.Canceledcontext.Canceled
获取具体原因不支持context.Cause(ctx)
向后兼容完全兼容,cancel(nil) 等价于旧版

迁移建议: 在新代码中优先使用 WithCancelCause 替代 WithCancel,即使暂时不需要原因信息,传入 nil 即可保持相同行为,为将来扩展预留空间。

十、Go 1.21 Context 新特性#

Go 1.21 为 context 包引入了两个重要 API:WithoutCancelAfterFunc,分别用于脱离父 Context 的取消信号和注册取消后的回调函数。

10.1 WithoutCancel —— 脱离父级取消#

在处理 HTTP 请求时,经常需要在请求结束后继续执行一些后台任务(如日志记录、指标上报、缓存预热等)。但请求的 Context 被取消后,这些后台任务也会被终止。WithoutCancel 可以创建一个不受父级取消影响的 Context。

// WithoutCancel 返回一个新 Context,它不受 parent 取消的影响
// 新 Context 没有截止时间,也不会被父级取消
// 但保留了 parent 中的值(Value)
// 源码参考:https://github.com/golang/go/blob/go1.25.0/src/context/context.go
func WithoutCancel(parent Context) Context
func handleRequest(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// 处理请求
data, err := fetchData(ctx)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(data)
// 请求已响应,但需要在后台继续处理
// 使用 WithoutCancel 创建脱离请求生命周期的 Context
bgCtx := context.WithoutCancel(ctx)
// bgCtx 中的 Value 仍然可以访问
traceID := ctx.Value(traceIDKey) // 可以读取
go func() {
// 即使原始请求 ctx 被取消,bgCtx 也不会被取消
logAccess(bgCtx, traceID, data)
updateCache(bgCtx, data)
emitMetrics(bgCtx, data)
}()
}

使用场景:

// 场景 1:请求完成后的异步日志记录
func logAfterResponse(ctx context.Context, req *http.Request) {
bgCtx := context.WithoutCancel(ctx)
go func() {
// 不受请求取消影响
log.Printf("method=%s path=%s traceID=%s",
req.Method, req.URL.Path, bgCtx.Value(traceIDKey))
}()
}
// 场景 2:请求触发的后台任务需要独立的超时控制
func spawnBackgroundTask(ctx context.Context) {
bgCtx := context.WithoutCancel(ctx)
// 给后台任务单独设置超时
taskCtx, cancel := context.WithTimeout(bgCtx, 10*time.Minute)
defer cancel()
go processInBackground(taskCtx)
}

注意事项:

  • WithoutCancel 返回的 Context 没有截止时间(Deadline() 返回 false
  • Done() 通道为 nil(永远不会关闭)
  • Err() 永远返回 nil
  • Value() 方法仍然可以读取父 Context 中的值

10.2 AfterFunc —— 注册取消后的回调#

AfterFunc 允许在 Context 被取消或超时后自动执行一个函数。这为资源清理、通知下游服务等场景提供了更优雅的方式。

// AfterFunc 注册一个在 ctx 被取消后异步调用 f 的回调
// 返回一个 stop 函数,调用它可以取消注册(如果 f 尚未执行)
// 如果 ctx 已经被取消,f 会在新的 goroutine 中立即执行
// 源码参考:https://github.com/golang/go/blob/go1.25.0/src/context/context.go
func AfterFunc(ctx Context, f func()) (stop func() bool)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 注册取消后的清理回调
stop := context.AfterFunc(ctx, func() {
fmt.Println("Context 被取消,执行清理操作...")
cleanupResources()
notifyDependentServices()
})
// 如果需要在 ctx 取消前停止回调
defer stop()
// 模拟工作
doWork(ctx)
}

实际应用:连接池释放

func handleConnection(ctx context.Context, pool *ConnectionPool) error {
conn, err := pool.Get(ctx)
if err != nil {
return err
}
// 当 ctx 被取消时,自动将连接归还到池中
stop := context.AfterFunc(ctx, func() {
fmt.Println("请求取消,释放连接回连接池")
pool.Put(conn)
})
defer stop()
// 使用连接处理业务
return processWithConn(ctx, conn)
}

实际应用:超时降级通知

func fetchWithFallback(ctx context.Context) ([]byte, error) {
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
var fallbackTriggered atomic.Bool
// 注册超时后的降级回调
context.AfterFunc(ctx, func() {
if fallbackTriggered.CompareAndSwap(false, true) {
// 超时后触发降级:使用缓存数据
log.Println("请求超时,触发降级逻辑")
triggerFallbackCache()
}
})
data, err := fetchFromPrimary(ctx)
if err != nil {
return nil, err
}
return data, nil
}

10.3 AfterFunc 的 stop 函数详解#

AfterFunc 返回的 stop 函数有几个重要行为:

ctx, cancel := context.WithCancel(context.Background())
// 注册回调
stop := context.AfterFunc(ctx, func() {
fmt.Println("回调已执行")
})
// stop() 的行为:
// 1. 如果 f 尚未启动,取消注册并返回 true
// 2. 如果 f 已经启动,返回 false(无法停止正在运行的 f)
// 3. 如果 f 已经执行完毕,返回 false
result := stop()
fmt.Println("是否成功阻止回调:", result)
cancel() // 取消 ctx

最佳实践: 总是 defer stop(),确保在函数退出时清理注册,避免回调在不需要时执行。

十一、源码解析#

以下源码分析基于 Go 官方仓库: context/context.go

11.1 cancelCtx 实现#

// cancelCtx 是 WithCancel 和 WithCancelCause 的底层实现
// 源码:https://github.com/golang/go/blob/go1.25.0/src/context/context.go
type cancelCtx struct {
Context
mu sync.Mutex
done atomic.Value // 惰性初始化的 chan struct{}
children map[canceler]struct{}
err error // 取消原因
}
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // 已经取消过
}
c.err = err
d, _ := c.done.Load().(chan struct{})
if d == nil {
c.done.Store(make(chan struct{}))
} else {
close(d) // 关闭 Done 通道
}
// 递归取消子节点
for child := range c.children {
child.cancel(false, err)
}
c.children = nil
c.mu.Unlock()
}

十二、总结#

函数版本用途
Background()Go 1创建根 Context
TODO()Go 1临时占位 Context
WithCancel()Go 1创建可取消的 ctx
WithTimeout()Go 1创建带超时的 ctx
WithDeadline()Go 1创建带截止时间的 ctx
WithValue()Go 1创建携带值的 ctx
WithCancelCause()Go 1.20创建可携带取消原因的 ctx
Cause()Go 1.20获取 ctx 的取消原因
WithoutCancel()Go 1.21创建脱离父级取消的 ctx
AfterFunc()Go 1.21注册 ctx 取消后的回调函数

最佳实践:

  1. 作为第一个参数传递 Context
  2. 总是检查 ctx 是否已取消
  3. 超时设置要合理
  4. 用自定义类型作为 value 的 key
  5. HTTP/gRPC 请求自动传播取消信号
  6. 需要区分取消原因时使用 WithCancelCause
  7. 后台任务需要脱离请求生命周期时使用 WithoutCancel
  8. 需要在 ctx 取消时执行清理逻辑时使用 AfterFunc

常见问题 FAQ#

Q1:context.Background 和 context.TODO 有什么区别?#

Background 返回非 nil 空 context,作为整棵 context 树的根。TODO 语义上表示”暂时不确定用什么 context,后续补充”。功能上相同,区别仅在语义。

Q2:context.Value 应该用来传什么?#

只传请求级别的元数据(trace_id、user_id 等),不传业务参数或可选配置。Value 机制是附加的,不应成为参数传递的主要方式。滥用 Value 会导致隐式依赖。

Q3:WithCancel 和 WithTimeout 怎么选?#

WithTimeout 适合有明确超时时间的场景(如 RPC 调用)。WithCancel 适合需要手动取消的场景(如用户中断)。WithTimeout 本质是 WithDeadline 的语法糖。

Q4:context 取消后,goroutine 会自动退出吗?#

不会。context 取消只是发出信号(ctx.Done() 返回 closed channel),goroutine 需要自己检查 ctx.Done() 并主动退出。不会强制中断 goroutine 的执行。

小结#

  • context 是 Go 并发控制的基石:取消信号、超时控制、值传递
  • context 树形结构:子 context 继承父 context 的取消和超时
  • WithCancel/WithTimeout/WithDeadline 创建可取消的子 context
  • context.Value 仅传请求元数据,不传业务参数
  • 正确使用:及时 cancel 释放资源、不传 nil context、检查 ctx.Done()

参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

Go Context 深度解析:取消信号与请求作用域
https://blog.souloss.com/posts/golang/go-context/
作者
Souloss
发布于
2022-11-08
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时