go并发编程和工程管理

定义package(包)

package cuser
//package 用来组织源码,是多个源码的集合,代码复用基础;每个源码文件都要声明所属的package,同一个文件夹下的源码文件必须是同一包里
type Coures struct{
	Name string
}


package cuser
func GetCourse(c Coures) string{
	return c.Name
}

  目录示例

image

import 导入
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

  示意图

image

跳过耗时的单元测试

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

那么:

  1. A 一定先于 B 执行完成;

  2. 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 都没有准备好
}

  

要监控多个channel任何一个channel有值都知道
1.某一个分支就绪就执行该分支
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 的区别?

switchselect
判断表达式 判断 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信息
退出监控
监控完成

  

 

 

 

 

 

 

 

 

 

 

 

posted @ 2026-06-20 16:15  烟雨楼台,行云流水  阅读(4)  评论(0)    收藏  举报