一文读懂go context


1.写在前面

go context相信大家经常遇到,也知道一些用法。今天主要是阅读一下go context的源码

go 1.21.1的context的源码只有一个文件。源代码785行,去掉注释后有效代码497行。

package main

import (
	"errors"
	"internal/reflectlite"
	"sync"
	"sync/atomic"
	"time"
)

type Context interface {
	Deadline() (deadline time.Time, ok bool)
	Done() <-chan struct{}
	Err() error
	Value(key any) any
}

var Canceled = errors.New("context canceled")

var DeadlineExceeded error = deadlineExceededError{}

type deadlineExceededError struct{}

func (deadlineExceededError) Error() string   { return "context deadline exceeded" }
func (deadlineExceededError) Timeout() bool   { return true }
func (deadlineExceededError) Temporary() bool { return true }

type emptyCtx struct{}

func (emptyCtx) Deadline() (deadline time.Time, ok bool) {
	return
}

func (emptyCtx) Done() <-chan struct{} {
	return nil
}

func (emptyCtx) Err() error {
	return nil
}

func (emptyCtx) Value(key any) any {
	return nil
}

type backgroundCtx struct{ emptyCtx }

func (backgroundCtx) String() string {
	return "context.Background"
}

type todoCtx struct{ emptyCtx }

func (todoCtx) String() string {
	return "context.TODO"
}
func Background() Context {
	return backgroundCtx{}
}

func TODO() Context {
	return todoCtx{}
}

type CancelFunc func()

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
	c := withCancel(parent)
	return c, func() { c.cancel(true, Canceled, nil) }
}

type CancelCauseFunc func(cause error)

func WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc) {
	c := withCancel(parent)
	return c, func(cause error) { c.cancel(true, Canceled, cause) }
}

func withCancel(parent Context) *cancelCtx {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	c := &cancelCtx{}
	c.propagateCancel(parent, c)
	return c
}

func Cause(c Context) error {
	if cc, ok := c.Value(&cancelCtxKey).(*cancelCtx); ok {
		cc.mu.Lock()
		defer cc.mu.Unlock()
		return cc.cause
	}
	return nil
}

func AfterFunc(ctx Context, f func()) (stop func() bool) {
	a := &afterFuncCtx{
		f: f,
	}
	a.cancelCtx.propagateCancel(ctx, a)
	return func() bool {
		stopped := false
		a.once.Do(func() {
			stopped = true
		})
		if stopped {
			a.cancel(true, Canceled, nil)
		}
		return stopped
	}
}

type afterFuncer interface {
	AfterFunc(func()) func() bool
}

type afterFuncCtx struct {
	cancelCtx
	once sync.Once
	f    func()
}

func (a *afterFuncCtx) cancel(removeFromParent bool, err, cause error) {
	a.cancelCtx.cancel(false, err, cause)
	if removeFromParent {
		removeChild(a.Context, a)
	}
	a.once.Do(func() {
		go a.f()
	})
}

type stopCtx struct {
	Context
	stop func() bool
}

var goroutines atomic.Int32

var cancelCtxKey int

func parentCancelCtx(parent Context) (*cancelCtx, bool) {
	done := parent.Done()
	if done == closedchan || done == nil {
		return nil, false
	}
	p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
	if !ok {
		return nil, false
	}
	pdone, _ := p.done.Load().(chan struct{})
	if pdone != done {
		return nil, false
	}
	return p, true
}

func removeChild(parent Context, child canceler) {
	if s, ok := parent.(stopCtx); ok {
		s.stop()
		return
	}
	p, ok := parentCancelCtx(parent)
	if !ok {
		return
	}
	p.mu.Lock()
	if p.children != nil {
		delete(p.children, child)
	}
	p.mu.Unlock()
}

type canceler interface {
	cancel(removeFromParent bool, err, cause error)
	Done() <-chan struct{}
}

var closedchan = make(chan struct{})

func init() {
	close(closedchan)
}

