首页 > 其他分享 >标准库之context(很重要)

标准库之context(很重要)

时间:2024-03-14 16:11:45浏览次数:25  
标签:重要 context fmt goroutine 标准 Context time 上下文

目录

标准库之context

  • 在 Go的http包的Server端中,每一个请求在都有一个对应的 goroutine 去处理。请求处理函数通常会启动额外的 goroutine 用来访问后端服务,比如数据库和RPC服务。用来处理一个请求的 goroutine 通常需要访问一些与请求特定的数据,比如终端用户的身份认证信息、验证相关的token、请求的截止时间。 当一个请求被取消或超时时,所有用来处理该请求的 goroutine 都应该迅速退出,然后系统才能释放这些 goroutine 占用的资源。

一、为什么需要context

  • 可以先挨个看看下面的几个并发示例的不足之处

1. 基本并发的示例

  • 本例中,我们只能老老实实的等待子 goroutine 完成后,才能继续主 goroutine 的执行。无法手动去结束子 goroutine
package main

import (
	"fmt"
	"sync"
	"time"
)

var wg sync.WaitGroup

// 初始的例子

func worker() {
	for {
		fmt.Println("worker")
		time.Sleep(time.Second)
	}
	// 如何接收外部命令实现退出
	wg.Done()
}

func main() {
	wg.Add(1)
	go worker()
	// 如何优雅的实现结束子goroutine
	wg.Wait()
	fmt.Println("over")
}

2. 全局变量方式的并发

  • 使用一个全局变量,当该全局变量作为参数进入多个 goroutine的任务中时,或者跨包时就变得不好控制了。
  • 同样,还存在上面基本并发中无法手动控制子 goroutine的退出
package main

import (
"fmt"
"sync"
"time"
)

var wg sync.WaitGroup
var exit bool

// 全局变量方式存在的问题:
// 1. 使用全局变量在跨包调用时不容易统一
// 2. 如果worker中再启动goroutine,就不太好控制了。

func worker() {
	for {
		fmt.Println("worker")
		time.Sleep(time.Second)
		if exit {
			break
		}
	}
	wg.Done()
}

func main() {
	wg.Add(1)
	go worker()
	time.Sleep(time.Second * 3) // sleep3秒以免程序过快退出
	exit = true                 // 修改全局变量实现子goroutine的退出
	wg.Wait()
	fmt.Println("over")
}

3. 通道方式的并发

  • 采用channel作为多个 goroutine之间的数据交互的媒介,是实现了数据安全的交互,但道理同上面的全局变量一样,当跨包时,或者子 goroutine中继续启动 goroutine时,需要维护一个共用的channel,也不太友好。
  • 同样,还存在上面基本并发中无法手动控制子 goroutine的退出
package main

import (
"fmt"
"sync"
"time"
)

var wg sync.WaitGroup

// 管道方式存在的问题:
// 1. 使用全局变量在跨包调用时不容易实现规范和统一,需要维护一个共用的channel

func worker(exitChan chan struct{}) {
LOOP:
	for {
		fmt.Println("worker")
		time.Sleep(time.Second)
		select {
		case <-exitChan: // 等待接收上级通知
			break LOOP
		default:
		}
	}
	wg.Done()
}

func main() {
	var exitChan = make(chan struct{})
	wg.Add(1)
	go worker(exitChan)
	time.Sleep(time.Second * 3) // sleep3秒以免程序过快退出
	exitChan <- struct{}{}      // 给子goroutine发送退出信号
	close(exitChan)
	wg.Wait()
	fmt.Println("over")
}

4. 官方版的方案

  • 下面是官方给出的解决方案的参考案例:使用context包进行上下文的管理
package main

import (
"context"
"fmt"
"sync"
"time"
)

var wg sync.WaitGroup

