一篇文章搞懂Go语言中的Context

目录
  • 0 前置知识sync.WaitGroup
  • 1 简介
  • 2 context.Context引入
  • 3 context包的其他常用函数
    • 3.1 context.Background和context.TODO
    • 3.2 context.WithCancel和
    • 3.3 context.WithTimeout
    • 3.4 context.WithDeadline
    • 3.5 context.WithValue
  • 4 实例:请求浏览器超时
  • 5 Context包都在哪些地方使用
  • 6 小结

0 前置知识sync.WaitGroup

sync.WaitGroup是等待一组协程结束。它实现了一个类似任务队列的结构,可以向队列中加入任务,任务完成后就把任务从队列中移除,如果队列中的任务没有全部完成,队列就会触发阻塞以阻止程序继续运行。 sync.WaitGroup只有3个方法,Add(),Done(),Wait() 。

其中Done()是Add(-1)的别名,使用Add()添加计数,Done()减掉一个计数,计数不为0, 阻塞Wait()的运行。

示例:

package main
import (
   "fmt"
   "sync"
   "time"
)
var group sync.WaitGroup
func sayHello() {
   for i := 0; i < 5; i++ {
      fmt.Println("hello......")
      time.Sleep(time.Second)
   }
   //线程结束 -1
   group.Done()
}
func sayHi() {
   //线程结束 -1
   defer group.Done()
   for i := 0; i < 5; i++ {
      fmt.Println("hi......")
      time.Sleep(time.Second)
   }
}
func main() {
   //+2
   group.Add(2)
   fmt.Println("main正在阻塞...")
   go sayHello()
   fmt.Println("main持续阻塞...")
   go sayHi()
   //线程等待
   group.Wait()
   fmt.Println("main貌似结束了阻塞...")
}

效果:

1 简介

在 Go 服务器中,每个传入请求都在其自己的 goroutine 中处理。请求处理程序通常会启动额外的 goroutine 来访问后端,例如数据库和 RPC 服务。处理请求的一组 goroutine 通常需要访问特定于请求的值,例如最终用户的身份、授权令牌和请求的截止日期。当请求被取消或超时时,处理该请求的所有 goroutine 都应该快速退出,以便系统可以回收它们正在使用的任何资源。

为此,开发了一个context包,可以轻松地将请求范围的值、取消信号和截止日期跨 API 边界传递给处理请求所涉及的所有 goroutine。

Context携带一个截止日期、一个取消信号和其他跨越API边界的值。上下文的方法可以被多个gor例程同时调用。

对服务器的传入请求应该创建一个上下文,对服务器的传出调用应该接受一个上下文。它们之间的函数调用链必须传播 Context,可选择将其替换为使用 WithCancel、WithDeadline、WithTimeout 或 WithValue 创建的派生 Context。当一个上下文被取消时,所有从它派生的上下文也被取消。

WithCancel、WithDeadline 和 WithTimeout 函数采用 Context(父)并返回派生的 Context(子)和 CancelFunc。调用 CancelFunc 会取消子项及其子项,删除父项对子项的引用,并停止任何关联的计时器。调用 CancelFunc 失败会泄漏子项及其子项,直到父项被取消或计时器触发。go vet 工具检查是否在所有控制流路径上使用了 CancelFuncs。

使用上下文的程序应遵循以下规则,以保持跨包的接口一致,并启用静态分析工具来检查上下文传播:

不要将上下文存储在结构类型中;相反,将 Context 显式传递给需要它的每个函数。

Context 应该是第一个参数,通常命名为 ctx:

func DoSomething(ctx context.Context, arg Arg) error {
    // ... 使用 ctx ...
}

即使函数允许,也不要传递 nil 上下文。如果不确定要使用哪个 Context,请传递 context.TODO。

仅将上下文值用于传输流程和 API 的请求范围数据,而不用于将可选参数传递给函数。

相同的 Context 可以传递给在不同的 goroutine 中运行的函数;上下文对于多个 goroutine 同时使用是安全的。

2 context.Context引入

