go并发编程和工程管理
定义package(包)
package cuser
//package 用来组织源码,是多个源码的集合,代码复用基础;每个源码文件都要声明所属的package,同一个文件夹下的源码文件必须是同一包里
type Coures struct{
Name string
}
package cuser
func GetCourse(c Coures) string{
return c.Name
}
目录示例

package main
import (
"fmt"
cuser "lnhgo/day04/demo01/user"//导包使用别名
)
func main(){
t:=cuser.Coures{
Name: "c",
}
fmt.Println(cuser.GetCourse(t))
}
执行结果
[Running] go run "d:\golang\goproject\src\lnhgo\day04\demo01\main\tempCodeRunnerFile.go"
c
[Done] exited with code=0 in 1.355 seconds
导入方式2(尽量少用可读性差)
package main
import (
"fmt"
//cuser "lnhgo/day04/demo01/user"//导包使用别名
. "lnhgo/day04/demo01/user"//导包使用别名.表示导入到main包里
)
func main(){
t:=Coures{//使用.导入使用方式
Name: "c",
}
fmt.Println(GetCourse(t))//调用方式
}
执行结果
[Running] go run "d:\golang\goproject\src\lnhgo\day04\demo01\main\main.go"
c
[Done] exited with code=0 in 0.939 seconds
匿名导入
package cuser
import "fmt"
func init(){
fmt.Println("this is 10")
}
main文件
package main
import (
"fmt"
//cuser "lnhgo/day04/demo01/user"//导包使用别名
_ "lnhgo/day04/demo01/user"//匿名导入,只是启动时调用包里init函数
)
func main(){
// t:=Coures{//使用.导入使用方式
// Name: "c",
// }
fmt.Println("t")//调用方式
}
执行结果
[Running] go run "d:\golang\goproject\src\lnhgo\day04\demo01\main\main.go"
this is 10
t
[Done] exited with code=0 in 1.272 seconds
go.mod文件用法
下载包该操作在项目根目录(D:\golang\goproject\src\lnhgo)
PS D:\golang\goproject\src\lnhgo> go get github.com/gin-gonic/gin go: github.com/gin-gonic/gin@v1.12.0 requires go >= 1.25.0; switching to go1.25.11 go: downloading github.com/gin-contrib/sse v1.1.0 go: downloading github.com/mattn/go-isatty v0.0.20 go: downloading github.com/quic-go/quic-go v0.59.0 go: downloading golang.org/x/net v0.51.0 go: downloading github.com/go-playground/validator/v10 v10.30.1 go: downloading github.com/goccy/go-yaml v1.19.2 go: downloading github.com/pelletier/go-toml/v2 v2.2.4 go: downloading github.com/ugorji/go/codec v1.3.1 go: downloading go.mongodb.org/mongo-driver/v2 v2.5.0 go: downloading google.golang.org/protobuf v1.36.10 go: downloading github.com/bytedance/sonic v1.15.0 go: downloading github.com/goccy/go-json v0.10.5 go: downloading github.com/json-iterator/go v1.1.12 go: downloading golang.org/x/sys v0.41.0 go: downloading github.com/gabriel-vasile/mimetype v1.4.12 go: downloading github.com/go-playground/universal-translator v0.18.1 go: downloading github.com/leodido/go-urn v1.4.0 go: downloading golang.org/x/crypto v0.48.0 go: downloading golang.org/x/text v0.34.0 go: downloading github.com/quic-go/qpack v0.6.0 go: downloading github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd go: downloading github.com/modern-go/reflect2 v1.0.2 go: downloading github.com/go-playground/locales v0.14.1 go: downloading github.com/bytedance/gopkg v0.1.3 go: downloading github.com/cloudwego/base64x v0.1.6 go: downloading golang.org/x/arch v0.22.0 go: downloading github.com/klauspost/cpuid/v2 v2.3.0 go: downloading github.com/bytedance/sonic/loader v0.5.0 go: downloading github.com/twitchyliquid64/golang-asm v0.15.1 go: upgraded go 1.22.2 => 1.25.0 go: added github.com/bytedance/gopkg v0.1.3 go: added github.com/bytedance/sonic v1.15.0 go: added github.com/bytedance/sonic/loader v0.5.0 go: added github.com/cloudwego/base64x v0.1.6 go: added github.com/gabriel-vasile/mimetype v1.4.12 go: added github.com/gin-contrib/sse v1.1.0 go: added github.com/gin-gonic/gin v1.12.0 go: added github.com/go-playground/locales v0.14.1 go: added github.com/go-playground/universal-translator v0.18.1 go: added github.com/go-playground/validator/v10 v10.30.1 go: added github.com/goccy/go-json v0.10.5 go: added github.com/goccy/go-yaml v1.19.2 go: added github.com/json-iterator/go v1.1.12 go: added github.com/klauspost/cpuid/v2 v2.3.0 go: added github.com/leodido/go-urn v1.4.0 go: added github.com/mattn/go-isatty v0.0.20 go: added github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd go: added github.com/modern-go/reflect2 v1.0.2 go: added github.com/pelletier/go-toml/v2 v2.2.4 go: added github.com/quic-go/qpack v0.6.0 go: added github.com/quic-go/quic-go v0.59.0 go: added github.com/twitchyliquid64/golang-asm v0.15.1 go: added github.com/ugorji/go/codec v1.3.1 go: added go.mongodb.org/mongo-driver/v2 v2.5.0 go: added golang.org/x/arch v0.22.0 go: added golang.org/x/crypto v0.48.0 go: added golang.org/x/net v0.51.0 go: added golang.org/x/sys v0.41.0 go: added golang.org/x/text v0.34.0 go: added google.golang.org/protobuf v1.36.10
查看go.mod文件
module lnhgo go 1.25.0 require ( github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/sonic v1.15.0 // indirect github.com/bytedance/sonic/loader v0.5.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect github.com/gin-contrib/sse v1.1.0 // indirect github.com/gin-gonic/gin v1.12.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.30.1 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.19.2 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/quic-go v0.59.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect golang.org/x/arch v0.22.0 // indirect golang.org/x/crypto v0.48.0 // indirect golang.org/x/net v0.51.0 // indirect golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.34.0 // indirect google.golang.org/protobuf v1.36.10 // indirect )
设置goproxy
PS D:\golang\goproject\src\lnhgo> go env -w GO111MODULE=on PS D:\golang\goproject\src\lnhgo> go env -w GO GOPROXY=https://goproxy.cn.direct
指定版本下载包
PS D:\golang\goproject\src\lnhgo> go list -m -versions github.com/gin-gonic/gin github.com/gin-gonic/gin v1.1.1 v1.1.2 v1.1.3 v1.1.4 v1.3.0 v1.4.0 v1.5.0 v1.6.0 v1.6.1 v1.6.2 v1.6.3 v1.7.0 v1.7.1 v1.7.2 v1.7.3 v1.7.4 v1.7.5 v1.7.6 v1.7.7 v1.8.0 v1.8.1 v1.8.2 v1.9.0 v1.9.1 v1.10.0 v1.10.1 v1.11.0 v1.12.0 PS D:\golang\goproject\src\lnhgo> go get github.com/gin-gonic/gin@v1.12.0
gomod 相关命令
PS D:\golang\goproject\src\lnhgo> go mod help
Go mod provides access to operations on modules.
Note that support for modules is built into all the go commands,
not just 'go mod'. For example, day-to-day adding, removing, upgrading,
and downgrading of dependencies should be done using 'go get'.
See 'go help modules' for an overview of module functionality.
Usage:
go mod <command> [arguments]
The commands are:
download download modules to local cache
edit edit go.mod from tools or scripts
graph print module requirement graph
init initialize new module in current directory 初始化
tidy add missing and remove unused modules 整理依赖
vendor make vendored copy of dependencies
verify verify dependencies have expected content
why explain why packages or modules are needed
Use "go help mod <command>" for more information about a command.
PS D:\golang\goproject\src\lnhgo> go mod graph 查看依赖关系
lnhgo github.com/bytedance/gopkg@v0.1.3
go tidy 也可以下载
PS D:\golang\goproject\src\lnhgo> go mod tidy go: downloading github.com/stretchr/testify v1.11.1 go: downloading go.uber.org/mock v0.6.0 go: downloading github.com/go-playground/assert/v2 v2.2.0 go: downloading github.com/davecgh/go-spew v1.1.1 go: downloading gopkg.in/yaml.v3 v3.0.1 go: downloading github.com/pmezard/go-difflib v1.0.0 go: finding module for package gorm.io/gorm go: finding module for package gorm.io/driver/sqlite go: downloading gorm.io/driver/sqlite v1.6.0 go: downloading gorm.io/gorm v1.31.1 go: found gorm.io/driver/sqlite in gorm.io/driver/sqlite v1.6.0 go: found gorm.io/gorm in gorm.io/gorm v1.31.1 go: downloading github.com/mattn/go-sqlite3 v1.14.22 go: downloading github.com/jinzhu/now v1.1.5 go: downloading github.com/jinzhu/inflection v1.0.0
升级操作
go get -u 升级最新版本 go get -u=patch 升级到最新修订版本 go get gorm.io/gorm@version 指定版本
go 编码简单规范
1、代码规范
代码规范不是强制,但是不同语言一些细微规范还是遵守
代码规范主要是方便团队内部形成一个统一的代码风格,提高代码可读性统一性
命名规范
包名:尽量和目录名保持一致;采取有意义的包名,尽量简短,尽量和标准库重名尽量采用小写
文件名: 如果对个单词可以采用蛇形命名
变量名:
蛇形:python php
驼峰:go c java;go首字母小写
一些专有 URL
结构体
驼峰:
接口命名
接口名和结构体差不多
常量命令:全部大写
2、注释规范
go 提供两种注释行注释 、块注释
3、import 规范
单元测试
package main
func add(a,b int) int{
return a+b
}
测试用例
package main
import "testing"
func TestAdd(t *testing.T) {
a:=add(1,4)
//if a!=4{
if a!=5{
t.Errorf("expect %d,actual:%d",3,a)
}
}
PS D:\golang\goproject\src\lnhgo\day04\demo03\main> go test
--- FAIL: TestAdd (0.00s)
add_test.go:6: expect 3,actual:5
FAIL
exit status 1
FAIL lnhgo/day04/demo03/main 1.019s
PS D:\golang\goproject\src\lnhgo\day04\demo03\main> go test
PASS
ok lnhgo/day04/demo03/main 0.825s
示意图

跳过耗时的单元测试
package main
import (
"fmt"
"testing"
)
func TestAdd(t *testing.T) {
a:=add(1,4)
//if a!=4{
if a!=5{
t.Errorf("expect %d,actual:%d",3,a)
}
}
func TestAdd2(t *testing.T) {
fmt.Println("Yes1 ")
if testing.Short(){
t.Skip("skip 模式下跳过")
}
fmt.Println("Yes2 ")
a:=add(1,5)
//if a!=4{
if a!=6{
t.Errorf("expect %d,actual:%d",6,a)
}
}
执行结果
PS D:\golang\goproject\src\lnhgo\day04\demo03\main> go test -short
Yes1
PASS
ok lnhgo/day04/demo03/main 1.023s
基于表格测试用例
package main
import (
"fmt"
"testing"
)
func TestAdd(t *testing.T) {
var dataset = []struct{
a int
b int
out int
}{
{1,2,3},
{12,12,24},
{-9,8,-1},
{0,0,0},
}
for _,v:= range dataset{
re:=add(v.a,v.b)
if re == v.out{
fmt.Println("正确")
}
}
}
执行结果
PS D:\golang\goproject\src\lnhgo\day04\demo03\main> go test
正确
正确
正确
正确
PASS
ok lnhgo/day04/demo03/main 0.959s
性能测试
package main
import (
"fmt"
"strconv"
"strings"
"testing"
)
func TestAdd(t *testing.T) {
var dataset = []struct{
a int
b int
out int
}{
{1,2,3},
{12,12,24},
{-9,8,-1},
{0,0,0},
}
for _,v:= range dataset{
re:=add(v.a,v.b)
if re == v.out{
fmt.Println("正确")
}
}
}
func BenchmarkAdd(bb *testing.B) {
a:=123
b:=456
c:=579
for i:=0;i<bb.N;i++{
if actual:=add(a,b);actual!=c{
fmt.Println("aaa ")
}
}
}
const num =10000
func BenchmarkStringsprintf(b *testing.B) {
b.ResetTimer()
for i:=0;i<b.N;i++{
var str string
for j:=0;j<num;j++{
str=fmt.Sprintf("%s%d",str,j)
}
}
b.StopTimer()
}
func BenchmarkStringAdd(b *testing.B) {
b.ResetTimer()
for i:=0;i<b.N;i++{
var str string
for j:=0;j<num;j++{
str=str+strconv.Itoa(j)
}
}
b.StopTimer()
}
func BenchmarkStringBuilder(b *testing.B) {
b.ResetTimer()
for i:=0;i<b.N;i++{
//var str string
var build strings.Builder
for j:=0;j<num;j++{
build.WriteString(strconv.Itoa(j))
//str=str+strconv.Itoa(j)
}
_ = build.String()
}
b.StopTimer()
}
执行结果
PS D:\golang\goproject\src\lnhgo\day04\demo03\main> go test -bench=".*"
正确
正确
正确
正确
goos: windows
goarch: amd64
pkg: lnhgo/day04/demo03/main
cpu: Intel(R) Core(TM) i7-10510U CPU @ 1.80GHz
BenchmarkAdd-8 1000000000 0.6571 ns/op
BenchmarkStringsprintf-8 14 80123886 ns/op
BenchmarkStringAdd-8 25 44124932 ns/op
BenchmarkStringBuilder-8 3601 810793 ns/op
PASS
ok lnhgo/day04/demo03/main 6.774s
go 语言并发
Go 最大的特色之一就是并发(Concurrency)。Go 提供了 Goroutine 和 Channel,使得编写并发程序比传统线程模型简单得多。
1. 什么是并发?
并发(Concurrency):
多个任务在同一时间段内交替执行。
例如:
-
下载文件
-
接收网络请求
-
处理数据库操作
-
日志记录
都可以同时进行。
2. Goroutine(协程)
Goroutine 是 Go 提供的轻量级线程。
启动一个 Goroutine 非常简单:
package main
import (
"fmt"
)
func hello() {
fmt.Println("hello goroutine")
}
func main() {
go hello()
fmt.Println("main")
}
运行结果可能:
main
或者:
main
hello goroutine
因为主线程结束后,程序就退出了。
等待 Goroutine
可以简单使用:
package main
import (
"fmt"
"time"
)
func hello() {
fmt.Println("goroutine")
}
func main() {
go hello()
time.Sleep(time.Second)
}
输出:
goroutine
但实际项目一般不用 Sleep。
3. sync.WaitGroup
等待多个 Goroutine 执行完成。
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func worker(i int) {
defer wg.Done()
fmt.Println(i)
}
func main() {
for i := 0; i < 5; i++ {
wg.Add(1)
go worker(i)
}
wg.Wait()
}
输出顺序不固定:
0
3
1
4
2
因为多个协程是并发执行的。
4. Channel(通道)
Channel 用于 Goroutine 之间通信。
创建:
ch := make(chan int)
发送:
ch <- 100
接收:
x := <-ch
示例
package main
import "fmt"
func main() {
ch := make(chan int)
go func() {
ch <- 10
}()
num := <-ch
fmt.Println(num)
}
输出:
10
5. 无缓冲 Channel
ch := make(chan int)
发送:
ch <- 10
必须有人接收,否则阻塞。
像两个人握手:
发送方 ←→ 接收方
6. 有缓冲 Channel
创建:
ch := make(chan int, 3)
最多放三个元素:
ch <- 1
ch <- 2
ch <- 3
第四次:
ch <- 4
会阻塞。
7. 关闭 Channel
发送完数据:
close(ch)
接收:
v, ok := <-ch
例如:
package main
import "fmt"
func main() {
ch := make(chan int, 2)
ch <- 1
ch <- 2
close(ch)
for v := range ch {
fmt.Println(v)
}
}
输出:
1
2
8. 单向 Channel
只发送:
func producer(ch chan<- int) {
ch <- 10
}
只接收:
func consumer(ch <-chan int) {
num := <-ch
fmt.Println(num)
}
提高安全性。
9. select
监听多个 Channel:
select {
case x := <-ch1:
fmt.Println(x)
case y := <-ch2:
fmt.Println(y)
default:
fmt.Println("没有数据")
}
类似:
if channel1 有数据
执行1
否则 if channel2 有数据
执行2
否则
default
10. 定时器
select {
case <-time.After(time.Second):
fmt.Println("超时")
}
1 秒后:
超时
11. Context
控制 Goroutine 生命周期。
ctx, cancel := context.WithCancel(context.Background())
启动:
go worker(ctx)
取消:
cancel()
worker:
func worker(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("退出")
return
default:
fmt.Println("工作中")
}
}
}
12. Mutex(互斥锁)
多个协程共享变量:
错误:
count++
会发生竞争。
使用:
var mu sync.Mutex
mu.Lock()
count++
mu.Unlock()
保证同一时刻只有一个协程修改数据。
13. RWMutex(读写锁)
var rw sync.RWMutex
读:
rw.RLock()
//读取
rw.RUnlock()
写:
rw.Lock()
//修改
rw.Unlock()
多个读可以同时进行。
写是独占的。
14. sync.Once
保证只执行一次:
var once sync.Once
once.Do(func() {
fmt.Println("初始化")
})
无论调用多少次:
初始化
只输出一次。
常用于:
-
单例模式
-
配置加载
-
数据库连接初始化
15. sync.Pool
对象池:
var pool = sync.Pool{
New: func() interface{} {
return make([]byte, 1024)
},
}
获取:
buf := pool.Get().([]byte)
归还:
pool.Put(buf)
减少内存分配。
16. 原子操作
atomic.AddInt64(&count, 1)
代替:
mu.Lock()
count++
mu.Unlock()
效率更高。
17. 并发和并行
并发(Concurrency)
一个 CPU:
任务1
任务2
任务1
任务2
交替执行。
并行(Parallelism)
多个 CPU:
CPU1 → 任务1
CPU2 → 任务2
真正同时运行。
Go 并发模型
Go 借鉴了:
Don't communicate by sharing memory; share memory by communicating.
即:
不要通过共享内存来通信,
而要通过通信来共享内存。
推荐:
Goroutine
↓
Channel
↓
Context
而不是:
Goroutine
↓
Mutex
↓
共享变量
Go 并发学习路线
goroutine
↓
WaitGroup
↓
channel
↓
select
↓
context
↓
Mutex
↓
RWMutex
↓
atomic
↓
sync.Once
↓
sync.Pool
↓
生产者消费者模型
↓
工作池(Worker Pool)
↓
Fan In / Fan Out
↓
Pipeline
↓
并发安全 map
↓
errgroup
↓
高并发服务器
示例启动协程
package main
import (
"fmt"
"time"
)
func asyncPrint() {
fmt.Println("chen")
}
//主协程
func main(){
//主死随从
go asyncPrint()//异步执行;
for i:=0;i<100;i++{
// 为什么不按顺序重复;
// 1.闭包问题,2for循环问题for循环的时候,每个变量会重用,,当进入到第二轮的时候for循环时候,这个i的值就变了;所以进入for循环第一时间将i赋值给一个变量;
// go协程启动的时候打印新变量;或者值传递
//tmp:=i
//go func (){
go func (i int) {
//fmt.Println(tmp)
fmt.Println(i)
//}()
}(i)
}
fmt.Println("main goroutine")
time.Sleep(5*time.Second)
}
执行结果
PS D:\golang\goproject\src\lnhgo> go run day04\demo04\main.go
2
chen
6
36
4
5
9
87
...
main goroutine
...
97
go gmp协程调度
Go 的协程调度(Scheduler)是 Go 并发模型的核心。它负责把大量 Goroutine 高效地分配到少量操作系统线程上执行。
简单来说:
Go 调度器 = 管理 Goroutine 在多个线程上的运行。
1. 为什么需要调度器?
假设有:
go task1()
go task2()
go task3()
如果每个 Goroutine 都对应一个操作系统线程:
task1 ---> Thread1
task2 ---> Thread2
task3 ---> Thread3
当有几万个 Goroutine 时:
100000 goroutine
↓
100000 thread
线程切换开销会非常大。
所以 Go 采用:
100000 Goroutine
↓
8 核 CPU:
P0
P1
P2
...
P7
默认有 8 个 P。
3. GMP 关系
一个 M 必须绑定一个 P:
M0 <--> P0
P 中维护:
G1
G2
G3
执行过程:
P0
↓
取出 G1
↓
交给 M0
↓
CPU 执行
执行完:
再取 G2
4. 本地队列
每个 P 有自己的队列:
P0:
G1 G2 G3
P1:
G4 G5
P2:
G6 G7
这样减少锁竞争,提高性能。
5. 全局队列
除了本地队列,还有:
Global Queue
当:
-
本地队列满
-
创建新的 Goroutine
会进入全局队列。
调度器会:
Global Queue
↓
分配给空闲 P
6. Work Stealing(工作窃取)
如果:
P0:
G1 G2 G3 G4
P1:
空
P1 不会闲着。
而是:
P1
↓
偷取 P0 一半任务
结果:
P0:
G1 G2
P1:
G3 G4
提高 CPU 利用率。
7. Goroutine 创建
go hello()
流程:
创建 G
↓
加入当前 P 的队列
↓
M 获取 G
↓
CPU 执行
而不是:
go hello()
↓
创建线程
这是 Goroutine 很轻量的原因。
8. 系统调用阻塞
例如:
time.Sleep()
或者:
net/http
数据库 IO
文件 IO
如果:
M0 + P0
执行时发生阻塞:
M0 阻塞
Go 会:
把 P0 拿走
绑定给新的线程:
M1 + P0
继续运行其它 Goroutine。
所以:
一个线程阻塞不会影响整个程序。
9. 协作式调度(早期)
Go1.13 前:
只有:
runtime.Gosched()
或者:
-
channel
-
syscall
-
sleep
才会主动让出 CPU。
可能出现:
for {
}
死循环。
其它 Goroutine 得不到执行机会。
10. 抢占式调度(Go1.14+)
现在:
即使:
for {
}
调度器也会在大约 10ms 后强制抢占:
G1
↓
运行 10ms
↓
暂停
↓
执行 G2
避免一个协程独占 CPU。
11. GOMAXPROCS
查看:
fmt.Println(runtime.GOMAXPROCS(0))
设置:
runtime.GOMAXPROCS(4)
表示:
最多同时有 4 个 P 工作
即:
最多利用 4 个 CPU 核。
12. 调度流程
假设:
go A()
go B()
go C()
创建:
G1
G2
G3
加入:
P0 queue
调度:
P0
↓
G1
↓
M0
↓
CPU
结束
↓
G2
结束
↓
G3
多个 P:
P0 --> M0
P1 --> M1
P2 --> M2
可以真正并行。
13. 整个 GMP 模型
Global Queue
│
▼
┌──────────────┬──────────────┐
▼ ▼ ▼
P0 P1 P2
G1 G2 G3 G4 G5 G6 G7
│ │ │
▼ ▼ ▼
M0 M1 M2
│ │ │
▼ ▼ ▼
CPU0 CPU1 CPU2
14. 为什么 Go 并发性能高?
因为:
Goroutine 很轻
初始栈只有几 KB。
可以创建:
10万
100万
个 Goroutine。
而线程通常只能:
几千
个。
Work Stealing
负载均衡。
P 本地队列
减少锁竞争。
抢占式调度
防止饿死。
网络 IO 多路复用
减少线程数量。
面试最经典的问题
Go 调度模型是什么?
答:
Go 采用 GMP 调度模型,G 表示 Goroutine,M 表示系统线程,P 表示逻辑处理器。P 持有 Goroutine 队列,M 必须绑定 P 才能执行 G。调度器通过本地队列、全局队列和 Work Stealing 实现高效调度,并在 Go1.14 后支持抢占式调度。
waitgroup 的使用;避免main函数提前关闭
package main
import (
"fmt"
"sync"
)
// ""
func main(){
//WaitGroup 主要用于goroutine的执行,ADD方法要与Done方法配套使用
var r sync.WaitGroup
r.Add(100)
for i :=0;i<100;i++{
go func (i int ) {
defer r.Done()
fmt.Println(i)
//r.Done()
}(i)
}
r.Wait()
}
go的锁(互斥锁)
package main
import (
"fmt"
"sync"
)
/*
锁 资源竞争
*/
var total int
var t sync.WaitGroup
var lock sync.Mutex//锁千万不要复制否则就会出现锁失效
func add(){
defer t.Done()
for i:=0;i<10000000;i++{
lock.Lock() //操作数据前加锁
total+=1
lock.Unlock()//操作完解锁
}
}
func sub(){
defer t.Done()
for i:=0;i<10000000;i++{
lock.Lock() //枷锁
total -=1
lock.Unlock() //解锁
}
}
func main(){
t.Add(2)
go add()
go sub()
fmt.Println(total)
}
执行结果
PS D:\golang\goproject\src\lnhgo> go run day04\demo06\main.go
0
整数加减保障原子性的函数atomic.AddInt32
package main
import (
"fmt"
"sync"
"sync/atomic"
)
/*
锁 资源竞争
*/
var total int32
var t sync.WaitGroup
var lock sync.Mutex//锁千万不要复制否则就会出现锁失效
func add(){
defer t.Done()
for i:=0;i<10000000;i++{
atomic.AddInt32(&total,+1)
//lock.Lock() //操作数据前加锁
//total+=1
//lock.Unlock()//操作完解锁
}
}
func sub(){
defer t.Done()
for i:=0;i<10000000;i++{
atomic.AddInt32(&total,-1)
//lock.Lock() //枷锁
//total -=1
//lock.Unlock() //解锁
}
}
func main(){
t.Add(2)
go add()
go sub()
t.Wait()
fmt.Println(total)
}
执行结果
0
读写锁
package main
import (
"fmt"
"sync"
"time"
)
/*
锁本质上是将并行的代码串型化了,使用lock肯定会影响性能
即使设计锁也要尽量保证并行
有两组协程
一组负责写,一组负责读
读协程之间应该允许并发、读写之间应该串行
*/
func main(){
var num int
var rwlock sync.RWMutex
var wg sync.WaitGroup
wg.Add(2)
go func () {
defer wg.Done()
rwlock.Lock()
defer rwlock.Unlock()
num =12
}()
time.Sleep(time.Second)
go func () {
defer wg.Done()
// for i:=0;i<1000000;i++{
rwlock.RLock()
defer rwlock.RUnlock()
fmt.Println(num)
//}
}()
wg.Wait()
}
协程通讯之管道
有缓冲管道
package main
import "fmt"
func main() {
/*
不要通过共享内存来通信,而是要通过通信来实现内存共享
channel
*/
var msg chan string
msg = make(chan string, 1)//channel 的初始化值,如果为0的话,放值进去会阻塞;有缓冲无缓冲;设置为0表示无缓冲管道;只要大于0;就是有缓冲
// msg = make(chan string, 0)
// go func (msg chan string) {
// data:=<-msg
// fmt.Println(data)
// }(msg)
msg <- "bobby" //放值到channel里
data := <-msg //从channel里取值
fmt.Println(data)
}
执行结果
bobby
无缓冲管道
package main
import "fmt"
func main() {
/*
不要通过共享内存来通信,而是要通过通信来实现内存共享
channel
*/
var msg chan string
//msg = make(chan string, 1)//channel 的初始化值,如果为0的话,放值进去会阻塞;有缓冲无缓冲;设置为0表示无缓冲管道;只要大于0;就是有缓冲
msg = make(chan string, 0)// 无缓存
//
go func (msg chan string) {// go有一种happen-before机制,可以保障
data:=<-msg
fmt.Println(data)
}(msg)
msg <- "bobby" //放值到channel里
// data := <-msg //从channel里取值
// fmt.Println(data)
}
执行结果
bobby
happen-before机制
happen-before(HB) 是 Go 内存模型中的核心概念,用来描述:
一个操作的结果,对于另一个操作是否一定可见。
它解决的问题是:
多个 Goroutine 并发执行时,
谁先发生?
谁能看到谁的结果?
如果两个操作之间存在 happen-before 关系:
A happen-before B
那么:
-
A 一定先于 B 执行完成;
-
A 对内存的修改,B 一定能看到。
为什么需要 happen-before?
例如:
var x int
go func() {
x = 100
}()
fmt.Println(x)
可能输出:
0
也可能:
100
因为:
主 goroutine 和子 goroutine 之间
不存在 happen-before 关系
调度顺序是不确定的。
channel 建立 HB 关系
package main
import "fmt"
func main() {
ch := make(chan int)
var x int
go func() {
x = 100
ch <- 1
}()
<-ch
fmt.Println(x)
}
输出一定:
100
因为:
x = 100
↓
ch <- 1
↓ HB
<- ch
↓
fmt.Println(x)
Go 内存模型规定:
channel 的发送 happens-before 对应的接收。
Mutex 建立 HB
var (
mu sync.Mutex
x int
)
go func() {
mu.Lock()
x = 100
mu.Unlock()
}()
另一个协程:
mu.Lock()
fmt.Println(x)
mu.Unlock()
一定输出:
100
因为:
Unlock()
happens-before
Lock()
即:
goroutine1
Lock
x=100
Unlock
↓HB
goroutine2
Lock
print(x)
Unlock
WaitGroup 建立 HB
var wg sync.WaitGroup
var x int
wg.Add(1)
go func() {
defer wg.Done()
x = 100
}()
wg.Wait()
fmt.Println(x)
一定输出:
100
因为:
Done()
↓ HB
Wait() 返回
Once 建立 HB
var once sync.Once
var x int
once.Do(func() {
x = 100
})
之后:
once.Do(func() {
})
能够保证:
第一次 Do 中的所有写操作
HB 于
后续所有 Do 返回
原子操作 HB
atomic.StoreInt32(&x, 1)
与:
atomic.LoadInt32(&x)
之间建立同步关系。
例如:
go func() {
data = "hello"
atomic.StoreInt32(&ready, 1)
}()
另一个协程:
for atomic.LoadInt32(&ready) == 0 {
}
fmt.Println(data)
一定输出:
hello
因为:
Store
↓HB
Load
init() 与 main()
Go 保证:
init()
happens-before
main()
例如:
var x int
func init() {
x = 100
}
func main() {
fmt.Println(x)
}
输出:
100
Goroutine 创建
x = 100
go func() {
fmt.Println(x)
}()
Go 保证:
go 语句之前
HB
goroutine 开始执行
因此:
100
一定可见。
但 Goroutine 退出没有 HB
go func() {
x = 100
}()
fmt.Println(x)
没有:
goroutine结束
HB
主协程继续
所以:
输出不确定。
必须借助:
-
channel
-
WaitGroup
-
Mutex
建立同步关系。
最经典例子
有数据竞争
var x int
go func() {
x = 1
}()
go func() {
fmt.Println(x)
}()
不存在:
x=1
HB
Println(x)
因此:
数据竞争
结果未定义
HB 图
例如:
x = 100
go func() {
y = 200
ch <- 1
}()
<- ch
fmt.Println(y)
关系:
main
x=100
↓
go语句
↓ HB
goroutine
y=200
↓
send
↓ HB
receive
↓
fmt.Println(y)
所以:
y=200
一定可见
Go 内存模型核心思想
可以记住一句:
如果两个操作之间不存在 happen-before 关系,那么它们的执行顺序就是不确定的。
换句话说:
没有 HB
=
没有同步
=
数据竞争
=
结果不可预测
面试中的标准回答
如果被问:
什么是 happen-before?
可以回答:
happen-before 是 Go 内存模型定义的一种偏序关系。如果操作 A happen-before 操作 B,那么 A 的执行结果对 B 一定可见,并且 A 在逻辑上先于 B 执行。Go 通过 channel、Mutex、WaitGroup、Once、atomic 等同步原语建立 happen-before 关系,从而保证并发程序的正确性。
这是理解:
-
channel
-
Mutex
-
WaitGroup
-
atomic
-
数据竞争
-
内存模型
乃至整个 Go 并发编程最重要的基础之一。
管道使用场景
无缓冲channel适用于通知,B要第一时间知道A是否已经完成 有缓冲channel适用于消费者和生产者之间通信 使用场景 1.消息传递 2.信号广播 3.事件订阅和广播 4.任务分发 5.结果汇总 6. 并发控制 7.同步和异步
遍历管道
package main
import (
"fmt"
"time"
)
func main(){
var msg chan int
msg = make(chan int,2)
go func (msg chan int) {
for data:=range msg{
fmt.Println(data)
}
fmt.Println("all done")
}(msg)
msg <-1
msg <-2
close(msg)//关闭管道
d:=<-msg//关闭通道可以取数据;不能放值
fmt.Println(d)
//msg <-3 已经关闭的管道无法写入数据
time.Sleep(time.Second*3)
}
执行结果
1
2
all done
单向管道使用
package main
import (
"fmt"
"time"
)
func producer(out chan<- int){
for i:=1;i<10;i++{
out <- i*i
}
close(out)
}
func consumer(in <-chan int){
for num := range in{
fmt.Printf("num=%d\r\n",num)
}
}
func main(){
// var ch1 chan int//双向
// var ch2 chan<- float64//单向只能写
// var ch3 <-chan int //单向只能取
// c:=make(chan int,3)
// var send chan<- int = c//只能写数据
// var read <-chan int = c //只能取数据
// send <- 1
// <-read
c:=make(chan int)
go producer(c)
go consumer(c)
//fmt.Println("j")
time.Sleep(10*time.Second)
}
执行结果
num=1
num=4
num=9
num=16
num=25
num=36
num=49
num=64
num=81
利用管道交叉打印
package main
import (
"fmt"
"time"
//"sync"
)
var number,letter= make(chan bool),make(chan bool)//两个无缓存管道
//var wg sync.WaitGroup
func printNum(){
i :=1
for {
<-number
fmt.Printf("%d%d",i,i+1)
i+=2
letter<-true
}
}
func printLetter(){
i :=0
str:="ABCDEFGHIJKLMNOPQSTUVWXYZ"
for {
<-letter
if i+2>=len(str){
fmt.Print(str[len(str)-1:])
return
}
fmt.Print(str[i:i+2])
i+=2
number<-true
}
}
func main(){
go printNum()
go printLetter()
number <- true
time.Sleep(time.Second*5)
}
监控go 协程的执行
select 是 Go 并发编程中非常重要的关键字,用于同时监听多个 Channel 的读写操作。
可以把它理解成:Channel 版本的 switch。
select {
case <-ch1:
// ch1 有数据
case data := <-ch2:
// 从 ch2 接收数据
case ch3 <- 100:
// 向 ch3 发送数据
default:
// 所有 channel 都没有准备好
}
package main
import (
"fmt"
"sync"
"time"
)
//ar done bool
var lock sync.Mutex
var done = make(chan struct{})//chan 是多协程安全的;管道要初始化
//很多时候不会多个协程写同一个管道
func g1(ch chan struct{}){
time.Sleep(time.Second)
ch <- struct{}{}//
}
func g2(ch chan struct{}){
time.Sleep(2*time.Second)
//time.Sleep(time.Second)
ch <- struct{}{}
}
func main(){
g1chan:=make(chan struct{})
g2chan:=make(chan struct{})
go g1(g1chan)
go g2(g2chan)
//要监控多个channel任何一个channel有值都知道
//1.某一个分支就绪就执行该分支
//2.如果两个都就绪那就随机执行一个
select{
case <-g1chan://
fmt.Println("g1 done")
case <-g2chan:
fmt.Println("g2 done")
}
}
执行结果
PS D:\golang\goproject\src\lnhgo> go run day04\demo12\main.go
g1 done
如果两个都就绪那就随机执行一个;目的是防止饥饿
package main
import (
"fmt"
"sync"
"time"
)
//ar done bool
var lock sync.Mutex
var done = make(chan struct{})//chan 是多协程安全的;管道要初始化
//很多时候不会多个协程写同一个管道
func g1(ch chan struct{}){
time.Sleep(time.Second)
ch <- struct{}{}//
}
func g2(ch chan struct{}){
time.Sleep(2*time.Second)
//time.Sleep(time.Second)
ch <- struct{}{}
}
func main(){
g1chan:=make(chan struct{},1)
g2chan:=make(chan struct{},2)
g1chan <-struct{}{}
g2chan <-struct{}{}
//要监控多个channel任何一个channel有值都知道
//1.某一个分支就绪就执行该分支
//2.如果两个都就绪那就随机执行一个
select{
case <-g1chan://
fmt.Println("g1 done")
case <-g2chan:
fmt.Println("g2 done")
}
}
执行结果
PS D:\golang\goproject\src\lnhgo> go run day04\demo12\main.go
g1 done
PS D:\golang\goproject\src\lnhgo> go run day04\demo12\main.go
g1 done
PS D:\golang\goproject\src\lnhgo> go run day04\demo12\main.go
g2 done
解决等待超时防止阻塞
package main
import (
"fmt"
"sync"
"time"
)
//ar done bool
var lock sync.Mutex
var done = make(chan struct{})//chan 是多协程安全的;管道要初始化
//很多时候不会多个协程写同一个管道
func g1(ch chan struct{}){
time.Sleep(time.Second)
ch <- struct{}{}//
}
func g2(ch chan struct{}){
time.Sleep(2*time.Second)
//time.Sleep(time.Second)
ch <- struct{}{}
}
func main(){
g1chan:=make(chan struct{},1)
g2chan:=make(chan struct{},2)
go g1(g1chan)
go g2(g2chan)
//要监控多个channel任何一个channel有值都知道
//1.某一个分支就绪就执行该分支
//2.如果两个都就绪那就随机执行一个
for{
select{
case <-g1chan://
fmt.Println("g1 done")
case <-g2chan:
fmt.Println("g2 done")
default:
time.Sleep(10*time.Microsecond)
fmt.Println("default")
}
}
}
执行结果
PS D:\golang\goproject\src\lnhgo> go run day04\demo12\main.go
default
。。。
方式2超时退出建议使用
package main
import (
"fmt"
"sync"
"time"
)
//ar done bool
var lock sync.Mutex
var done = make(chan struct{})//chan 是多协程安全的;管道要初始化
//很多时候不会多个协程写同一个管道
func g1(ch chan struct{}){
time.Sleep(time.Second)
ch <- struct{}{}//
}
func g2(ch chan struct{}){
time.Sleep(2*time.Second)
//time.Sleep(time.Second)
ch <- struct{}{}
}
func main(){
g1chan:=make(chan struct{},1)
g2chan:=make(chan struct{},2)
go g1(g1chan)
go g2(g2chan)
//要监控多个channel任何一个channel有值都知道
//1.某一个分支就绪就执行该分支
//2.如果两个都就绪那就随机执行一个
timer:=time.NewTicker(time.Second)
for{
select{
case <-g1chan://
fmt.Println("g1 done")
case <-g2chan:
fmt.Println("g2 done")
case <- timer.C:
//time.Sleep(10*time.Second)
fmt.Println("timeout")
return
}
}
}
执行结果
PS D:\golang\goproject\src\lnhgo> go run day04\demo12\main.go
g1 done
timeout
select 是 Go 并发编程中非常重要的关键字,用于同时监听多个 Channel 的读写操作。
可以把它理解成:
Channel 版本的 switch。
1. 基本语法
select {
case <-ch1:
// ch1 有数据
case data := <-ch2:
// 从 ch2 接收数据
case ch3 <- 100:
// 向 ch3 发送数据
default:
// 所有 channel 都没有准备好
}
2. 为什么需要 select?
假设有两个 channel:
ch1 := make(chan int)
ch2 := make(chan int)
如果写:
x := <-ch1
y := <-ch2
程序会:
等待 ch1
↓
再等待 ch2
不能同时监听。
而:
select {
case x := <-ch1:
fmt.Println(x)
case y := <-ch2:
fmt.Println(y)
}
表示:
谁先准备好
谁先执行
3. 最简单例子
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
time.Sleep(time.Second)
ch1 <- 1
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- 2
}()
select {
case x := <-ch1:
fmt.Println("ch1:", x)
case y := <-ch2:
fmt.Println("ch2:", y)
}
}
输出:
ch1: 1
因为:
ch1 先准备好
4. 多个 case 同时满足
例如:
ch1 := make(chan int, 1)
ch2 := make(chan int, 1)
ch1 <- 1
ch2 <- 2
两个 channel 都有数据。
select {
case x := <-ch1:
fmt.Println(x)
case y := <-ch2:
fmt.Println(y)
}
结果:
1
或者:
2
select 会随机选择一个 case。
并不是按照顺序。
5. default
select {
case x := <-ch:
fmt.Println(x)
default:
fmt.Println("没有数据")
}
如果:
ch
没有数据:
立即执行:
没有数据
不会阻塞。
6. 没有 default
select {
case <-ch1:
case <-ch2:
}
如果:
ch1
ch2
都没有准备好:
select 会阻塞。
直到:
某一个 channel 可用
7. 超时控制
这是最经典的用法。
select {
case data := <-ch:
fmt.Println(data)
case <-time.After(time.Second):
fmt.Println("超时")
}
如果:
1 秒内:
ch
没有数据。
输出:
超时
8. forever + select
for {
select {
case msg := <-ch:
fmt.Println(msg)
}
}
不断消费 channel。
这是消费者模式。
9. select + default 实现非阻塞
select {
case x := <-ch:
fmt.Println(x)
default:
fmt.Println("继续干别的")
}
类似:
if channel 有数据
否则继续执行
10. select + close(channel)
v, ok := <-ch
其中:
ok == false
表示 channel 已关闭。
结合:
for {
select {
case v, ok := <-ch:
if !ok {
return
}
fmt.Println(v)
}
}
优雅退出。
11. select + Context
这是实际项目最常用的。
func worker(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("退出")
return
default:
fmt.Println("工作中")
}
}
}
取消:
cancel()
立即:
退出
12. select + ticker
定时任务:
ticker := time.NewTicker(time.Second)
for {
select {
case <-ticker.C:
fmt.Println("执行任务")
}
}
每秒执行一次。
13. select + 多生产者
select {
case data := <-redisChan:
case data := <-mysqlChan:
case data := <-kafkaChan:
}
谁先来处理谁。
14. select 原理
底层:
case1
case2
case3
会先随机打乱:
case2
case1
case3
然后:
检查哪个 channel 可操作。
因此:
select 天然公平。
避免:
case1 永远先执行
的问题。
15. 空 select
select {}
表示:
永久阻塞
相当于:
for {
}
但不占 CPU。
经典例子
生产者:
go func() {
time.Sleep(time.Second)
ch <- "hello"
}()
消费者:
select {
case msg := <-ch:
fmt.Println(msg)
case <-time.After(3 * time.Second):
fmt.Println("timeout")
}
输出:
hello
如果:
time.Sleep(5 * time.Second)
则:
输出:
timeout
面试中最经典的问题
select 是随机的吗?
是。
如果多个 case 同时满足:
随机选择一个执行
select 会遍历所有 case 吗?
会。
但会先随机化顺序,再检查。
select 能监听普通变量吗?
不能。
只能监听:
channel
的:
-
接收
-
发送
default 的作用?
实现:
非阻塞
select 和 switch 的区别?
| switch | select |
|---|---|
| 判断表达式 | 判断 channel |
| 顺序匹配 | 随机选择 |
| 不阻塞 | 可阻塞 |
| 用于普通逻辑 | 用于并发通信 |
在 Go 并发中,可以记住一条路线:
goroutine
↓
channel
↓
select
↓
context
↓
WaitGroup
↓
Mutex
↓
atomic
其中 select + channel + context 是 Go 高并发编程最核心的组合。
context 解决goroutine协程信息传递
其他方式解决
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
//需求:可以主动监控
func cpuInfo(s chan struct{}) {
defer wg.Done()
for{
select {
case <- s:
fmt.Println("退出监控")
return
default:
time.Sleep(2*time.Second)
fmt.Println("cpu信息")
}
// if stop {
// break
// }
}
}
func main() {
var stop = make(chan struct{})
wg.Add(1)
go cpuInfo(stop)
time.Sleep(6*time.Second)
stop <-struct{}{}
wg.Wait()
fmt.Println("监控完成")
}
执行结果
PS D:\golang\goproject\src\lnhgo> go run day04\demo13\main.go
cpu信息
cpu信息
cpu信息
退出监控
监控完成
context介绍
context 是 Go 标准库中的一个包:
import "context"
它主要用于:
在多个 Goroutine 之间传递取消信号、超时控制和请求范围的数据。
在实际开发中(Gin、数据库、微服务、HTTP 调用),几乎随处可见。
为什么需要 Context?
假设有一个任务:
func worker() {
for {
fmt.Println("工作中...")
time.Sleep(time.Second)
}
}
启动:
go worker()
问题来了:
怎么通知 worker 停止?
如果 Goroutine 很多:
main
├── goroutine1
├── goroutine2
├── goroutine3
└── goroutine4
一个个关闭会很麻烦。
于是 Go 提供了:
context.Context
Context 接口
源码:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
重点掌握:
Done()
Err()
Value()
1. context.Background()
创建根 Context:
ctx := context.Background()
通常:
func main() {
ctx := context.Background()
}
作为整个 Context 树的根节点。
2. context.WithCancel()
最常用。
创建:
ctx, cancel := context.WithCancel(
context.Background(),
)
返回:
ctx
cancel
示例
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("退出")
return
default:
fmt.Println("工作中")
time.Sleep(time.Second)
}
}
}
func main() {
ctx, cancel := context.WithCancel(
context.Background(),
)
go worker(ctx)
time.Sleep(3 * time.Second)
cancel()
time.Sleep(time.Second)
}
输出:
工作中
工作中
工作中
退出
Done()
返回一个只读 Channel:
ctx.Done()
当:
cancel()
被调用时:
<-ctx.Done()
立即触发。
3. context.WithTimeout()
超时自动取消。
ctx, cancel := context.WithTimeout(
context.Background(),
3*time.Second,
)
defer cancel()
示例:
ctx, cancel := context.WithTimeout(
context.Background(),
3*time.Second,
)
defer cancel()
<-ctx.Done()
fmt.Println(ctx.Err())
输出:
context deadline exceeded
4. context.WithDeadline()
指定截止时间。
deadline := time.Now().Add(
5 * time.Second,
)
ctx, cancel := context.WithDeadline(
context.Background(),
deadline,
)
5 秒后自动取消。
WithTimeout 和 WithDeadline
WithTimeout
↓
相对时间
3秒后取消
WithDeadline
↓
绝对时间
18:00取消
5. context.WithValue()
传递请求范围数据。
例如:
ctx := context.WithValue(
context.Background(),
"user",
"chenxi",
)
获取:
name := ctx.Value("user")
fmt.Println(name)
输出:
chenxi
推荐写法
不要直接用字符串。
定义类型:
type key string
const UserKey key = "user"
存:
ctx = context.WithValue(
ctx,
UserKey,
"chenxi",
)
取:
ctx.Value(UserKey)
避免 key 冲突。
Context 树
例如:
root := context.Background()
ctx1, cancel1 :=
context.WithCancel(root)
ctx2, cancel2 :=
context.WithCancel(ctx1)
关系:
Background
│
▼
ctx1
│
▼
ctx2
如果:
cancel1()
则:
ctx1 取消
ctx2 取消
全部关闭。
实际开发中的使用
HTTP 请求
req, _ := http.NewRequest(
"GET",
url,
nil,
)
ctx, cancel := context.WithTimeout(
context.Background(),
3*time.Second,
)
defer cancel()
req = req.WithContext(ctx)
超时自动取消请求。
数据库查询
例如:
db.QueryContext(
ctx,
sql,
)
超时自动终止 SQL。
Gin
Gin 的 Context(*gin.Context)内部也持有标准库 Context。
例如:
func User(c *gin.Context) {
ctx := c.Request.Context()
service(ctx)
}
Context + Select
最经典写法:
for {
select {
case <-ctx.Done():
return
case msg := <-ch:
fmt.Println(msg)
}
}
含义:
有消息处理消息
收到取消信号立即退出
Err()
取消原因:
ctx.Err()
可能返回:
手动取消
context canceled
超时
context deadline exceeded
面试常问
Context 有什么作用?
答:
Context 用于在 Goroutine 之间传递取消信号、超时控制和请求范围的数据。
为什么不用全局变量通知退出?
因为:
多个 Goroutine
父子关系
超时控制
链式传递
Context 更统一、更安全。
Context 能存业务数据吗?
可以:
WithValue()
但官方建议:
只存请求范围的数据
例如:
userID
traceID
requestID
不要把它当万能参数容器。
Context 最重要的三个 API
context.WithCancel
context.WithTimeout
context.WithDeadline
配合:
ctx.Done()
使用。
对于你现在正在学习的 Go 后端路线(Gin → GORM → Redis → 微服务),建议把下面三个知识点重点掌握:
goroutine
↓
channel + select
↓
context
因为在 Gin 请求处理、数据库超时控制、微服务调用链中,context 几乎是必备知识。
示例2
package main
import (
"context"
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
//需求:可以主动监控
func cpuInfo(ctx context.Context) {
defer wg.Done()
for{
select {
case <- ctx.Done():
fmt.Println("退出监控")
return
default:
time.Sleep(2*time.Second)
fmt.Println("cpu信息")
}
// if stop {
// break
// }
}
}
func main() {
//var stop = make(chan struct{})
wg.Add(1)
ctx,cancel:=context.WithCancel(context.Background())
go cpuInfo(ctx)
time.Sleep(6*time.Second)
cancel()
wg.Wait()
fmt.Println("监控完成")
}
执行结果
PS D:\golang\goproject\src\lnhgo> go run day04\demo13\main.go
cpu信息
cpu信息
cpu信息
退出监控
监控完成
示例2
package main
import (
"context"
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
//需求:可以主动监控
func cpuInfo(ctx context.Context) {
defer wg.Done()
for{
select {
case <- ctx.Done():
fmt.Println("退出监控")
return
default:
time.Sleep(2*time.Second)
fmt.Println("cpu信息")
}
// if stop {
// break
// }
}
}
func main() {
//var stop = make(chan struct{})
wg.Add(1)
/* context 包提供了3中函数,WithCancel,WithTimeout,WithValue:返回不同类型的结构体
如果goroutine 函数中,如果希望被控制,超时消息,传值,但是不希望影响原来的接口信息的时候,函数参数中第一个参数要尽量加上一个ctx,
*/
ctx1,cancel1:=context.WithCancel(context.Background())
ctx2,_:=context.WithCancel(ctx1)
go cpuInfo(ctx2)
time.Sleep(6*time.Second)
cancel1()
wg.Wait()
fmt.Println("监控完成")
}
执行结果
cpu信息
cpu信息
cpu信息
退出监控
监控完成
主动超时
package main
import (
"context"
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
//需求:可以主动监控
func cpuInfo(ctx context.Context) {
//函数里拿到链路ID
// fmt.Printf("trancid:%s\r\n",ctx.Value("traceid"))
//记录日志
defer wg.Done()
for{
select {
case <- ctx.Done():
fmt.Println("退出监控")
return
default:
time.Sleep(2*time.Second)
fmt.Println("cpu信息")
}
// if stop {
// break
// }
}
}
func main() {
//var stop = make(chan struct{})
wg.Add(1)
/* context 包提供了3中函数,WithCancel,WithTimeout,WithValue:返回不同类型的结构体
如果goroutine 函数中,如果希望被控制,超时消息,传值,但是不希望影响原来的接口信息的时候,函数参数中第一个参数要尽量加上一个ctx,
*/
// ctx1,cancel1:=context.WithCancel(context.Background())
// ctx2,_:=context.WithCancel(ctx1)
//方式2:超时自动退出
ctx,_:=context.WithTimeout(context.Background(),6*time.Second)
//方式3:WithDeadline 指定退出时间
// ctx1:=context.WithValue(ctx,"traceid","gjw12")
//方式4:WithValue
go cpuInfo(ctx)
wg.Wait()
fmt.Println("监控完成")
}
执行结果
cpu信息
cpu信息
cpu信息
退出监控
监控完成
带参数的主动超时
package main
import (
"context"
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
//需求:可以主动监控
func cpuInfo(ctx context.Context) {
//函数里拿到链路ID
fmt.Printf("trancid:%s\r\n",ctx.Value("traceid"))
//记录日志
defer wg.Done()
for{
select {
case <- ctx.Done(): //自动超时可以省略
fmt.Println("退出监控")
return
default:
time.Sleep(2*time.Second)
fmt.Println("cpu信息")
}
// if stop {
// break
// }
}
}
func main() {
//var stop = make(chan struct{})
wg.Add(1)
/* context 包提供了3中函数,WithCancel,WithTimeout,WithValue:返回不同类型的结构体
如果goroutine 函数中,如果希望被控制,超时消息,传值,但是不希望影响原来的接口信息的时候,函数参数中第一个参数要尽量加上一个ctx,
*/
// ctx1,cancel1:=context.WithCancel(context.Background())
// ctx2,_:=context.WithCancel(ctx1)
//方式2:超时自动退出
ctx,_:=context.WithTimeout(context.Background(),6*time.Second)
//方式3:WithDeadline 指定退出时间
ctx1:=context.WithValue(ctx,"traceid","gjw12")
//方式4:WithValue
go cpuInfo(ctx1)
wg.Wait()
fmt.Println("监控完成")
}
执行结果
trancid:gjw12
cpu信息
cpu信息
cpu信息
退出监控
监控完成

浙公网安备 33010602011771号