func worker(ctx context.Context) {
LOOP:  // 定义一个待结束的标签
	for {
		fmt.Println("worker")
		time.Sleep(time.Second)
		select {
		case <-ctx.Done():  // 等待上级通知
			break LOOP
		default:
		}
	}
	wg.Done()
}

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	wg.Add(1)
	go worker(ctx)
	time.Sleep(time.Second * 3)
	cancel()  // 通知子goroutine结束
	wg.Wait()
	fmt.Println("over")
}
  • 当子goroutine又开启另外一个goroutine时,只需要将ctx传入即可:
package main

import (
	"context"
	"fmt"
	"sync"
	"time"
)

var wg sync.WaitGroup

func worker(ctx context.Context) {
	go worker2(ctx)
LOOP:  // 定义一个待结束的标签
	for {
		fmt.Println("worker")
		time.Sleep(time.Second)
		select {
		case <-ctx.Done(): // 等待上级通知
			break LOOP
		default:
		}
	}
	wg.Done()
}

func worker2(ctx context.Context) {
LOOP:
	for {
		fmt.Println("worker2")
		time.Sleep(time.Second)
		select {
		case <-ctx.Done(): // 等待上级通知
			break LOOP
		default:
		}
	}
}
func main() {
	ctx, cancel := context.WithCancel(context.Background())
	wg.Add(1)
	go worker(ctx)
	time.Sleep(time.Second * 3)
	cancel() // 通知子goroutine结束
	wg.Wait()
	fmt.Println("over")
}

二、Context初识

  • Go1.7版本加入了一个新的标准库context,它定义了Context类型,专门用来简化 对于处理单个请求的多个 goroutine 之间与请求域的数据、取消信号、截止时间等相关操作,这些操作可能涉及多个 API 调用。

  • 对服务器传入的请求应该创建上下文,而对服务器的传出调用应该接受上下文。它们之间的函数调用链必须传递上下文,或者可以使用WithCancelWithDeadlineWithTimeoutWithValue创建的派生上下文。当一个上下文被取消时,它派生的所有上下文也被取消。

1. Context接口

  • context.Context是一个接口,该接口定义了四个需要实现的方法。具体签名如下:
type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}

其中:

  • Deadline 方法需要返回当前Context被取消的时间,也就是完成工作的截止时间(deadline)
  • Done 方法需要返回一个 Channel,这个Channel会在当前工作完成或者上下文被取消之后关闭,多次调用Done 方法会返回同一个 Channel
  • Err 方法会返回当前 Context 结束的原因,它只会在 Done 返回的Channel被关闭时才会返回非空的值
    • 如果当前Context被取消就会返回Canceled错误
    • 如果当前Context超时就会返回DeadlineExceeded错误
  • Value方法会从Context中返回键对应的值,对于同一个上下文来说,多次调用Value 并传入相同的Key会返回相同的结果,该方法仅用于传递跨API和进程间跟请求域的数据

2. 两个顶级Context

  • context包提供两种顶级的上下文类型 Background()TODO(),这两个内置的上下文对象作为最顶层的partent context,衍生出更多的子上下文对象

(1)Background()和TODO()

  • context.Background()主要用于main函数、初始化以及测试代码中,作为Context这个树结构的最顶层的Context,也就是根Context。

  • context.TODO() 返回非零的空上下文。当不清楚要使用哪个上下文或者它还不可用时,应该使用context.TODO()

(2)区别

  • 本质上都是emptyCtx结构体类型,其源码实现是一样的,是一个不可取消,没有设置截止时间,没有携带任何值的Context。只不过使用场景不同,context.Background()通常由主函数、初始化和测试使用,是顶级Context;context.TODO()通常用于主协程外的其他协程向下传递,分析工具可识别它在调用栈中传播

3. 派生Context(With系列函数)

  • 此外,context包中还定义了四个With系列函数。

(1)WithCancel(可取消context)

  • 用于创建一个具有取消功能的上下文(context)。它的主要目的是允许你在需要的时候取消一个长时间运行的任务或多个任务,以确保程序可以优雅地退出或处理超时情况

  • WithCancel的函数定义如下:

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