type cancelCtx struct {
	Context

	mu       sync.Mutex            // protects following fields
	done     atomic.Value          // of chan struct{}, created lazily, closed by first cancel call
	children map[canceler]struct{} // set to nil by the first cancel call
	err      error                 // set to non-nil by the first cancel call
	cause    error                 // set to non-nil by the first cancel call
}

func (c *cancelCtx) Value(key any) any {
	if key == &cancelCtxKey {
		return c
	}
	return value(c.Context, key)
}

func (c *cancelCtx) Done() <-chan struct{} {
	d := c.done.Load()
	if d != nil {
		return d.(chan struct{})
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	d = c.done.Load()
	if d == nil {
		d = make(chan struct{})
		c.done.Store(d)
	}
	return d.(chan struct{})
}

func (c *cancelCtx) Err() error {
	c.mu.Lock()
	err := c.err
	c.mu.Unlock()
	return err
}

func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
	c.Context = parent

	done := parent.Done()
	if done == nil {
		return // parent is never canceled
	}

	select {
	case <-done:
		child.cancel(false, parent.Err(), Cause(parent))
		return
	default:
	}

	if p, ok := parentCancelCtx(parent); ok {
		p.mu.Lock()
		if p.err != nil {
			child.cancel(false, p.err, p.cause)
		} else {
			if p.children == nil {
				p.children = make(map[canceler]struct{})
			}
			p.children[child] = struct{}{}
		}
		p.mu.Unlock()
		return
	}

	if a, ok := parent.(afterFuncer); ok {
		c.mu.Lock()
		stop := a.AfterFunc(func() {
			child.cancel(false, parent.Err(), Cause(parent))
		})
		c.Context = stopCtx{
			Context: parent,
			stop:    stop,
		}
		c.mu.Unlock()
		return
	}

	goroutines.Add(1)
	go func() {
		select {
		case <-parent.Done():
			child.cancel(false, parent.Err(), Cause(parent))
		case <-child.Done():
		}
	}()
}

type stringer interface {
	String() string
}

func contextName(c Context) string {
	if s, ok := c.(stringer); ok {
		return s.String()
	}
	return reflectlite.TypeOf(c).String()
}

func (c *cancelCtx) String() string {
	return contextName(c.Context) + ".WithCancel"
}

func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
	if err == nil {
		panic("context: internal error: missing cancel error")
	}
	if cause == nil {
		cause = err
	}
	c.mu.Lock()
	if c.err != nil {
		c.mu.Unlock()
		return // already canceled
	}
	c.err = err
	c.cause = cause
	d, _ := c.done.Load().(chan struct{})
	if d == nil {
		c.done.Store(closedchan)
	} else {
		close(d)
	}
	for child := range c.children {
		child.cancel(false, err, cause)
	}
	c.children = nil
	c.mu.Unlock()

	if removeFromParent {
		removeChild(c.Context, c)
	}
}

func WithoutCancel(parent Context) Context {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	return withoutCancelCtx{parent}
}

type withoutCancelCtx struct {
	c Context
}

func (withoutCancelCtx) Deadline() (deadline time.Time, ok bool) {
	return
}

func (withoutCancelCtx) Done() <-chan struct{} {
	return nil
}

func (withoutCancelCtx) Err() error {
	return nil
}

func (c withoutCancelCtx) Value(key any) any {
	return value(c, key)
}

func (c withoutCancelCtx) String() string {
	return contextName(c.c) + ".WithoutCancel"
}

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
	return WithDeadlineCause(parent, d, nil)
}

func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	if cur, ok := parent.Deadline(); ok && cur.Before(d) {
		// The current deadline is already sooner than the new one.
		return WithCancel(parent)
	}
	c := &timerCtx{
		deadline: d,
	}
	c.cancelCtx.propagateCancel(parent, c)
	dur := time.Until(d)
	if dur <= 0 {
		c.cancel(true, DeadlineExceeded, cause) // deadline has already passed
		return c, func() { c.cancel(false, Canceled, nil) }
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.err == nil {
		c.timer = time.AfterFunc(dur, func() {
			c.cancel(true, DeadlineExceeded, cause)
		})
	}
	return c, func() { c.cancel(true, Canceled, nil) }
}

