Golang Context

·

3 min read

What is golang context

Context is used to pass request-scope values, and control timeout, deadline and cancel of sub goroutines in a request.

Common use cases.

  • Pass value through context.
  • Set timeout or deadline to context
  • Cancel context, and stop run sub goroutines.
import (
    "context"
    "fmt"
    "log"
    "strings"
    "time"

    "github.com/parnurzeal/gorequest"
)

type ctxKeyTp struct{}

var ctxKey = ctxKeyTp{}

type result struct {
    content string
    err     error
}

func CrawlWeb(ctx context.Context, url string) (*result, error) {
    ctxValue, ok := ctx.Value(ctxKey).(string)
    log.Printf("get value from context(%v): %s, %t\n", ctxKey, ctxValue, ok)

    var c = make(chan result, 1)
    go func() {
        now := time.Now()
        request := gorequest.New()
        _, body, errs := request.Get(url).End()
        if len(errs) > 0 {
            var errStr []string
            for _, e := range errs {
                errStr = append(errStr, e.Error())
            }

            log.Printf("get error: %v, at: %v\n", errStr, time.Since(now))
            c <- result{err: fmt.Errorf(strings.Join(errStr, ","))}
            return
        }
        log.Printf("get result: %s, at: %v\n", body, time.Since(now))
        c <- result{content: body}
    }()

    select {
    // when context canceled, timeout or deadline, then done channel will be closed.
    // returns a channel that's closed when work done
    case <-ctx.Done():
        // Err() if Done is closed, then Err returns non nil error of the reason(canceled, deadlined)
        ctxErr := ctx.Err()
        log.Printf("ctx done, error: %v\n", ctxErr)
        return nil, ctxErr
    case res := <-c:
        return &res, nil
    }
}

Context timeout

WithTimeout arranges for Done to be closed when the timeout elapses.

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
    return WithDeadline(parent, time.Now().Add(timeout))
}
// get value from context({}): , false
// ctx done, error: context deadline exceeded
// since:  502.230142ms
func TestCrawlWebTimeout(t *testing.T) {
    timeOut := 500 * time.Millisecond
    // context.WithTimeout returns a copy of parent context with timout
    ctx, cancel := context.WithTimeout(context.Background(), timeOut)
    defer cancel()

    start := time.Now()
    res, err := CrawlWeb(ctx, "https://blog.golang.org/context")
    fmt.Println("since: ", time.Since(start))
    assert.NotNil(t, err)

    // After 500ms, CrawlWeb don't finish, So returns ctx.Err
    assert.Equal(t, context.DeadlineExceeded, err)
    assert.Nil(t, res)
}

Context Deadline

WithDeadline arranges for Done to be closed when the deadline expires. returns copy of parent context with the deadline adjusted to be no later then d.

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

Deadline Context Also returns context.DeadlineExceeded error same with WithTimeout

// ctx done, error: context deadline exceeded
// res:  <nil>
// err:  context deadline exceeded
func TestCrawlWebWithDeadline(t *testing.T) {
    timeOut := 500 * time.Millisecond
    ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(timeOut))
    defer cancel()

    res, err := CrawlWeb(ctx, "https://blog.golang.org/context")
    log.Println("res: ", res)
    log.Println("err: ", err)
    assert.NotNil(t, err)
    assert.Equal(t, context.DeadlineExceeded, err)
    assert.Nil(t, res)
}

Cancel context

  • WithCancel returns a copy of parent with a new Done channel.
  • When call returned cancel func, then the Done channel will be closed. And context.Err() returns context.Canceled error.
// 500ms reached, cancel this request
// ctx done, error: context canceled
// res:  <nil> , error:  context canceled
func TestCrawlWebCancel(t *testing.T) {
    type crawlResult struct {
        res *result
        err error
    }
    var c = make(chan crawlResult, 1)
    // if the result not returned in 1 second will cancel this.
    timer := time.NewTimer(500 * time.Millisecond)
    defer timer.Stop()

    ctx, cancel := context.WithCancel(context.Background())
    go func() {
        res, err := CrawlWeb(ctx, "https://blog.golang.org/context")
        log.Println("res: ", res, ", error: ", err)
        assert.Equal(t, context.Canceled, err)
        c <- crawlResult{
            res: res,
            err: err,
        }
    }()
    select {
    case <-timer.C: // reached 500ms, but not get result. cancel this request.
        log.Println("500ms reached, cancel this request")
        cancel()
    case res := <-c:
        log.Println("get result in 500ms, great!")
        assert.Nil(t, res.err)
        assert.NotNil(t, res.res)
    }
}

Pass value through context.

// get value from context({}): Hello,, true
func TestPassValueThroughContext(t *testing.T) {
    ctx := context.WithValue(context.Background(), ctxKey, "Hello,")
    res, err := CrawlWeb(ctx, "https://blog.golang.org/context")
    assert.Nil(t, err)
    assert.NotNil(t, res)
}

Receive context Value from context.

ctxValue, ok := ctx.Value(ctxKey).(string)
log.Printf("get value from context(%v): %s, %t\n", ctxKey, ctxValue, ok)