- context.WithCancel 接受一个父上下文(parent),并返回一个新的上下文 ctx 和一个 cancel 函数。

- ctx 是一个新创建的上下文,它会继承父上下文的截止时间(deadline)、值(values)、取消函数等属性。

- cancel 是一个函数,当调用 cancel 函数时,它会取消 ctx 及其派生的所有子上下文,导致所有与这些上下文相关的操作都会尽早退出
  • WithCancel返回带有新Done通道的父节点的副本。当调用返回的cancel函数或当关闭父上下文的Done通道时,将关闭返回上下文的Done通道,无论先发生哪种情况

  • 取消此上下文将释放与其关联的资源,因此代码应该在此上下文中运行的操作完成后立即调用cancel。

i. context控制goroutine内部的goroutine的停止

// 示例: 通过context控制goroutine内部的goroutine的停止
package main

import (
	"context"
	"fmt"
	"time"
)

func gen(ctx context.Context) <-chan int {
		dst := make(chan int)
		n := 1
		go func() {
			for {
				select {
				case <-ctx.Done():
					return // return结束该goroutine,防止泄露
				case dst <- n:
					n++
				}
			}
		}()
		return dst
	}

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel() // 当我们取完需要的整数后调用cancel
 
	for n := range gen(ctx) {
		fmt.Println(n)
		if n == 5 {
			break
		}
	}
}

/*
上面的示例代码中,`gen`函数在单独的`goroutine`中生成整数并将它们发送到返回的通道。 `gen`的调用者在使用生成的整数之后需要取消上下文,以免在`gen`内部启动的`goroutine`发生泄漏。
*/

ii. context控制多个goroutine的停止

package main

import (
	"context"
	"fmt"
	"time"
)

func task(ctx context.Context, s string) {
hsw:
	for {
		select {
		case <-ctx.Done():
			fmt.Println("task:我收到取消指令,我结束了")
			break hsw  // 结束掉 label位置的循环
		default:
			fmt.Println("打印一次传入的值:", s)
			time.Sleep(1 * time.Second)
		}
	}
}
func main() {
	parent := context.Background()
	ctx, cancle := context.WithCancel(parent)
	go task(ctx, "hsw is Nb")
	go task(ctx, "hsw is handsome")
	time.Sleep(5 * time.Second) // 睡个5s钟,发现上面两句话不停打印
	cancle()  // 通过ctx控制,上面两个go协程关闭
	time.Sleep(5 * time.Second)  // 睡个5s钟,发现确实被停止了,不打印了

}

(2)WithDeadline(超时取消context)

  • WithDeadline 用于创建一个到达指定时间后能被自动取消的上下文

  • WithDeadline的函数签名如下:

func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
  • WithDeadline函数返回父上下文的副本,其截止时间调整为不迟于d。如果父上下文的截止时间早于d,则WithDeadline(Parent,d)在语义上等同于父上下文。

  • 当截止时间到期、调用返回的cancel函数或关闭父上下文的done通道(以先发生者为准)时,返回的上下文的done通道将关闭(即当设定的延迟时间到了之后,或者未到延迟时间时主动调用cancel函数,或关闭父上下文的done通道,就关闭上下文,以最先发生的情况为准)。

  • 取消此上下文将释放与其关联的资源,因此代码应该在此上下文中运行的操作完成后立即调用cancel

1. 示例1
package main

import (
	"context"
	"fmt"
	"time"
)

func main() {
	d := time.Now().Add(50 * time.Millisecond)
	ctx, cancel := context.WithDeadline(context.Background(), d)

	// 尽管ctx会过期,但在任何情况下调用它的cancel函数都是很好的实践。
	// 如果不这样做,可能会使上下文及其父类存活的时间超过必要的时间。
	defer cancel()

	select {
	case <-time.After(1 * time.Second):
		fmt.Println(111, "overslept")
	case <-ctx.Done():
		fmt.Println(222, ctx.Err())
	}
}

