专注于 JetBrains IDEA 全家桶,永久激活,教程
持续更新 PyCharm,IDEA,WebStorm,PhpStorm,DataGrip,RubyMine,CLion,AppCode 永久激活教程

Golang中的Context你学会了么

context简介

包context定义了Context类型,该类型在API边界之间以及进程之间传递截止日期,取消信号和其他请求范围的值。

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

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

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

  • 不要将上下文存储在结构类型中; 而是将上下文明确传递给需要它的每个函数。 Context应该是第一个参数,通常命名为ctx:
  • 即使函数允许,也不要传递nil Context。 如果不确定使用哪个上下文,请传递context.TODO。
  • 仅将上下文值用于传递流程和API的请求范围的数据,而不用于将可选参数传递给函数。

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

// A Context carries a deadline, cancelation signal, and request-scoped values
// across API boundaries. Its methods are safe for simultaneous use by multiple
// goroutines.
type Context interface {
    // Done returns a channel that is closed when this Context is canceled
    // or times out.
    Done() <-chan struct{}

    // Err indicates why this context was canceled, after the Done channel
    // is closed.
    Err() error

    // Deadline returns the time when this Context will be canceled, if any.
    Deadline() (deadline time.Time, ok bool)

    // Value returns the value associated with key or nil if none.
    Value(key interface{}) interface{}
}

Done方法返回一个管道,作为代表上下文运行的函数的取消信号:当通道关闭时,函数应该放弃它们的工作并返回。Err方法返回一个错误,指示为什么取消上下文。

一个Context并没有Cancel方法,因为Done管道是一个只能接受的管道。函数接受一个取消信号。特别地,当一个父操作启动goroutine为了子操作, 这些子操作不应该有能力取消父操作。取代的,WithCancel函数提供了一个方式取消一个新的Context.

Deadline方法允许函数决定他们是否应该启动工作。如果时间较短,就不值得。 Value方法允许一个Context传递请求域的数据,数据必须是多goroutine安全的。

派生context

上下文包提供了从现有值派生新的Context值的功能。这些值形成一棵树:取消上下文时,从该上下文派生的所有上下文也会被取消。

context.Background()是任何上下文树的根; 它永远不会被取消

WithCancel和WithTimeout返回派生的Context值,该值可以比父Context早被取消。请求处理程序返回时,通常会取消与传入请求关联的上下文。使用多个副本时,WithCancel对于取消冗余请求也很有用。WithTimeout对于设置对后端服务器的请求的截止日期很有用:

// WithCancel returns a copy of parent whose Done channel is closed as soon as
// parent.Done is closed or cancel is called.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

// A CancelFunc cancels a Context.
type CancelFunc func()

// WithTimeout returns a copy of parent whose Done channel is closed as soon as
// parent.Done is closed, cancel is called, or timeout elapses. The new
// Context's Deadline is the sooner of now+timeout and the parent's deadline, if
// any. If the timer is still running, the cancel function releases its
// resources.
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

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

// WithValue returns a copy of parent whose Value method returns val for key.
func WithValue(parent Context, key interface{}, val interface{}) Context

源码中context的使用案例

根据他database/sql源码中的Ping()方法来学习一下context.Context的使用。

首先,Ping()方法的原型如下:

// Ping verifies a connection to the database is still alive,
// establishing a connection if necessary.
func (db *DB) Ping() error {
    return db.PingContext(context.Background())
}

PingContext接受一个上下文作为参数,进行验证一个数据库连接是否还在存活。如果需要建立一个新的连接

// PingContext verifies a connection to the database is still alive,
// establishing a connection if necessary.
func (db *DB) PingContext(ctx context.Context) error {
    var dc *driverConn
    var err error

    for i := 0; i < maxBadConnRetries; i++ {
        dc, err = db.conn(ctx, cachedOrNewConn)
        if err != driver.ErrBadConn {
            break
        }
    }
    if err == driver.ErrBadConn {
        dc, err = db.conn(ctx, alwaysNewConn)
    }
    if err != nil {
        return err
    }

    return db.pingDC(ctx, dc, dc.releaseConn)
}

conn返回一个新的连接或者缓存的连接。如果连接复用策略是cachedOrNewConn,那么如果在空闲连接够用的情况下,会使用空闲连接列表中第一个连接作为返回值返回。