//上下文携带截止日期、取消信号和请求范围的值在API的界限。它的方法是安全的同时使用多个了goroutine。
type Context interface {
    // Done返回一个在上下文被取消或超时时关闭的通道。
    Done() <-chan struct{}
​
    // Err表示在Done通道关闭后为何取消此上下文。
    Err() error
​
    // Deadline返回上下文将被取消的时间(如果有的话)。
    Deadline() (deadline time.Time, ok bool)
​
    // Value返回与key相关的值,如果没有则返回nil。
    Value(key interface{}) interface{}
}
  • Done方法返回一个通道,该通道作为代表运行的函数的取消信号Context:当通道关闭时,函数应该放弃它们的工作并返回。
  • Err方法返回一个错误,指示Context取消的原因。
  • 一个Context对于多个 goroutine 同时使用是安全的。代码可以将单个传递Context给任意数量的 goroutines 并取消它Context以向所有goroutine 发出信号。
  • Deadline方法允许函数确定它们是否应该开始工作,还可以使用截止日期来设置 I/O 操作的超时时间。
  • Value允许一个Context携带请求范围的数据。该数据必须是安全的,以便多个 goroutine 同时使用。

3 context包的其他常用函数

3.1 context.Background和context.TODO

Background是任何Context树的根,它永远不会被取消:

//Background返回一个空的Context。 它永远不会取消,没有截止日期,没有价值。 Background通常用于main、init和tests,并作为传入请求的顶级上下文。  
func Background() Context

给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO()

3.2 context.WithCancel和

WithCancelt返回派生的Context值,可以比父Context更快地取消。当请求处理程序返回时,通常会取消与传入请求关联的content。当使用多个副本时,WithCancel对于取消冗余请求也很有用。

// WithCancel返回一个父进程的副本,该父进程的Done通道被尽快关闭。 关闭Done或调用cancel。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
// CancelFunc取消一个上下文。
type CancelFunc func()

示例:

package main
import (
   "context"
   "fmt"
)
func play(ctx context.Context) <-chan int {
   dist := make(chan int)
   n := 1
   //匿名函数 向dist中加入元素
   go func() {
      for {
         select {
         //ctx为空时将不会执行这个
         case <-ctx.Done():
            return // return结束该goroutine,防止泄露
            //向dist中加入元素
         case dist <- n:
            n++
         }
      }
   }()
   return dist
}
func main() {
   //返回空的context
   ctx, cancel := context.WithCancel(context.Background())
   defer cancel() // 调用cancel
   for n := range play(ctx) {
      fmt.Println(n)
      if n == 5 {
         break
      }
   }
}

扩展:go中select的用法

```
select的用法与switch语言非常类似,由select开始一个新的选择块,每个选择条件由case语句来描述。
与switch语句相比, select有比较多的限制,其中最大的一条限制就是每个case语句里必须是一个IO操作,大致的结构如下:
``` go
select {
   case <-chan1:
      // 如果chan1成功读到数据,则进行该case处理语句
   case chan2 <- 1:
      // 如果成功向chan2写入数据,则进行该case处理语句
   default:
      // 如果上面都没有成功,则进入default处理流程
}
```
在一个select语句中,Go语言会按顺序从头至尾评估每一个发送和接收的语句。
如果其中的任意一语句可以继续执行(即没有被阻塞),那么就从那些可以执行的语句中任意选择一条来使用。
如果没有任意一条语句可以执行(即所有的通道都被阻塞),那么有两种可能的情况:
- 如果给出了default语句,那么就会执行default语句,同时程序的执行会从select语句后的语句中恢复。
- 如果没有default语句,那么select语句将被阻塞,直到至少有一个通信可以进行下去。
```

3.3 context.WithTimeout

WithTimeout返回派生的Context值,WithTimeout用于设置请求到后端服务器的截止日期:

//WithTimeout返回一个父进程的副本,该父进程的Done通道被立即关闭的父母。关闭“完成”、调用“取消”或超时结束。新
//Context的Deadline是现在的更快+timeout和父的Deadline,如果任何。 如果计时器仍然在运行,则cancel函数释放它资源。
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
// CancelFunc取消一个上下文。
type CancelFunc func()

示例:

package main
import (
   "context"
   "fmt"
   "sync"
   "time"
)
var wg sync.WaitGroup
func worker(ctx context.Context) {
    LOOP:
   for {
      fmt.Println("db connecting ...")
      time.Sleep(time.Millisecond * 10) // 假设正常连接数据库耗时10毫秒
      select {
      case <-ctx.Done(): // 50毫秒后自动调用
         break LOOP
      default:
      }
   }
   fmt.Println("worker done!")
   wg.Done()
}
func main() {
   // 设置一个50毫秒的超时
   ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
   wg.Add(1)
   go worker(ctx)
   time.Sleep(time.Second * 5)
   cancel() // 通知子goroutine结束
   wg.Wait()
   fmt.Println("over")
}

执行结果:

3.4 context.WithDeadline

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
   if parent == nil {
      panic("cannot create context from nil parent")
   }
   if cur, ok := parent.Deadline(); ok && cur.Before(d) {
      // 目前的期限已经比新的期限提前
      return WithCancel(parent)
   }
   c := &timerCtx{
      cancelCtx: newCancelCtx(parent),
      deadline:  d,
   }
   propagateCancel(parent, c)
   dur := time.Until(d)
   if dur <= 0 {
      c.cancel(true, DeadlineExceeded) // 截止日期已经过去了
      return c, func() { c.cancel(false, Canceled) }
   }
   c.mu.Lock()
   defer c.mu.Unlock()
   if c.err == nil {
      c.timer = time.AfterFunc(dur, func() {
         c.cancel(true, DeadlineExceeded)
      })
   }
   return c, func() { c.cancel(true, Canceled) }
}

示例:

package main
import (
   "context"
   "fmt"
   "time"
)
func main() {
   d := time.Now().Add(500 * time.Millisecond)
   ctx, cancel := context.WithDeadline(context.Background(), d)
   // 尽管ctx会过期,但在任何情况下调用它的cancel函数都是很好的实践。
   // 如果不这样做,可能会使上下文及其父类存活的时间超过必要的时间。
   defer cancel()
   select {
   case <-time.After(1 * time.Second):
      fmt.Println("over")
   case <-ctx.Done():
      fmt.Println(ctx.Err())
   }
}

执行结果:

3.5 context.WithValue

WithValue提供了一种将请求范围的值与Context关联的方法 :

//WithValue返回父元素的副本,其Value方法返回val for key。
func WithValue(parent Context, key interface{}, val interface{}) Context

了解如何使用context包的最好方法是通过一个已工作的示例。

示例:

package main
import (
   "context"
   "fmt"
   "sync"
   "time"
)
type TraceCode string
var wg sync.WaitGroup
func worker(ctx context.Context) {
   key := TraceCode("KEY_CODE")
   traceCode, ok := ctx.Value(key).(string) // 在子goroutine中获取trace code
   if !ok {
      fmt.Println("invalid trace code")
   }
    LOOP:
   for {
      fmt.Printf("worker,code:%s\n", traceCode)
      time.Sleep(time.Millisecond * 10) // 假设正常连接数据库耗时10毫秒
      select {
      case <-ctx.Done(): // 50毫秒后自动调用
         break LOOP
      default:
      }
   }
   fmt.Println("worker is over!")
   wg.Done()
}
​
func main() {
   // 设置一个50毫秒的超时
   ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
   // 在系统的入口中设置trace code传递给后续启动的goroutine实现日志数据聚合
   ctx = context.WithValue(ctx, TraceCode("KEY_CODE"), "12512312234")
   wg.Add(1)
   go worker(ctx)
   time.Sleep(time.Second * 5)
   cancel() // 通知子goroutine结束
   wg.Wait()
   fmt.Println("over")
}

执行结果:

4 实例:请求浏览器超时

server端:

package main
import (
   "fmt"
   "math/rand"
   "net/http"
   "time"
)
// server端,随机出现慢响应
func indexHandler(w http.ResponseWriter, r *http.Request) {
   number := rand.Intn(2)
   if number == 0 {
      time.Sleep(time.Second * 10) // 耗时10秒的慢响应
      fmt.Fprintf(w, "slow response")
      return
   }
   fmt.Fprint(w, "quick response")
}
func main() {
   http.HandleFunc("/", indexHandler)
   err := http.ListenAndServe(":9999", nil)
   if err != nil {
      panic(err)
   }
}

client端:

package main
import (
   "context"
   "fmt"
   "io/ioutil"
   "net/http"
   "sync"
   "time"
)
// 客户端
​
type respData struct {
   resp *http.Response
   err  error
}
func doCall(ctx context.Context) {
   // http长连接
   transport := http.Transport{DisableKeepAlives: true}
   client := http.Client{Transport: &transport}
​
   respChan := make(chan *respData, 1)
   req, err := http.NewRequest("GET", "http://127.0.0.1:9999/", nil)
   if err != nil {
      fmt.Println(err)
      return
   }
   req = req.WithContext(ctx) // 使用带超时的ctx创建一个新的client request
   var wg sync.WaitGroup
   wg.Add(1)
   defer wg.Wait()
   go func() {
      resp, err := client.Do(req)
      fmt.Printf("resp:%v, err:%v\n", resp, err)
      rd := &respData{
         resp: resp,
         err:  err,
      }
      respChan <- rd
      wg.Done()
   }()
   select {
   case <-ctx.Done():
      fmt.Println("timeout...")
   case result := <-respChan:
      fmt.Println("success....")
      if result.err != nil {
         fmt.Printf("err:%v\n", result.err)
         return
      }
      defer result.resp.Body.Close()
      data, _ := ioutil.ReadAll(result.resp.Body)
      fmt.Printf("resp:%v\n", string(data))
   }
}
​
func main() {
   // 定义一个100毫秒的超时
   ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100)
   defer cancel() // 调用cancel释放子goroutine资源
   doCall(ctx)
}

5 Context包都在哪些地方使用

许多服务器框架提供了用于承载请求作用域值的包和类型。我们可以定义“Context”接口的新实现,在使用现有框架的代码和需要“Context”参数的代码之间架起桥梁。

6 小结

在谷歌中,要求Go程序员将“Context”参数作为传入和传出请求之间的调用路径上的每个函数的第一个参数传递。这使得许多不同团队开发的Go代码能够很好地互操作。它提供了对超时和取消的简单控制,并确保像安全凭证这样的关键值能够正确地传输Go程序。

想要构建在“Context”上的服务器框架应该提供“Context”的实现来连接它们的包和那些需要“Context”参数的包。然后,它们的客户端库将接受来自调用代码的“Context”。通过为请求范围的数据和取消建立一个公共接口,“上下文”使包开发人员更容易共享创建可伸缩服务的代码。