type timerCtx struct {
	cancelCtx
	timer *time.Timer // Under cancelCtx.mu.

	deadline time.Time
}

func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
	return c.deadline, true
}

func (c *timerCtx) String() string {
	return contextName(c.cancelCtx.Context) + ".WithDeadline(" +
		c.deadline.String() + " [" +
		time.Until(c.deadline).String() + "])"
}

func (c *timerCtx) cancel(removeFromParent bool, err, cause error) {
	c.cancelCtx.cancel(false, err, cause)
	if removeFromParent {
		removeChild(c.cancelCtx.Context, c)
	}
	c.mu.Lock()
	if c.timer != nil {
		c.timer.Stop()
		c.timer = nil
	}
	c.mu.Unlock()
}

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
	return WithDeadline(parent, time.Now().Add(timeout))
}

func WithTimeoutCause(parent Context, timeout time.Duration, cause error) (Context, CancelFunc) {
	return WithDeadlineCause(parent, time.Now().Add(timeout), cause)
}

func WithValue(parent Context, key, val any) Context {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	if key == nil {
		panic("nil key")
	}
	if !reflectlite.TypeOf(key).Comparable() {
		panic("key is not comparable")
	}
	return &valueCtx{parent, key, val}
}

type valueCtx struct {
	Context
	key, val any
}

func stringify(v any) string {
	switch s := v.(type) {
	case stringer:
		return s.String()
	case string:
		return s
	}
	return "<not Stringer>"
}

func (c *valueCtx) String() string {
	return contextName(c.Context) + ".WithValue(type " +
		reflectlite.TypeOf(c.key).String() +
		", val " + stringify(c.val) + ")"
}

func (c *valueCtx) Value(key any) any {
	if c.key == key {
		return c.val
	}
	return value(c.Context, key)
}

func value(c Context, key any) any {
	for {
		switch ctx := c.(type) {
		case *valueCtx:
			if key == ctx.key {
				return ctx.val
			}
			c = ctx.Context
		case *cancelCtx:
			if key == &cancelCtxKey {
				return c
			}
			c = ctx.Context
		case withoutCancelCtx:
			if key == &cancelCtxKey {
				// This implements Cause(ctx) == nil
				// when ctx is created using WithoutCancel.
				return nil
			}
			c = ctx.c
		case *timerCtx:
			if key == &cancelCtxKey {
				return &ctx.cancelCtx
			}
			c = ctx.Context
		case backgroundCtx, todoCtx:
			return nil
		default:
			return c.Value(key)
		}
	}
}

强大的context代码不足500行,是不是很神奇。

2.Context接口

在最开始,就定义了一个Context接口

type Context interface {
	Deadline() (deadline time.Time, ok bool)
	Done() <-chan struct{}
	Err() error
	Value(key any) any
}

(1) Deadline() (deadline time.Time, ok bool)

  • 作用:返回上下文的截止时间。

  • 返回值:

    • deadline 表示截止时间。
    • ok 是一个布尔值,指示是否设置了截止时间。如果没有设置截止时间,okfalse

(2) Done() <-chan struct{}

  • 作用:返回一个chan,如果当前context不能被取消或者没有设置超时,则返回nil;否则返回一个没有缓冲区的chan,如果从chan读取数据则会被阻塞,只有取消函数被执行或者超时到了,才可以从这个chan取到值。一般用于WithCancelWithDeadline创建的context。

(3) Err() error

  • 作用:返回上下文的取消原因。如果上下文尚未取消,返回 nil;如果上下文已取消,返回 CanceledDeadlineExceeded,具体取决于取消原因。跟Done()一样,一般用于WithCancelWithDeadline创建的context。

(4) Value(key any) any

  • 作用:返回与给定键关联的值。通常,上下文值用于传递跨进程和 API 边界的请求范围数据,而不是将可选参数传递给函数。
  • 参数:key 是一个标识上下文值的键。
  • 返回值:返回与指定键关联的值。如果没有值与键关联,则返回 nil。