// conn returns a newly-opened or cached *driverConn.
func (db *DB) conn(ctx context.Context, strategy connReuseStrategy) (*driverConn, error) {
    db.mu.Lock()
    if db.closed {
        db.mu.Unlock()
        return nil, errDBClosed
    }
    // Check if the context is expired.
    select {
    default:
    case <-ctx.Done():
        db.mu.Unlock()
        return nil, ctx.Err()
    }
    lifetime := db.maxLifetime

    // Prefer a free connection, if possible.
    numFree := len(db.freeConn)
    if strategy == cachedOrNewConn && numFree > 0 {
        conn := db.freeConn[0]
        copy(db.freeConn, db.freeConn[1:])
        db.freeConn = db.freeConn[:numFree-1]
        conn.inUse = true
        db.mu.Unlock()
        if conn.expired(lifetime) {
            conn.Close()
            return nil, driver.ErrBadConn
        }
        // Lock around reading lastErr to ensure the session resetter finished.
        conn.Lock()
        err := conn.lastErr
        conn.Unlock()
        if err == driver.ErrBadConn {
            conn.Close()
            return nil, driver.ErrBadConn
        }
        return conn, nil
    }

    // Out of free connections or we were asked not to use one. If we're not
    // allowed to open any more connections, make a request and wait.
    if db.maxOpen > 0 && db.numOpen >= db.maxOpen {
        // Make the connRequest channel. It's buffered so that the
        // connectionOpener doesn't block while waiting for the req to be read.
        req := make(chan connRequest, 1)
        reqKey := db.nextRequestKeyLocked()
        db.connRequests[reqKey] = req
        db.waitCount++
        db.mu.Unlock()

        waitStart := time.Now()

        // Timeout the connection request with the context.
        select {
        case <-ctx.Done():
            // Remove the connection request and ensure no value has been sent
            // on it after removing.
            db.mu.Lock()
            delete(db.connRequests, reqKey)
            db.mu.Unlock()

            atomic.AddInt64(&db.waitDuration, int64(time.Since(waitStart)))

            select {
            default:
            case ret, ok := <-req:
                if ok && ret.conn != nil {
                    db.putConn(ret.conn, ret.err, false)
                }
            }
            return nil, ctx.Err()
        case ret, ok := <-req:
            atomic.AddInt64(&db.waitDuration, int64(time.Since(waitStart)))

            if !ok {
                return nil, errDBClosed
            }
            if ret.err == nil && ret.conn.expired(lifetime) {
                ret.conn.Close()
                return nil, driver.ErrBadConn
            }
            if ret.conn == nil {
                return nil, ret.err
            }
            // Lock around reading lastErr to ensure the session resetter finished.
            ret.conn.Lock()
            err := ret.conn.lastErr
            ret.conn.Unlock()
            if err == driver.ErrBadConn {
                ret.conn.Close()
                return nil, driver.ErrBadConn
            }
            return ret.conn, ret.err
        }
    }

    db.numOpen++ // optimistically
    db.mu.Unlock()
    ci, err := db.connector.Connect(ctx)
    if err != nil {
        db.mu.Lock()
        db.numOpen-- // correct for earlier optimism
        db.maybeOpenNewConnections()
        db.mu.Unlock()
        return nil, err
    }
    db.mu.Lock()
    dc := &driverConn{
        db:        db,
        createdAt: nowFunc(),
        ci:        ci,
        inUse:     true,
    }
    db.addDepLocked(dc, dc)
    db.mu.Unlock()
    return dc, nil
}

现在PingContext获取到一个连接之后就要进行真正的pingDC操作了,该函数的第一个参数就是上下文参数,符合上下文使用的规范。 第三个参数是一个函数类型,先来看看pingDC操作,稍后再具体看看这个释放功能的函数。pingDC函数,从驱动连接中获取Pinger。 然后调用释放函数。

func (db *DB) pingDC(ctx context.Context, dc *driverConn, release func(error)) error {
    var err error
    if pinger, ok := dc.ci.(driver.Pinger); ok {
        withLock(dc, func() {
            err = pinger.Ping(ctx)
        })
    }
    release(err)
    return err
}

看一下释放函数

func (dc *driverConn) releaseConn(err error) {
    dc.db.putConn(dc, err, true)
}

因为重点不在该函数,所以简单描述一下这个函数的功能就是将连接放回到空闲连接中,供后续的数据库连接使用。

具体看看PingDC中真正执行ping操作的逻辑

withLock是一个闭包,在核心函数调用上加锁。PingDC函数中使用的就是第一个参数结构体上的锁。来保护第二个参数,一个函数类型的参数调用。该函数进行具体的ping操作。

Pinger是一个接口类型,只有一个Ping方法,该方法仅有一个参数,就是上下文参数。而这个接口类型是一个可选的接口由Conn实现。 如果Conn并没有实现Pinger,sql包中的DB.Ping和DB.PingContext将检测是否有一个Conn可用。

// Pinger is an optional interface that may be implemented by a Conn.
//
// If a Conn does not implement Pinger, the sql package's DB.Ping and
// DB.PingContext will check if there is at least one Conn available.
//
// If Conn.Ping returns ErrBadConn, DB.Ping and DB.PingContext will remove
// the Conn from pool.
type Pinger interface {
    Ping(ctx context.Context) error
}

如果Conn.Ping返回ErrBadConn,DB.PingDB.PingContext将移除连接池中的Conn.直到检测到是否有一个Conn可用。

看完了数据库中的Ping功能,感觉是不是和你想象中的不太一样啊,我也是….

文章永久链接:https://tech.souyunku.com/41004

未经允许不得转载:搜云库技术团队 » Golang中的Context你学会了么

JetBrains 全家桶,激活、破解、教程

提供 JetBrains 全家桶激活码、注册码、破解补丁下载及详细激活教程,支持 IntelliJ IDEA、PyCharm、WebStorm 等工具的永久激活。无论是破解教程,还是最新激活码,均可免费获得,帮助开发者解决常见激活问题,确保轻松破解并快速使用 JetBrains 软件。获取免费的破解补丁和激活码,快速解决激活难题,全面覆盖 2024/2025 版本!

联系我们联系我们