/*
分析:上面的代码中,定义了一个50毫秒之后过期的deadline,然后我们调用`context.WithDeadline(context.Background(), d)`得到一个上下文(ctx)和一个取消函数(cancel),然后使用一个select让主程序陷入等待:等待1秒后打印`overslept`退出或者等待ctx过期后退出。 因为ctx50秒后就过期,所以`ctx.Done()`会先接收到值,上面的代码会打印ctx.Err()取消原因。
*/

2. 示例2(官方使用示例)
// 这个例子传递一个具有任意截止时间的上下文,告诉一个阻塞函数一旦到达它就应该放弃它的工作
package main

import (
	"context"
	"fmt"
	"time"
)

func task(ctx context.Context, s string) {
lqz:
	for {
		select {
		case <-ctx.Done():
			fmt.Println("task:我收到取消指令,我结束了")
			fmt.Println(ctx.Err())
			// 正常到时间:context deadline exceeded
			// 手动调用cancel :context canceled

			break lqz // 结束掉 label位置的循环

		default:
			fmt.Println("打印一次传入的值:", s)
			time.Sleep(1 * time.Second)

		}
	}
}
func task2(ctx context.Context, s string) {
lqz:
	for {
		select {
		case <-ctx.Done():
			fmt.Println("task:我收到取消指令,我结束了")
			fmt.Println(ctx.Err())
			// 正常到时间:context deadline exceeded
			// 手动调用cancel :context canceled

			break lqz // 结束掉 label位置的循环
		case <-time.After(1 * time.Second):
			fmt.Println("1s时间到了,打印:",s)
			fmt.Println(ctx.Err()) // 执行到此,如果还没到结束时间,Err为nil
		}
	}
}
func main() {
	// 1 正常到时间
	//parent := context.Background()
	//t:=time.Now().Add(5*time.Second) // 5s后的时间
	//ctx, _ := context.WithDeadline(parent,t)
	//go task(ctx, "lqz is Nb")
	//time.Sleep(10 * time.Second) // 睡个10s钟,由于5s结束,后5s没有输出

	// 2 手动调用cancle取消
	//parent := context.Background()
	//t := time.Now().Add(5 * time.Second) // 5s后的时间
	//ctx, cancel := context.WithDeadline(parent, t)
	//go task(ctx, "lqz is Nb")
	//time.Sleep(3 * time.Second) // 睡个3s钟,由于5s还没到,手动结束
	//cancel()
	//time.Sleep(7 * time.Second) // 再睡7s看输出

	//3 1s后输出一次内容的另一种写法
	parent := context.Background()
	t := time.Now().Add(5 * time.Second) // 5s后的时间
	ctx, cancel := context.WithDeadline(parent, t)
	go task2(ctx, "lqz is Nb")
	time.Sleep(3 * time.Second) // 睡个3s钟,由于5s还没到,手动结束
	cancel()
	time.Sleep(7 * time.Second) // 再睡7s看输出

}

(3)WithTimeout(超时取消context)

  • WithTimeout 方法用于创建一个经过一段时间后能被自动取消的上下文,他也是调用 WithDeadline 这个方法

  • WithTimeout的函数签名如下:

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
  • WithTimeout返回WithDeadline(parent, time.Now().Add(timeout))

  • 取消此上下文将释放与其相关的资源,因此代码应该在此上下文中运行的操作完成后立即调用cancel,通常用于数据库或者网络连接的超时控制。具体示例如下:

1. 示例1
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")
}

2. 示例2
// 这个例子传递一个带有超时的上下文,告诉一个阻塞函数它应该在超时结束后放弃它的工作
package main

import (
	"context"
	"fmt"
	"time"
)