3.全局变量

  • Canceled:当上下文被取消时,通过调用 Err 方法返回这个错误,标志着上下文的取消状态。程序员可以检查这个错误,以确定操作是否应该终止或采取其他措施。
  • DeadlineExceeded:当上下文的截止时间到达时,通过调用 Err 方法返回这个错误,表示上下文的工作在截止时间内没有完成。这可以用于处理超时情况,例如设置某个操作的最大执行时间。

这两个错误变量主要用于标识上下文的状态,以便程序员在处理上下文取消的情况时能够根据具体的错误类型采取相应的措施。例如,可以使用 Err 方法检查是否为 Canceled 错误,从而决定是否需要终止某个操作。

// 示例代码
if err := ctx.Err(); err != nil {
    switch err {
    case context.Canceled:
        // 处理上下文被取消的情况
    case context.DeadlineExceeded:
        // 处理上下文截止时间过期的情况
    default:
        // 处理其他错误
    }
}

通过这种方式,程序员可以更精确地了解上下文的取消原因,从而更好地控制程序的行为。

  • closedchan:全局chan,并且在init函数中关闭,确保它是一个全局的、已关闭的chan,主要用于在可取消的context中,调用cancel()函数时作为固定值传入cancelCtx.done中
    cancelCtx的定义如下:
    type cancelCtx struct {
    	Context
    
    	mu       sync.Mutex            // protects following fields
    	done     atomic.Value          // of chan struct{}, created lazily, closed by first cancel call
    	children map[canceler]struct{} // set to nil by the first cancel call
    	err      error                 // set to non-nil by the first cancel call
    	cause    error                 // set to non-nil by the first cancel call
    }

cancelCtx.Done()函数定义如下

func (c *cancelCtx) Done() <-chan struct{} {
	d := c.done.Load()
	if d != nil {
		return d.(chan struct{})
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	d = c.done.Load()
	if d == nil {
		d = make(chan struct{})
		c.done.Store(d)
	}
	return d.(chan struct{})
}

Done函数是从done变量中获取一个chan,如果没有则初始化一个。但是,需要注意的是,在执行cancel()函数时是向c.done中写一个chan,如果每次都写一个新的、并且需要关闭的chan,如果cancel是在Done之前执行的,则这个新的、已关闭的chan是不是可以共用?因为这个chan已经关闭了,没有任何作用,不会用于数据传输,仅用于传递关闭这个信号。

基于此共用的目的,定义了一个全局的、已经关闭的chan,如果cancel()在Done()之前执行,则将全局的chan放入c.done即可。

如果是先调用Done()后调用cancel()则不能用这个全局变量,因为Done()被调用的时候还没cancel,需要被阻塞;后面调用cancel()的时候也不能关闭用这个全局变量,而应该关闭c.done里面的chan。代码如下:

func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
	if err == nil {
		panic("context: internal error: missing cancel error")
	}
	if cause == nil {
		cause = err
	}
	c.mu.Lock()
	if c.err != nil {
		c.mu.Unlock()
		return // already canceled
	}
	c.err = err
	c.cause = cause
	d, _ := c.done.Load().(chan struct{})
	if d == nil {
		c.done.Store(closedchan)// 如果d=nil,则说明Done()还没调用,则这里服用全局的chan
	} else {
		close(d) // d!=nil,说明已经调用了Done(),则这里需要close(d)
	}
	for child := range c.children {
		child.cancel(false, err, cause)
	}
	c.children = nil
	c.mu.Unlock()

	if removeFromParent {
		removeChild(c.Context, c)
	}
}

4.全局结构体

4.1两个基础结构体emptyCtxcancelCtx

emptyCtx

emptyCtx 结构体是 context 包中定义的一个结构体,用于表示一个空的上下文。它是 context 包中的两个基础结构体之一。

  1. 无操作: emptyCtx 并不包含任何取消或超时的机制,它是一个基础的、不可取消的上下文。它不提供截止时间、取消信号或其他值,因此其 Deadline 方法返回 (deadline time.Time, ok bool),即截止时间不存在。
  2. 基础上下文: emptyCtx 主要用作上下文链的基础结构,是 backgroundCtxtodoCtx 的共同父类。这两个上下文类型都是可取消的,而 emptyCtx 作为不可取消的上下文,为这两者提供了一种基础结构。
  3. 用于无截止时间和取消需求的场景: 当某个函数或服务并不关心截止时间和取消信号时,可以使用 emptyCtx。例如,在程序初始化、测试、或者一些不需要关注上下文取消状态的场景。