到此这篇关于一篇文章搞懂Go语言中的Context的文章就介绍到这了,更多相关 Go Context内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • go语言context包功能及操作使用详解

    目录 Context包到底是干嘛用的? context原理 什么时候应该使用 Context? 如何创建 Context? 主协程通知有子协程,子协程又有多个子协程 context核心接口 emptyCtx结构体 Backgroud TODO valueCtx结构体 WithValue向context添加值 Value向context取值 示例 WithCancel可取消的context cancelCtx结构体 WithDeadline-超时取消context WithTimeout-超时取消

  • 详解Go语言的context包从放弃到入门

    一.Context包到底是干嘛用的 我们会在用到很多东西的时候都看到context的影子,比如gin框架,比如grpc,这东西到底是做啥的? 大家都在用,没几个知道这是干嘛的,知其然而不知其所以然, 谁都在CRUD,谁都觉得if else就完了,有代码能copy我也行,原理啥啥不懂不重要,反正就是一把梭 原理说白了就是: 当前协程取消了,可以通知所有由它创建的子协程退出 当前协程取消了,不会影响到创建它的父级协程的状态 扩展了额外的功能:超时取消.定时取消.可以给子协程共享数据 二.主协程退出通

  • 深入理解 Go 语言中的 Context

    Hi,大家好,我是明哥. 在自己学习 Golang 的这段时间里,我写了详细的学习笔记放在我的个人微信公众号 <Go编程时光>,对于 Go 语言,我也算是个初学者,因此写的东西应该会比较适合刚接触的同学,如果你也是刚学习 Go 语言,不防关注一下,一起学习,一起成长. 我的在线博客:http://golang.iswbm.com 我的 Github:github.com/iswbm/GolangCodingTime 1. 什么是 Context? 在 Go 1.7 版本之前,context 还

  • GOLANG使用Context实现传值、超时和取消的方法

    GO1.7之后,新增了context.Context这个package,实现goroutine的管理. Context基本的用法参考GOLANG使用Context管理关联goroutine. 实际上,Context还有个非常重要的作用,就是设置超时.比如,如果我们有个API是这样设计的: type Packet interface { encoding.BinaryMarshaler encoding.BinaryUnmarshaler } type Stack struct { } func

  • GoLang之使用Context控制请求超时的实现

    起因   之前接触了一个需求:提供一个接口,这个接口有一个超时时间,如果超时了返回超时异常:这个接口中调用其他的接口,如果调用超时了,所有请求全部结束.   在这个接口中,我使用了go协程去调用其他接口,所以不仅涉及到请求的超时控制,而且还涉及到父协程对子协程的控制问题.在翻阅了一些资料之后,了解到了Context的基本知识. Context   Context是golang.org.pkg下的一个包,类型是接口类型.主要功能有 父协程控制所有的子协程   Context可以通过context.

  • Go语言context上下文管理的使用

    目录 context有什么作用 传递共享的数据 取消goroutine 防止goroutine泄漏 context.Value的查找过程是怎样的 context 有什么作用 context 主要用来在goroutine 之间传递上下文信息,包括:取消信号.超时时间.截止时间.k-v 等. Go 常用来写后台服务,通常只需要几行代码,就可以搭建一个 http server. 在 Go 的 server 里,通常每来一个请求都会启动若干个 goroutine 同时工作:有些去数据库拿数据,有些调用下

  • Go语言上下文context底层原理

    目录 1. context 介绍 2. 基本介绍 3. 源码分析 3.1 Context 接口 3.2 emptyCtx 3.3 cancelCtx 3.4 timerCtx 3.5 valueCtx 4. 使用建议 1. context 介绍 很多时候,我们会遇到这样的情况,上层与下层的goroutine需要同时取消,这样就涉及到了goroutine间的通信.在Go中,推荐我们以通信的方式共享内存,而不是以共享内存的方式通信.所以,就需要用到channl,但是,在上述场景中,如果需要自己去处理

  • 一篇文章搞懂Go语言中的Context

    目录 0 前置知识sync.WaitGroup 1 简介 2 context.Context引入 3 context包的其他常用函数 3.1 context.Background和context.TODO 3.2 context.WithCancel和 3.3 context.WithTimeout 3.4 context.WithDeadline 3.5 context.WithValue 4 实例:请求浏览器超时 5 Context包都在哪些地方使用 6 小结 0 前置知识sync.Wait

  • 一篇文章搞懂JavaScript正则表达式之方法

    咱们来看看JavaScript中都有哪些操作正则的方法. RegExp RegExp 是正则表达式的构造函数. 使用构造函数创建正则表达式有多种写法: new RegExp('abc'); // /abc/ new RegExp('abc', 'gi'); // /abc/gi new RegExp(/abc/gi); // /abc/gi new RegExp(/abc/m, 'gi'); // /abc/gi 它接受两个参数:第一个参数是匹配模式,可以是字符串也可以是正则表达式:第二个参数是

  • 一篇文章搞懂python的转义字符及用法

    什么是转义字符 转义字符是一个计算机专业词汇.在计算机当中,我们可以写出123 ,也可以写出字母abcd,但有些字符我们无法手动书写,比如我们需要对字符进行换行处理,但不能写出来换行符,当然我们也看不见换行符.像这种情况,我们需要在字符中使用特殊字符时,就需要用到转义字符,在python里用反斜杠\转义字符. 在交互式解释器中,输出的字符串用引号引起来,特殊字符用反斜杠\转义.虽然可能和输入看上去不太一样,但是两个字符串是相等的. 在python里,转义字符\可以转义很多字符,比如\n表示换行,

  • 一篇文章搞懂Python反斜杠的相关问题

    大家在开发Python的过程中,一定会遇到很多反斜杠的问题,很多人被反斜杠的数量搞得头大. 首先我们写一段非常简单的Python代码,它的作用是把一个字段先转换为JSON格式的字符串,然后把这个字符串再转换为JSON格式的字符串: import json info = {'name': 'kingname', 'address': '杭州', 'salary': 99999} info_json = json.dumps(info) # 第一次转换以后,打印出来 print(info_json)

  • 一篇文章搞懂Python Unittest测试方法的执行顺序

    目录 Unittest 回到主题 源码初窥 回到问题的本质 1. 以字典序的方式编写test方法 2. 回归本质,从根本解决问题 总结 Unittest unittest大家应该都不陌生.它作为一款博主在5-6年前最常用的单元测试框架,现在正被pytest,nose慢慢蚕食. 渐渐地,看到大家更多的讨论的内容从unittest+HTMLTestRunner变为pytest+allure2等后起之秀. 不禁感慨,终究是自己落伍了,跟不上时代的大潮了. 回到主题 感慨完了,回到正文.虽然unitte

  • 一篇文章搞懂MySQL加锁机制

    目录 前言 锁的分类 乐观锁和悲观锁 共享锁(S锁)和排他锁(X锁) 按加锁粒度区分 全局锁 表级锁(表锁和MDL锁) 意向锁 行锁 间隙锁 next-key lock(临键锁) 加锁规则 死锁和死锁检测 总结 前言 在数据库中设计锁的目的是为了处理并发问题,在并发对资源进行访问时,数据库要合理控制对资源的访问规则. 而锁就是用来实现这些访问规则的一个数据结构. 在对数据并发操作时,没有锁可能会引起数据的不一致,导致更新丢失. 锁的分类 乐观锁和悲观锁 乐观锁: 对于出现更新丢失的可能性比较乐观

  • 一篇文章搞懂Vue3中如何使用ref获取元素节点

    目录 前言 1.回顾 Vue2 中的 ref 2.Vue3 中 ref 访问元素 3.v-for 中使用 ref 4.ref 绑定函数 5.组件上使用 ref 总结 前言 虽然在 Vue 中不提倡我们直接操作 DOM,毕竟 Vue 的理念是以数据驱动视图.但是在实际情况中,我们有很多需求都是需要直接操作 DOM 节点的,这个时候 Vue 提供了一种方式让我们可以获取 DOM 节点:ref 属性.ref 属性是 Vue2 和 Vue3 中都有的,但是使用方式却不大一样,这也导致了很多从 Vue2

  • 一篇文章搞懂:词法作用域、动态作用域、回调函数及闭包

    前言 把以前一直只限于知道,却不清晰理解的这几个概念完完整整地梳理了一番.内容参考自wiki页面,然后加上自己一些理解. 词法作用域和动态作用域 不管什么语言,我们总要学习作用域(或生命周期)的概念,比如常见的称呼:全局变量.包变量.模块变量.本地变量.局部变量等等.不管如何称呼这些作用域的范围,实现它们的目的都一样: (1)为了避免名称冲突; (2)为了限定变量的生命周期(本文以变量名说事,其它的名称在规则上是一样的). 但是不同语言的作用域规则不一样,虽然学个简单的基础就足够应用,因为我们有

  • 一篇文章搞懂python混乱的切换操作与优雅的推导式

    前言 因为工作中不怎么使用python,所以对python的了解不够,只是在使用的时候才去学,在之前的几个例子中几乎没使用什么python的特有语法,本着完成任务优先的原则也没有深入,但是在阅读别人的代码的时候发现有些特有语法不是很熟悉,搞不清代码的真正意思,今天就搞清楚切片和推导式的使用,OK.我们开始吧 记忆点:正向的时候第一个是0,负向的时候第一个是-1,可以把列表当做一个换,正向的是1 ,负向的 是-1,0 是中间点 1.混乱的切片操作 一个完整的切片表达式包含两个":",用于

  • python 一篇文章搞懂装饰器所有用法(建议收藏)

    01. 装饰器语法糖 如果你接触 Python 有一段时间了的话,想必你对 @ 符号一定不陌生了,没错 @ 符号就是装饰器的语法糖. 它放在一个函数开始定义的地方,它就像一顶帽子一样戴在这个函数的头上.和这个函数绑定在一起.在我们调用这个函数的时候,第一件事并不是执行这个函数,而是将这个函数做为参数传入它头顶上这顶帽子,这顶帽子我们称之为装饰函数 或 装饰器. 你要问我装饰器可以实现什么功能?我只能说你的脑洞有多大,装饰器就有多强大. 装饰器的使用方法很固定: 先定义一个装饰函数(帽子)(也可以

随机推荐