func task(ctx context.Context) {
	select {
	case <-ctx.Done():
		fmt.Println("task:我结束了")
		// cancle函数取消会打印context canceled
		// 到时间取消会打印:context deadline exceeded
		fmt.Println(ctx.Err())
	case <-time.After(1 * time.Second):
		fmt.Println("1s时间到了")
		fmt.Println(ctx.Err()) // 执行到此,如果还没到结束时间,Err为nil

	}

}

func main() {
	//ctx, cancle := context.WithTimeout(context.Background(), 1*time.Second) // 打印
	ctx, cancle := context.WithTimeout(context.Background(), 2*time.Second)
	go task(ctx)

	time.Sleep(3*time.Second)
	cancle()
	time.Sleep(3*time.Second)


}

(4)WithValue(向context添加值)

  • WithValue函数能够将请求作用域的数据与 Context 对象建立关系。声明如下:
func WithValue(parent Context, key, val interface{}) Context
  • WithValue返回父节点的副本,其中与key关联的值为val。

  • 仅对API和进程间传递请求域的数据使用上下文值,而不是使用它来传递可选参数给函数。

  • 所提供的键必须是可比较的,并且不应该是string类型或任何其他内置类型,以避免使用上下文在包之间发生冲突。WithValue的用户应该为键定义自己的类型。为了避免在分配给interface{}时进行分配,上下文键通常具有具体类型struct{}。或者,导出的上下文关键变量的静态类型应该是指针或接口。

package main

import (
"context"
"fmt"
"sync"
"time"
)

type TraceCode string

var wg sync.WaitGroup

func worker(ctx context.Context) {
	key := TraceCode("TRACE_CODE")
	traceCode, ok := ctx.Value(key).(string) // 在子goroutine中获取trace code
	if !ok {
		fmt.Println("invalid trace code")
	}
LOOP:
	for {
		fmt.Printf("worker, trace code:%s\n", traceCode)
		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)
	// 在系统的入口中设置trace code传递给后续启动的goroutine实现日志数据聚合
	ctx = context.WithValue(ctx, TraceCode("TRACE_CODE"), "12512312234")
	wg.Add(1)
	go worker(ctx)
	time.Sleep(time.Second * 5)
	cancel() // 通知子goroutine结束
	wg.Wait()
	fmt.Println("over")
}

三、使用Context的注意事项

  • 推荐以参数的方式显示传递Context
  • 以Context作为参数的函数方法,应该把Context作为第一个参数。
  • 给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO()
  • Context的Value相关方法应该传递请求域的必要数据,不应该用于传递可选参数
  • Context是线程安全的,可以放心的在多个goroutine中传递

四、Context常见示例

1. 控制10s后,所有协程退出

  • 使用context包来实现线程安全退出或超时的控制:控制10s后,所有协程退出
package main

import (
	"context"
	"fmt"
	"strconv"
	"sync"
	"time"
)

func task(ctx context.Context, s string, wg *sync.WaitGroup) {
	defer wg.Done()
	for {
		select {
		case <-ctx.Done():
			fmt.Println(s, "--->我结束了")
			//fmt.Println(ctx.Err())
			return
		default:
			fmt.Println(s)
			time.Sleep(1 * time.Second)

		}
	}

}

func main() {
	var wg sync.WaitGroup
	ctx, _ := context.WithTimeout(context.Background(), 10*time.Second)
	for i := 0; i < 10; i++ {
		wg.Add(1)
		s := fmt.Sprintf("我是第:%v 个任务", strconv.Itoa(i))
		go task(ctx, s, &wg)
	}
	wg.Wait()

}

// 当并发体超时或`main`主动停止工作者Goroutine时,每个工作者都可以安全退出

2. 控制某个go协程执行5次就结束