总体来说,emptyCtx 为上下文包提供了一个简单、不可取消的基础,使得可以构建出更复杂的可取消和带有截止时间的上下文类型。

cancelCtx

cancelCtx 结构体是 context 包中定义的一个结构体,用于表示一个可取消的上下文。它是 context 包中的两个基础结构体之一

  1. 可取消的上下文: cancelCtx 表示一个可取消的上下文,它可以通过调用 cancel 方法来取消。取消操作将关闭与该上下文相关联的 Done 通道,以及取消所有衍生(派生)自该上下文的子上下文。
  2. 继承关系: cancelCtx 可以作为其他上下文类型的基础结构,通过嵌套方式,形成上下文链。当一个 cancelCtx 取消时,它会递归取消所有直接或间接派生自它的上下文。
  3. 提供 Done 通道: cancelCtx 包含一个 Done 方法,返回一个通道(<-chan struct{})。该通道在上下文被取消时关闭。通过监听 Done 通道,可以在上下文取消的时候执行相应的操作。
  4. 错误信息: 当上下文被取消时,可以关联一个错误信息。这个错误信息可以通过调用 Err 方法获取。通常情况下,使用预定义的 CanceledDeadlineExceeded 错误,分别表示上下文被取消和截止时间过期。
  5. 父子关系: cancelCtx 与其衍生的子上下文之间有父子关系。当一个 cancelCtx 取消时,它会递归取消其所有子上下文。

总体而言,cancelCtx 结构体提供了上下文的基本取消机制,是构建更复杂上下文类型的基础。

4.2衍生结构体backgroundCtxtodoCtx

backgroundCtx

  • 作用: 表示一个不可取消、没有值和没有截止时间的后台上下文。它是整个上下文链的根节点。
  • 使用场景: 适用于整个应用程序的根上下文,对于不需要取消或截止时间的操作,如初始化和主函数。
  • 特点: 作为根节点,backgroundCtx 的 Done 通道永远不会关闭,而 Err 始终返回 nil。

todoCtx

  • 作用: 表示一个不可取消、没有值和没有截止时间的未定义上下文。类似于 backgroundCtx,但用于表示尚未确定上下文的情况。
  • 使用场景: 在代码尚未被扩展以接受上下文参数的情况下,作为占位符上下文。
  • 特点: 与 backgroundCtx 相似,todoCtx 的 Done 通道永远不会关闭,而 Err 始终返回 nil。

5.全局函数

5.1 func Background() Context

func Background() Context {
    return backgroundCtx{}
}

Background 函数是 context 包提供的一个函数,用于创建一个不可取消、没有值和没有截止时间的后台上下文(backgroundCtx)。这个函数的作用是返回一个根上下文,通常用作整个应用程序的根上下文。

具体作用和特点包括:

  1. 不可取消: 返回的上下文是不可取消的,即调用 WithCancel 函数返回的取消函数对这个上下文无效。
  2. 没有值: 不与任何特定的值关联,没有存储任何数据。
  3. 没有截止时间: 没有设定截止时间,即 Deadline 函数返回的截止时间为零值,并且 Done 通道永远不会关闭。
  4. 用作根上下文: 通常用作整个应用程序的根上下文,对于不需要取消或截止时间的操作,如初始化和主函数。

这个函数的返回值是一个 backgroundCtx 结构体,实现了 Context 接口。这样的根上下文提供了整个应用程序中共享的上下文,对于那些不需要特定上下文的操作,可以使用这个根上下文。

5.1 func TODO() Context

func TODO() Context {
	return todoCtx{}
}

TODO 函数是 context 包提供的另一个函数,用于创建一个不可取消、没有值和没有截止时间的上下文,类似于 Background 函数。返回的上下文通常用于表示某个函数或方法的参数中尚未明确指定的上下文。