// 控制goroutine 执行5次结束
func main() {
	// 定义一个运行次数变量
	runCount := 0
	//定义一个waitgroup,等待goroutine执行完成
	var wg sync.WaitGroup
	// 初始化context
	parent := context.Background()
	// 传入初始化的ctx,返回ctx和cancle函数
	ctx, cancle := context.WithCancel(parent)
	wg.Add(1) // 增加一个任务
	go func() {
		for {
			select {
			case <-ctx.Done():
				fmt.Println("任务结束")
				return
			default:
				fmt.Printf("任务执行了%d次\n", runCount)
				runCount++
			}
			// 执行了5次,使用ctx的取消函数将任务取消
			if runCount >= 5 {
				cancle()
				wg.Done() // goroutine执行完成
			}

		}
	}()

	wg.Wait() //等待所有任务完成

}

3. 打印100个素数

  • Go语言是带内存自动回收特性的,因此内存一般不会泄漏。当main函数不再使用管道时后台Goroutine有泄漏的风险。我们可以通过context包来避免这个问题,下面是防止内存泄露的素数筛实现:
// 返回生成自然数序列的管道: 2, 3, 4, ...
func GenerateNatural(ctx context.Context) chan int {
    ch := make(chan int)
    go func() {
        for i := 2; ; i++ {
            select {
            //父协程cancel()时安全退出该子协程
            case <- ctx.Done():
                return
            //生成的素数发送到管道
            case ch <- i:
            }
        }
    }()
    return ch
}

// 管道过滤器: 删除能被素数整除的数
func PrimeFilter(ctx context.Context, in <-chan int, prime int) chan int {
    out := make(chan int)
    go func() {
        for {
            if i := <-in; i%prime != 0 {
                select {
                //父协程cancel()时安全退出该子协程
                case <- ctx.Done():
                    return
                case out <- i:
                }
            }
        }
    }()
    return out
}

func main() {
    // 使用一个可由父协程控制子协程安全退出的Context。
    ctx, cancel := context.WithCancel(context.Background())

    ch := GenerateNatural(ctx) // 自然数序列: 2, 3, 4, ...
    
    for i := 0; i < 100; i++ {
        // 新出现的素数打印出来
        prime := <-ch 
        fmt.Printf("%v: %v\n", i+1, prime)
        // 基于新素数构造的过滤器
        ch = PrimeFilter(ctx, ch, prime) 
    }
    
    //输出100以内符合要求的素数后安全退出所有子协程
    cancel()
}

// 当main函数完成工作前,通过调用`cancel()`来通知后台Goroutine退出,这样就避免了Goroutine的泄漏

五、客户端超时取消示例

  • 调用服务端API时如何在客户端实现超时控制?

1. server端

// context_timeout/server/main.go
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(":8000", nil)
	if err != nil {
		panic(err)
	}
}

2. client端

// context_timeout/client/main.go
package main

import (
	"context"
	"fmt"
	"io/ioutil"
	"net/http"
	"sync"
	"time"
)

// 客户端

type respData struct {
	resp *http.Response
	err  error
}

func doCall(ctx context.Context) {
	transport := http.Transport{
		// 请求频繁可定义全局的client对象并启用长链接
		// 请求不频繁使用短链接
		DisableKeepAlives: true}
	client := http.Client{
		Transport: &transport,
	}

	respChan := make(chan *respData, 1)
	req, err := http.NewRequest("GET", "http://127.0.0.1:8000/", nil)
	if err != nil {
		fmt.Printf("new requestg failed, err:%v\n", 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("client.do resp:%v, err:%v\n", resp, err)
		rd := &respData{
			resp: resp,
			err:  err,
		}
		respChan <- rd
		wg.Done()
	}()

	select {
	case <-ctx.Done():
		//transport.CancelRequest(req)
		fmt.Println("call api timeout")
	case result := <-respChan:
		fmt.Println("call server api success")
		if result.err != nil {
			fmt.Printf("call server api failed, 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)
}

标签:重要,context,fmt,goroutine,标准,Context,time,上下文
From: https://www.cnblogs.com/Mcoming/p/18073098

相关文章

  • 第四章 python的标准库
    第四章python的标准库一、`os`1.1基本功能1.2文件和目录操作1.2.1目录操作1.2.2文件操作1.3路径操作1.4环境变量1.4.1`os.environ`1.4.2`os.pathsep`1.4.3`os.name`1.4.4`os.system()`1.4.5`os.putenv(key,value)`和`os.unsetenv(key)`1.5进程管理1.5......
  • Rust 标准库 Trait 指南
    部分内容来自Rust2021年期刊内容目录引言Trait基础自动Trait泛型Trait格式化Trait操作符Trait转换Trait错误处理迭代器TraitI/OTrait总结引言你是否曾想过下面这些trait有什么不同?Deref<Traget=T>,AsRef<T>,以及Borrow<T>?Clone,Copy,和ToOwned?From......
  • 单据类型参数设置增加自定义参数并通过BOS标准函数调用
     1、BOS函数说明2、创建对应单据的【单据类型参数】,继承自【单据类型参数模版】。 3、在单据参数中绑定【单据类型参数对象】 4、参数设置设置对应参数 5、在BOS中调用标准函数进行使用。 ......
  • STM32标准库低功耗
    STM32标准库低功耗1.睡眠模式1.1.进入以及退出方法睡眠模式较为简单,仅需要简单调用函数即可,且函数定义处于内核层,不需要引用多余的头文件。睡眠模式:仅内核停止,所有外设仍旧运行。调用"__WFI()"函数,退出方式为任意中断。调用"__WFE()"函数,退出方式为唤醒事件。2.停......
  • C# ContextMenuStrip创建主菜单与子菜单
    ContextMenuStrip创建主菜单与子菜单usingSystem;usingSystem.Windows.Forms;usingSystem.ComponentModel;usingSystem.Data;namespaceMenuStripExample{publicpartialclassForm1:Form{publicForm1(){InitializeCompo......
  • 探索数组的奥秘:数据结构的重要组成部分
    一.数组的定义1.概念数组是一种数据结构,用于存储相同类型的元素集合。这些元素按照索引或者下标访问,索引通常从0开始递增。2.数组的声明规则a.int[]array=newint[5];b.int[]array={1,2,3,4,5};c.int[]array =newint[]{1,2,3,4,5};数据类型[]数组名=初值......
  • DC电源模块在电子产品中的重要性分析
    DC电源模块在电子产品中的重要性分析BOSHIDADC电源模块在电子产品中具有重要的作用和意义。以下是一些分析: 1.提供稳定可靠的电源供应:电子产品的正常运行需要稳定可靠的电源供应。DC电源模块可以将交流电转换为直流电,并提供稳定的电压和电流输出,确保电子产品能够正常工作。......
  • 蓝队应急响应工具箱v10 [重要更新]
    / 蓝队工具箱V10 /1  简介蓝队工具箱是为打造一款专业级应急响应的集成多种工具的工具集,由真实应急响应环境所用到的工具进行总结打包而来,由ChinaRan404,W啥都学,清辉等开发者编写.把项目现场中所用到的工具连同环境一同打包,并实现“可移植性”“兼容性”“使用便......
  • C语言最重要的知识点(2)
    第二章第一节:数据输出(一)(二)1、使用printf和scanf函数时,要在最前面加上#include“stdio.h”2、printf可以只有一个参数,也可以有两个参数。(选择题考过一次)3、printf(“第一部分”,第二部分 );把第二部分的变量、表达式、常量以第一部分的形式展现出来!4、printf(“a=%d,b=%d”,12......
  • Windows右键菜单管理程序:ContextMenuManager
    前言ContextMenuManager是一款由中国人开发免安装的纯粹的Windows右键菜单管理开源程序,可以非常方便的管理Windows电脑的右键菜单,轻松去除不必要的或者是流氓的菜单选项,使你的电脑更加的干净、清爽。程序使用非常简单,没有任何门槛,我感觉非常棒,极力推荐道友们下载使用。Github......