具体作用和特点包括:

  1. 不可取消: 返回的上下文是不可取消的,即调用 WithCancel 函数返回的取消函数对这个上下文无效。
  2. 没有值: 不与任何特定的值关联,没有存储任何数据。
  3. 没有截止时间: 没有设定截止时间,即 Deadline 函数返回的截止时间为零值,并且 Done 通道永远不会关闭。
  4. 用于未明确指定上下文的情况: 可以将这个上下文用作那些在函数或方法中需要上下文,但调用者尚未明确提供的情况。

TODO 函数返回的是一个 todoCtx 结构体,实现了 Context 接口。这个上下文通常在编写代码时,暂时需要占位符上下文的场景中使用。

5.3 func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

  • 用法:WithCancel 函数用于基于一个父上下文创建一个新的上下文,并返回一个可用于取消新上下文的 CancelFunc 函数。
  • 使用场景:当某个操作需要在外部条件满足时取消时,可以使用 WithCancel 创建上下文。例如,在处理请求的函数中,可以使用 WithCancel 来创建一个上下文,然后在外部触发某个条件(比如用户取消请求)时调用返回的 CancelFunc 来取消这个上下文。

5.4 func WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc)

  • 用法:WithCancelCause 函数与 WithCancel 类似,但返回的是 CancelCauseFunc,这个函数不仅可以取消上下文,还可以设置取消的原因。
  • 使用场景:当需要在取消的同时记录导致取消的原因时,可以使用 WithCancelCause。这在一些需要追踪取消原因的场景下是有用的,例如在取消上下文时记录导致取消的错误信息,以便在程序中进行调试或记录。

WithCancel 适用于简单的取消场景,而 WithCancelCause 提供了更多的灵活性,可以在取消时附带更多的信息。选择使用哪个取决于你的具体需求。

5.5 func AfterFunc(ctx Context, f func()) (stop func() bool)

  • 用法:AfterFunc传入一个函数f,并且返回一个函数stop func() bool, stop函数是停止当前上下文,它会调用cancel函数,cancel函数会调用f函数,即在stop的时候调用f函数。stop返回bool值,true表示f成功调用cancelf函数;false表示cancelf函数已经调用了,不能重复调用。
  • 使用场景:这个函数通常用于需要在一定时间后执行某些清理或超时处理的场景。例如,你可以使用它来实现在超时时取消一项任务的逻辑,或者在一段时间后执行某个定时任务。

6.valueCtx

这是最重要的一个结构体,它包装了另一个上下文,并在其中存储了一个键值对,以便可以通过 Value 方法检索。直接看定义:

type valueCtx struct {
	Context
	key, val any
}

保存key-value

func WithValue(parent Context, key, val any) Context {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	if key == nil {
		panic("nil key")
	}
	if !reflectlite.TypeOf(key).Comparable() {
		panic("key is not comparable")
	}
	return &valueCtx{parent, key, val}
}

取值

func (c *valueCtx) Value(key any) any {
	if c.key == key {
		return c.val
	}
	return value(c.Context, key)
}

func value(c Context, key any) any {
	for {
		switch ctx := c.(type) {
		case *valueCtx:
			if key == ctx.key {
				return ctx.val
			}
			c = ctx.Context
		case *cancelCtx:
			if key == &cancelCtxKey {
				return c
			}
			c = ctx.Context
		case withoutCancelCtx:
			if key == &cancelCtxKey {
				// This implements Cause(ctx) == nil
				// when ctx is created using WithoutCancel.
				return nil
			}
			c = ctx.c
		case *timerCtx:
			if key == &cancelCtxKey {
				return &ctx.cancelCtx
			}
			c = ctx.Context
		case backgroundCtx, todoCtx:
			return nil
		default:
			return c.Value(key)
		}
	}
}

取值很简单,一层一层往下取,这里用到了for循环

总结

今天学习了一下context不到500行的代码,收获多多。

附:如果我是面试官,我可能会问:closedchan有什么作用?为什么在init中关闭它?


文章作者: Alex
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Alex !
  目录