前置:转Go学习笔记1语法入门

目录

  • Golang进阶
    • groutine协程并发
      • 概念梳理
      • 创建goroutine语法
    • channel实现goroutine之间通信
      • channel与range、select
    • GoModules
      • Go Modules与GOPATH
      • Go Modules模式
      • 用Go Modules初始化项目
      • 修改模块的版本依赖关系
      • Go Modules 版本号规范
      • vendor 模式实践

Golang进阶



groutine协程并发

概念梳理

Go 语言最具代表性的核心特性之一,就是其轻量级用户态协程——Goroutine。在深入理解 goroutine 之前,我们先回顾一下协程的基本概念以及它们解决的并发痛点。

1.单线程与多线程的限制

(1)单线程模型:在早期单核系统中,计算机只能顺序执行单一任务。当遇到 I/O 阻塞时,整个线程只能等待,导致 CPU 空转,浪费了大量计算资源。

在这里插入图片描述

(2)多线程/多进程:为提升 CPU 利用率,引入了多线程/多进程并发模型。通过操作系统的时间片轮转机制,多个线程/进程在逻辑上“同时”执行,实则在 CPU 核心间快速切换:

  • 优点:当一个线程阻塞时,CPU 可以调度其他线程执行,提升总体利用率。
  • 缺点:频繁的上下文切换带来额外开销(保存/恢复寄存器状态、内核态切换、线程栈空间大(通常几 MB)等),尤其在高并发场景下切换成本呈指数增长,影响整体性能。

在这里插入图片描述

在这里插入图片描述


2.协程模型演进

为降低多线程模型下的切换和调度开销,业界引入了用户态协程(coroutine)模型,其核心思想是将调度逻辑上移到用户态,避开内核态的频繁切换。常见调度模型对比:

  • 1:1 模型:每个用户线程绑定一个内核线程,调度仍完全依赖操作系统调度器,无法解决内核态切换开销。

  • N:1 模型:多个用户协程复用一个内核线程,切换由用户态调度器管理,内核无感,极大减少上下文切换开销。但当某个协程执行系统调用或纯阻塞操作时,会阻塞其所在的内核线程,导致所有复用该线程的协程均被阻塞。

  • M:N 模型:M 个内核线程复用 N 个用户协程,调度逻辑由语言运行时管理,能充分利用多核 CPU 并缓解阻塞问题。Go 语言采用的是 M:N 模型,并通过一套自研的调度器设计高效规避了 N:1 模型的典型阻塞痛点:

    • P 与 M 解耦:当 goroutine 阻塞时,调度器会将逻辑处理器 P 从阻塞的内核线程 M 中分离,并迁移到其他空闲或新建线程继续调度其他 goroutine,避免阻塞扩散。
    • 非阻塞 I/O 封装:Go 运行时内部将大部分系统调用(如网络、文件 I/O)封装为非阻塞模型,结合内置的网络轮询器(netpoller)机制,在用户态实现高效的 I/O 多路复用。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

Go 在实现 goroutine 时,不仅更名为 “Goroutine”,更在核心设计上做了优化:

  • 内存占用更小:每个 goroutine 栈空间通常只占用几 KB,且支持按需动态扩展,相比传统线程动辄几 MB 的栈空间大幅降低内存压力,进程甚至可能达到几GB。
  • 调度开销更低:轻量化特性让调度器可以频繁快速地切换执行 goroutine,整体并发性能得到大幅提升。

在这里插入图片描述

Go语言早期的调度器设计存在较大问题。如下图所示:其中 G 表示 Goroutine,M 表示操作系统线程。早期调度器的做法是:假设有一个 4 核 CPU,它维护一个全局队列,所有新创建的 Goroutine 都被加入到这个全局队列中。每个线程(M)在执行时,会先获取全局队列的锁,拿到一个 Goroutine 执行。执行后,其余未执行的 Goroutine 会被移动到队列头,等待下一个线程调度;执行完成的 Goroutine 会被放回队列尾部。整个流程简单粗暴,但存在以下明显缺陷:

  • 激烈的锁竞争:创建、销毁和调度 Goroutine 时,所有线程都需获取全局队列的锁,导致频繁的同步与阻塞,性能大幅下降。

  • 任务迁移延迟:如果某个 M 在运行 G 时新创建了 G’,理论上希望 G’ 也在当前 M 上执行以保持数据局部性。但由于使用全局队列,G’ 可能被其他 M 取走,增加了切换成本,降低了缓存命中率。

  • 系统调用频繁:线程在切换、阻塞与唤醒过程中,频繁进行系统调用,进一步增加了系统开销。

在这里插入图片描述


3.Go 的 GMP 模型

Go 采纳 M:N 模型,并引入了 G(goroutine)、M(machine,内核线程)、P(processor,逻辑处理器) 三元结构──GMP 模型:

  • G:轻量级协程,初始栈大小仅几 KB,按需动态增长,内存占用极小。

  • M:操作系统线程,真正执行 goroutine 的载体。

  • P:逻辑处理器,调度器核心单元,,持有本地任务队列(本地 runnable G 列表),决定哪个 G 由哪个 M 执行。P 的数量由 GOMAXPROCS 环境变量决定,最多并行(注意是并行而非并发)运行 P 个协程。

此外,还维护一个全局队列用于存放溢出的 goroutine,保证负载均衡。新创建的 goroutine 优先放入其所属 P 的本地队列,若本地队列已满,才会转移到全局队列,确保整体调度平衡。全队列还有一个锁的保护,所以从全队列取东西效率会比较慢一些。

在这里插入图片描述

在这里插入图片描述


4.Go 调度器的关键策略

Go调度器的设计包含四大核心策略:线程复用、并行利用、抢占机制、全局G队列。下面分别说明:

(1)线程复用(Work Stealing与Hand Off机制)

Go通过复用线程提升调度效率,主要依靠Work Stealing与Hand Off两种机制:

  • Work Stealing(工作窃取)
    每个P(Processor)有自己的本地G队列。当某个M(Machine)空闲时,它会从其他 P 的本地队列尾部"窃取"任务,充分提升资源利用率与并行度,避免任务堆积或线程空闲。
    在这里插入图片描述

  • Hand Off(让渡机制)
    当运行中的G发生阻塞(如IO或锁等待),绑定其所在P的M会尝试将P迁移给其他可用的M(新建或唤醒线程),继续执行本地队列中的其他G任务。阻塞的M进入休眠,待阻塞解除后再参与调度。该机制确保阻塞不会影响其他G的执行,最大化CPU利用率。

在这里插入图片描述

(2)并行利用

  • 通过设置 GOMAXPROCS 控制 P 数量,合理分配 CPU 资源。

  • 比如在 8 核 CPU 下,若将 GOMAXPROCS 设为 4,Go 运行时仅会使用 4 核心资源,剩余可供其他系统任务使用,提供良好的资源隔离能力。

(3)抢占机制

传统协程调度依赖协程主动让出CPU,容易导致长时间占用。Go 从 1.14 版本起引入强制抢占机制:每个G最多运行约10ms,无需其主动让出,调度器可强制将CPU分配给其他等待的G。此设计保证了调度公平性和系统响应性,避免某些G长期独占CPU。

在这里插入图片描述

(4)全局G队列

在每个P的本地队列之外,Go还维护一个全局G队列作为任务缓冲。新创建的G优先进入本地队列,若本地已满才进入全局队列。空闲的M在本地与其他P的队列均无任务时,最后尝试从全局队列取任务。全局队列的访问需要加锁,相比本地队列性能略低,但作为兜底机制,保障了任务分配的完整性与平衡性。



总结一下Go 调度器的关键策略:

  • 1.线程复用(Work Stealing & Hand Off)
    • 工作窃取:当某个 P 的本地队列空闲时,会从其它 P 窃取可执行的 G,避免某些线程闲置。
    • P 与 M 分离(Hand Off):当执行中的 G 阻塞(如网络 I/O),调度器会将对应的 P 从当前 M 分离,挂载到其他空闲或新建的 M 上,保持剩余 G 在本地队列不中断执行。

2.并行 通过 GOMAXPROCS 设置 P 的数量,决定最大并行协程数,灵活利用多核 CPU。

3.抢占 Go 从 1.14 起支持协程抢占,当某个 G 占用 CPU 超过一定时间(约 10 ms)或出现函数调用边界时,可强制调度,避免单个 G 长期占用,保证所有 G 的公平执行。

4.本地与全局队列 大部分 G 都存放在 P 的本地队列,只有在本地队列满时才会入全局队列。空闲时优先窃取本地队列,只有在无其他可用 G 时才访问全局队列,降低全局锁竞争。


小结 —— 为什么 Goroutine 如此高效?

  • 低内存开销:初始栈极小,且支持动态伸缩,百万级并发成为可能;

  • 高效调度:用户态调度极大减少内核切换次数,整体并发性能远优于传统线程;

  • 抢占式公平性:保证调度不会被单个 goroutine 长时间垄断;

  • 本地+全局队列:高效的本地队列配合全局队列兜底,确保任务平衡与快速分发;

  • I/O 封装优化:大部分阻塞 I/O 在用户态实现了非阻塞封装,极大缓解系统调用瓶颈。



创建goroutine语法

如下代码所示,通过go关键字创造goroutine

package mainimport ("fmt""time"
)// 一个用于演示的子goroutine任务函数,不断地每秒打印当前计数值。
func newTask() {i := 0for {i++fmt.Printf("new Goroutine : i = %d\n", i) // 其中 %d 表示格式化为十进制整数time.Sleep(1 * time.Second)               // // 通过 time.Sleep 让当前 goroutine 休眠 1 秒钟}
}// main 函数是 Go 程序的入口函数,同时它本身就是一个 goroutine(称为主 goroutine)
func main() {// 通过 go 关键字创建一个新的 goroutine,去异步执行 newTask() 函数go newTask()// 此处主 goroutine 继续往下执行,不会等待 newTask 执行结束fmt.Println("main goroutine exit")i := 0for {i++// 主 goroutine 也每秒打印一次当前计数值fmt.Printf("main goroutine: i = %d\n", i)time.Sleep(1 * time.Second)}// 1. 在 Go 语言中,使用 go 关键字可以在运行时动态创建新的 goroutine(轻量级线程)。//    Go 运行时会负责调度多个 goroutine,通常在一个或多个操作系统线程上并发执行。//// 2. 主 goroutine 退出时,整个进程随之结束,所有其他子 goroutine 无论是否完成都会被强制终止。//    因此,如果将上面的 for 循环注释掉,仅执行 fmt.Println 后主函数直接退出,//    那么子 goroutine newTask 也无法执行或只执行极短时间后被终止。//// 3. 在实际项目中,如果希望主 goroutine 等待其他 goroutine 执行结束,可以使用 sync.WaitGroup、//    channel 或 context 等机制来实现 goroutine 之间的同步与协调。
}

实际上在承载一个go程的时候不一定要把go程写为一个定义好的函数,我们直接写一个匿名函数去加载也可以,这里演示一下:

package mainimport ("fmt""runtime""time"
)// 本示例主要演示了在 Go 语言中:
// 1. 使用匿名函数(函数字面量)直接创建 goroutine;
// 2. 使用 runtime.Goexit() 退出当前 goroutine;
// 3. 说明 goroutine 函数中无法直接返回值给调用者。func main() {// 使用 go 关键字创建 goroutine,并在其中定义并调用匿名函数(没有参数和返回值)go func() {defer fmt.Println("A.defer") // 延迟执行,在当前匿名函数退出时执行// 内层匿名函数func() {defer fmt.Println("B.defer") // 延迟执行,在当前匿名函数退出时执行// 如何在go程中退出当前goroutine? 用runtime.Goexit()// runtime.Goexit() 用于立即终止当前 goroutine 的执行。// 注意:它只终止当前 goroutine,不会影响其他 goroutine,包括主 goroutine。// 此外,它在退出时仍会调用所有已注册的 defer 函数(类似于正常退出时的清理逻辑)。// 因此 "B.defer" 会被打印,而 "B" 不会被打印。// 注意如果这里是用return的话 只是退出了当前函数调用栈帧 "A"仍会被打印runtime.Goexit()// 由于上面调用了 Goexit(),所以下面这句不会被执行:fmt.Println("B")}() // 如果只是写这个函数,就只是定义了但没被调用,加个()等于我定义了这么一个函数,同时调用起来// 调用时我们没有传递任何参数,因为这里的函数定义就没有任何参数// 由于外层 goroutine 也被 Goexit() 终止了,因此这句也不会被执行:fmt.Println("A")// runtime.Goexit() 并不是像 return 那样只退出当前函数调用栈帧,// 它直接终止整个当前 goroutine,跳出所有调用栈,当然 defer 仍然会执行。}()// 使用匿名函数创建并立即调用带参数的 goroutinego func(a int, b int) bool {fmt.Println("a = ", a, ", b = ", b)return true}(10, 20) // 这里匿名函数定义后立刻通过()调用,并传入参数 10 和 20// 即使匿名函数有返回值 (bool),但由于 goroutine 是并发执行的,无法通过 return 直接获取结果/* 补充说明:- Go 语言中不支持像 flag := go func() bool {...}() 这样的语法,因为 go 关键字启动的 goroutine 是异步执行的,其返回值不会传递回主 goroutine。- goroutine 之间默认无法返回值或传递数据,若要实现结果返回或通信,需要借助 channel、sync 包或 context 机制来实现同步与通信。*/// 死循环用于防止 main goroutine 提前退出,确保前面创建的 goroutine 有机会执行完毕for {time.Sleep(1 * time.Second)}
}

在 Go 语言中,main 函数的退出意味着整个程序的结束。所以如果 main 函数提前退出,所有未执行完的子 goroutine 会立即被强制终止。在实际应用中,通常不建议用死循环阻塞主 goroutine,可以使用 sync.WaitGroup 更优雅地等待子 goroutine 结束。这里写一份goroutine + WaitGroup 基础通用模板:

package mainimport ("fmt""sync""time"
)// 子任务函数:可以传参,支持 defer、panic 恢复等
func worker(id int, wg *sync.WaitGroup) {defer wg.Done() // 每启动一个 goroutine,结束时必须调用 Done()// panic 保护(可选,但建议加上,避免单个 goroutine 崩溃导致全局异常)defer func() {if err := recover(); err != nil {fmt.Printf("Worker %d recovered from panic: %v\n", id, err)}}()fmt.Printf("Worker %d start\n", id)// 模拟任务执行时间time.Sleep(time.Duration(id) * time.Second)fmt.Printf("Worker %d done\n", id)
}func main() {var wg sync.WaitGroupnumWorkers := 5 // 启动 5 个并发任务for i := 1; i <= numWorkers; i++ {wg.Add(1) // 每个任务启动前,先增加计数go worker(i, &wg)}// 阻塞等待所有子 goroutine 完成wg.Wait()fmt.Println("所有任务执行完毕,主程序退出")
}

wg.Add(1) : 每个 goroutine 启动前,先登记 1 个待完成任务
defer wg.Done(): 每个 goroutine 执行完后自动减一,防止漏掉
recover() :捕获 panic,避免整个程序因某个 goroutine 崩溃
wg.Wait() : 阻塞主 goroutine,直到所有登记的任务完成
time.Sleep() : 模拟任务处理时间,实际可替换成任何逻辑



channel实现goroutine之间通信

channel是Go语言中的一个核心数据类型,可以把它看成管道,,主要用来解决go程的同步问题以及go程之间数据共享(数据传递)的问题。并发核心单元通过它就可以发送或者接收数据进行通讯,这在一定程度上又进一步降低了编程的难度。

goroutine运行在相同的地址空间,因此访问共享内存必须做好同步。goroutine 奉行通过通信来共享内存,而不是共享内存来通信。

下面我们学习一下channel的基本用法:

package mainimport "fmt"func main() {// 定义一个 channel,用于传递 int 类型的数据。// 这里使用的是无缓冲(unbuffered)channel:只能同时存放一个数据。// 当向无缓冲 channel 发送数据时,发送操作会阻塞直到有其他 goroutine 从 channel 中接收数据。c := make(chan int)// 启动一个新的 goroutine(协程,相当于一个轻量级线程)。// channel 通常用于多个 goroutine 之间的通信,这里就是 main goroutine 和新开启的 goroutine 之间的通信。go func() {// 在函数退出时输出一句话,表明这个 goroutine 结束了defer fmt.Println("goroutine结束")fmt.Println("goroutine 正在运行...")// 向 channel 中发送数据:666// 发送操作:c <- 666// 因为 channel 是无缓冲的,如果 main goroutine 没有准备好接收数据,发送操作会阻塞在这里c <- 666 //将666发送给c 这个是发送的语法}()// 从 channel 中接收数据:<-c// 这个接收操作会阻塞,直到有数据被发送到 channel 中// 接收到的数据赋值给变量 numnum := <-c //从c中接受数据,并赋值给num 这个是接收的语法// - <-c 是接收操作,把 channel 中的数据取出// - <-c 也可以单独写成:<-c 只取出数据而不保存(丢弃)//   例如: <-c  // 取出数据但不保存任何变量中,数据被丢弃fmt.Println("num = ", num) // num =  666fmt.Println("main goroutine 结束...")
}

这里因为使用的是无缓冲channel,当向无缓冲 channel 发送数据时,发送操作会阻塞直到有其他 goroutine 从 channel 中接收数据,接收操作会阻塞,直到有数据被发送到 channel 中。


在 Go 语言中,channel 分为无缓冲(unbuffered)和有缓冲(buffered)两种。

  • 无缓冲 channel:

    • 发送和接收必须同步进行。

    • 发送操作会阻塞,直到有接收者从 channel 中取走数据;接收操作也会阻塞,直到有发送者发送数据。

    • 适用于需要确保发送方与接收方同步的场景,常用于协程之间的同步控制。

  • 有缓冲 channel:

    • 在内部有一个有限的缓冲区,可以容纳一定数量的元素。

    • 发送操作在缓冲未满时不会阻塞;只有当缓冲区满时才会阻塞发送方。

    • 接收操作在缓冲非空时不会阻塞;只有当缓冲区为空时才会阻塞接收方。

    • 适用于发送和接收速度不完全匹配的场景,可以提升一定的并发性能和吞吐能力。

  • 简单来说:无缓冲更偏向同步,有缓冲更偏向异步


下面我们测试一下有缓冲channel的效果:

package mainimport ("fmt""time"
)func main() {// 创建一个带缓冲区的 channel,类型为 int,缓冲区大小为 3。// 这意味着最多可以缓存 3 个尚未被接收的元素。c := make(chan int, 3)// 打印当前 channel 的长度和容量:// len(c): 当前缓冲区中已有的数据个数(初始为 0)// cap(c): 缓冲区总容量(此处为 3)fmt.Println("len(c) = ", len(c), ", cap(c)", cap(c)) // 输出: len(c) = 0 , cap(c) = 3// 启动一个新的 goroutine 来向 channel 中发送数据go func() {defer fmt.Println("子go程结束") // 在函数结束时自动打印,标记子 goroutine 结束// 循环向 channel 中发送 4 个整数(注意:发送次数 > 缓冲区容量)for i := 0; i < 4; i++ {c <- i // 发送数据到 channelfmt.Println("子go程正在运行, 发送的元素=", i, " len(c)=", len(c), ", cap(c)=", cap(c))}}()// 主 goroutine 休眠 2 秒,确保子 goroutine 有时间执行发送操作// 这只是为了演示方便,实际中应使用同步机制(如 wait group)time.Sleep(2 * time.Second)// 从 channel 中依次取出 4 个元素(注意:实际发送了 4 个元素)for i := 0; i < 4; i++ {num := <-c //从c中接收数据,并赋值给numfmt.Println("num = ", num)}fmt.Println("main 结束")
}

运行结果为:
len© = 0 , cap© 3
子go程正在运行, 发送的元素= 0 len©= 1 , cap©= 3
子go程正在运行, 发送的元素= 1 len©= 2 , cap©= 3
子go程正在运行, 发送的元素= 2 len©= 3 , cap©= 3
num = 0
num = 1
num = 2
num = 3
main 结束


一开始 len© 是 0,因为还没有任何数据发送到 channel。
子 goroutine 发送前 3 个元素时:因为缓冲区容量为 3,每次发送成功后,缓冲区长度 len© 依次变为 1、2、3。此时发送都是非阻塞的(因为缓冲区未满)。
当尝试发送第 4 个元素(i=3)时:缓冲区已满,发送操作阻塞,直到主 goroutine 从 channel 中读取数据,腾出空间。由于主 goroutine 在 time.Sleep 中睡眠,子 goroutine 此时会卡在 c <- i 第 4 次发送这里,等待空间腾出。
睡眠结束后,主 goroutine 依次从 channel 中读取 4 个数据:前 3 个立即取出缓冲区中的数据(0、1、2)。取出第 3 个数据时,缓冲区变为不满,子 goroutine 解除阻塞,成功发送最后一个元素 3。主 goroutine 继续取出最后一个数据 3。
所有数据接收完成后,程序结束。


介绍了有缓冲和无缓冲channel的基本定义与使用后,我们再来看看channel的关闭特点:

package mainimport "fmt"// 在Go语言中,channel不像文件那样需要频繁关闭;通常只有以下两种情况需要关闭:
// 1. 确定不再向channel发送任何数据了(即:发送方完成了全部发送任务)。
// 2. 想要通过关闭channel通知接收方,配合range、for-select等结构优雅退出。
// 注意:关闭channel只是禁止继续发送数据(引发panic错误后导致接收立即返回零值);
//	而接收数据仍然是允许的,直到channel被完全读空。
// 另外:nil channel(值为nil的channel)在收发操作时都会永久阻塞。
func main() {c := make(chan int) // 创建一个无缓冲的整型channel,类型为chan intgo func() { // 启动一个匿名goroutine作为发送者for i := 0; i < 5; i++ { // 向channel中发送5个整数:0到4c <- i // 向channel发送数据,若没有接收方则会阻塞//close(c) // 注意:如果在这里关闭channel,将在第一次发送后关闭,再发送时panic!}//close可以关闭一个channelclose(c) // 循环发送完所有数据后,关闭channel,通知接收方:不会再有新的数据发送进来了}()for { // 启动主goroutine作为接收者// 这里使用了逗号ok的惯用写法:data接收从channel读取的数据// ok为布尔值,若channel未关闭或还有数据,ok为true;当channel关闭且数据读完后,ok返回falseif data, ok := <-c; ok { // channel仍然有数据可以读取fmt.Println(data)} else { // channel已关闭且数据读完,退出循环break}}fmt.Println("Main Finished..")// 如果不在子程里调用close(c) 或不在子goroutine里发送数据 // 如果不在子goroutine里发送数据,而直接在主goroutine中执行接收// 由于主goroutine会阻塞在 <-c ,而没有其他goroutine发送数据,最终会导致:// fatal error: all goroutines are asleep - deadlock// 这是因为Go运行时检测到了所有goroutine都阻塞,程序无法继续执行,因此直接panic报死锁。
}

这里if data, ok := <-c; ok { 里面有个分号,这是 Go 语言里 “if 语句支持短变量声明” 的语法,在 Go 里,if 语句可以有两部分:

if 简短变量声明; 条件判断 {// ...
}

也就是说:分号 ; 把变量声明和条件判断隔开。if 语句执行时,先执行分号前面的短变量声明(这里是 `data, ok := <-c`),然后判断分号后面的条件(这里是 `ok`)。这句代码拆开理解就是:
data, ok := <-c  // 从channel接收数据,同时判断channel是否已关闭
if ok {fmt.Println(data)
}    

但是因为 Go 允许你把声明写在 if 里,就可以缩写成一行



channel与range、select

下面我们再看一下channel跟两个比较特殊的关键字的配合使用

channel与range

package mainimport "fmt"func main() {c := make(chan int) // 创建一个无缓冲的整型channel,类型为chan intgo func() { // 启动一个匿名goroutine作为发送者for i := 0; i < 5; i++ { // 向channel中连续发送5个整数:0到4c <- i // 发送数据到channel,若无接收方会阻塞等待}// 发送完所有数据后,关闭channel,关闭channel的作用是通知接收方:不会再有新的数据了close(c)}()// =================== 之前写法(手动 for + ok 检查) ===================/*	for { // 启动主goroutine作为接收者// 这里使用了逗号ok的惯用写法:data接收从channel读取的数据// ok为布尔值,若channel未关闭或还有数据,ok为true;当channel关闭且数据读完后,ok返回falseif data, ok := <-c; ok { // channel仍然有数据可以读取fmt.Println(data)} else { // channel已关闭且数据读完,退出循环break}}*/// =================== 更简洁的写法:使用range迭代channel ===================// 使用range可以自动从channel中不断接收数据,直到channel被关闭且数据读空后自动退出// 注意:只有关闭了channel,range才能正常结束,否则会一直阻塞等待新数据for data := range c {fmt.Println(data)}// 本质上两种代码逻辑一样,但写法不同。fmt.Println("Main Finished..")// 总结:// 1. for + ok 写法:更通用,能灵活处理接收结果、区分接收失败(例如关闭时返回零值和ok=false)// 2. range 写法:语法更简洁,适用于简单读取全部channel数据直到关闭// 3. 不管哪种写法,关闭channel后都无法再向其中发送数据,否则panic// 4. 未关闭channel时,range会一直阻塞等待,容易导致程序卡死(死锁)
}


channel与select

单流程下一个go只能监控一个channel的状态,select可以完成监控多个channel的状态:

在这里插入图片描述

package mainimport "fmt"// 定义一个生成斐波那契数列的函数,使用channel与select控制流程
func fibonacii(c, quit chan int) {x, y := 1, 1 // 斐波那契数列的前两个数for {select {// select语句可以同时监听多个channel的通信状态// 当某个case对应的channel准备好后(发送/接收不再阻塞),select就会执行对应的casecase c <- x:// 当c可写时(即:有人在接收c的数据时),就会进入这个case// 把当前的x发送到channel c中// 然后计算下一个斐波那契数x = yy = x + ycase <-quit:// 当从quit channel中接收到数据时(不关心数据内容,所以直接用<-quit)// 表示收到停止信号,打印"quit",退出函数fmt.Println("quit")return // return,当前goroutine结束}}
}func main() {// 创建两个无缓冲channel:// c 用于传递斐波那契数列数据// quit 用于通知fibonacci函数何时退出c := make(chan int)quit := make(chan int)// 启动一个子goroutine负责消费fibonacci生成的数列数据go func() {for i := 0; i < 10; i++ {// 每次从c中接收一个数据并打印fmt.Println(<-c)}// 接收完10个数据后,通知fibonacci函数可以停止了quit <- 0}()// 主goroutine调用fibonacci函数,开始生成数据// 注意:该函数内是一个无限循环,直到收到quit信号才会退出fibonacii(c, quit)
}

用你更熟悉的 Java switch 来对比着帮你彻底讲清楚:
一句话总结:Go 的 select 每次执行时,先扫描所有 case 中的 channel,如果有一个或多个可以立即执行的,就随机选择其中一个执行(注意:真的随机,不是顺序!);一旦选定执行一个 case,本轮 select 立即结束,不会执行其他 case。如果没有任何 case 满足条件:如果有 default,则直接执行 default;如果没有 default,则整个 select 阻塞等待,直到至少有一个 case 满足条件。注意:只在所有case都无法执行时才会进入default。

每次 select 执行一轮:
+-----------------------------+
| 检查每个 case 是否 ready    |
+-----------------------------+↓有多个ready? ——→ 是 ——→ 随机选1个执行↓否↓是否有default? ——→ 有 ——→ 执行default↓没有↓阻塞等待

补充一点底层:
Go select 底层其实和调度器有关:Go runtime 会维护一个 goroutine 等待队列;
每当执行 select,实际上在 runtime 层面做了一次channel 状态 polling(检测收发是否能立即完成);
只要有任意一个 channel ready,就从 ready set 里随机取一个执行;
所以它既像“非阻塞的多路复用器”,也像是轻量的“并发调度器”——这也是为什么 Go select 很适合用来做高性能并发通信控制的原因。



GoModules

Go Modules与GOPATH

1.什么是Go Modules?

Go modules 是 Go 语言官方推荐的依赖管理工具,自 Go 1.11 引入,Go 1.13 后功能基本完善,在 Go 1.16 开始默认启用,完全取代了早期的 GOPATH 模式。

在 Go 1.11 之前,Go 一直依赖 GOPATH 进行代码组织和依赖管理,但存在诸多痛点:

  • 缺乏版本控制机制;
  • 不便于多个项目管理不同版本依赖;
  • 无法轻松复现项目依赖环境;
  • 不支持私有模块、镜像代理、校验等高级功能。

Go modules 彻底解决了这些问题,成为 Go 语言现代化开发的标配。


2.GOPATH的工作模式

Go Modoules的目的之一就是淘汰GOPATH, 那么GOPATH是个什么?为什么不再推荐 GOPATH 的模式了呢?

(1) What is GOPATH?

$ go envGOPATH="/home/itheima/go"
...

我们输入go env命令行后可以查看到 GOPATH 变量的结果,我们进入到该目录下进行查看,如下:

go
├── bin # 可执行文件
├── pkg # 预编译缓存
└── src # 所有源码(项目 & 第三方库)├── github.com├── golang.org├── google.golang.org├── gopkg.in....

GOPATH目录下一共包含了三个子目录,分别是:

  • bin:存储所编译生成的二进制文件。
  • pkg:存储预编译的目标文件,以加快程序的后续编译速度。
  • src:存储所有.go文件或源代码。在编写 Go 应用程序,程序包和库时,一般会以$GOPATH/src/github.com/foo/bar的路径进行存放。

因此在使用 GOPATH 模式下,我们需要将应用代码存放在固定的$GOPATH/src目录下,并且如果执行go get来拉取外部依赖会自动下载并安装到$GOPATH目录下。


(2) GOPATH模式的弊端

在 GOPATH 的 $GOPATH/src 下进行 .go 文件或源代码的存储,我们可以称其为 GOPATH 的模式,这个模式拥有一些弊端:

  • 没有版本控制:go get 无法指定具体版本,只能拉取最新。

  • 依赖不可复现:团队成员很难保持依赖版本一致。

  • 无法支持模块多版本共存:如 v1/v2 无法同时存在,容易出现包冲突。



Go Modules模式

我们接下来用Go Modules的方式创建一个项目, 建议为了与GOPATH分开,不要将项目创建在$GOPATH/src下.

(1) 常用go mod命令

命令作用
go mod init初始化 Go 项目并创建 go.mod 文件
通常在你开始一个新的 Go 项目时使用
go getgo get 用于获取并安装 Go 依赖的包,通常用于下载依赖、更新依赖版本,或者安装可执行包
这个命令通常用于添加新的依赖,或更新已安装的依赖。
go mod download下载 go.mod 中声明的依赖
你可以在从代码仓库 pull 最新代码后使用该命令来确保本地已经下载了项目中声明的所有依赖。
go mod tidy整理依赖、清理未使用的依赖
这条命令非常常用,可以帮助你保持 go.mod 和 go.sum 文件的干净整洁。你可以在:新增依赖时使用;从代码仓库 pull 后,执行这条命令来清理任何不再使用的依赖;在你修改了代码后,删除了一些不再需要的包时运行;在 push 代码之前使用,确保没有冗余依赖。
go mod graph查看项目的依赖图,了解哪些模块依赖于哪些其他模块
例如,你遇到了一些版本冲突或依赖错误时,这个命令可以帮助你查看依赖关系,找到冲突的根源。
go mod edit手动编辑 go.mod 文件,可以修改模块名称、添加模块或调整模块版本等
这个命令较少直接使用 ,如果需要手动指定版本号,或处理一些模块相关的高级需求时可以使用。
go mod vendor导出项目所有的依赖到vendor目录(依赖本地化)
在需要保证依赖的稳定性时使用,尤其是当你需要在没有网络连接的环境中工作,或者将代码部署到不稳定网络的环境中时。通常,大型公司或团队项目会使用该命令来确保依赖的完整性。
go mod verify验证 go.mod 中列出的依赖是否完整,检查模块是否被篡改
用于验证依赖的完整性,确保 go.modgo.sum 文件中的依赖没有被篡改,常在 CI/CD 流程中使用,确保代码的安全性。
go mod why查看某个依赖为何被引用,帮助你了解该依赖的使用情况
当你发现某个模块在 go.mod 中被列出,但你不清楚为什么需要这个依赖时,可以用 go mod why 查看其引用来源。它能够帮助你跟踪依赖链,尤其是复杂项目中的依赖分析。

可以通go mod help查看学习这些指令,强烈建议多用 go mod tidy,随时清理无效依赖,保持 go.mod & go.sum 干净整洁。

go getgo mod download 的区别:
go get:用于获取并安装依赖,可能会更新 go.mod 中的依赖版本。
go mod download:只会下载 go.mod 中列出的依赖,不会更新或修改任何文件,只保证依赖的存在。
简单来说,go get 主要用于获取和安装新的依赖,并可能改变项目依赖版本,而 go mod download 只是用来确保下载 go.mod 文件中列出的所有依赖。

(2) go mod环境变量

可以通过 go env 命令来进行查看:

$ go env
GO111MODULE="auto"
GOPROXY="https://proxy.golang.org,direct"
GONOPROXY=""
GOSUMDB="sum.golang.org"
GONOSUMDB=""
GOPRIVATE=""
...

GO111MODULE

Go语言提供了 GO111MODULE这个环境变量来作为 Go modules 的开关,(Go 1.16 及以后默认已废弃该变量,默认就是on),其允许设置以下参数:

  • auto:在含有 go.mod 时启用,目前在 Go1.11 至 Go1.14 中仍然是默认值。
  • on:始终启用 Go modules(推荐),未来版本中的默认值。
  • off:全禁用 Go modules(不推荐)。

可以通过下面的命令来设置:

$ go env -w GO111MODULE=on

GOPROXY

这个环境变量主要是用于设置 Go 模块代理(Go module proxy),其作用是用于使 Go 在后续拉取模块版本时直接通过镜像站点来快速拉取。

GOPROXY 的默认值是:https://proxy.golang.org,direct
proxy.golang.org国内访问不了,需要设置国内的代理

阿里云:https://mirrors.aliyun.com/goproxy/
七牛云: https://goproxy.cn,direct

如:

$ go env -w GOPROXY=https://goproxy.cn,direct

GOPROXY 的值是一个以英文逗号 “,” 分割的 Go 模块代理列表,允许设置多个模块代理,假设你不想使用,也可以将其设置为 “off” ,这将会禁止 Go 在后续操作中使用任何 Go 模块代理。

设置多个模块代理:

$ go env -w GOPROXY=https://goproxy.cn,https://mirrors.aliyun.com/goproxy/,direct

而在刚刚设置的值中,我们可以发现值列表中有 “direct” 标识,它又有什么作用呢?
实际上 “direct” 是一个特殊指示符,用于指示 Go 回源到模块版本的源地址去抓取(比如 GitHub 等),场景如下:当值列表中上一个 Go 模块代理返回 404 或 410 错误时,Go 自动尝试列表中的下一个,遇见 “direct” 时回源,也就是回到源地址去抓取,而遇见 EOF 时终止并抛出类似 “invalid version: unknown revision…” 的错误。


GOSUMDB

它的值是一个 Go checksum database,用于在拉取模块版本时(无论是从源站拉取还是通过 Go module proxy 拉取)保证拉取到的模块版本数据未经过篡改,若发现不一致,也就是可能存在篡改,将会立即中止。

GOSUMDB 的默认值为:sum.golang.org,在国内也是无法访问的,但是 GOSUMDB 可以被 Go 模块代理所代理,即GOPROXY默认充当这个网站。

因此我们可以通过设置 GOPROXY 来解决,而先前我们所设置的模块代理 goproxy.cn 就能支持代理 sum.golang.org,所以这一个问题在设置 GOPROXY 后,你可以不需要过度关心。

另外若对 GOSUMDB 的值有自定义需求,其支持如下格式:

  • 格式 1:<SUMDB_NAME>+<PUBLIC_KEY>
  • 格式 2:<SUMDB_NAME>+<PUBLIC_KEY> <SUMDB_URL>

也可以将其设置为“off”,也就是禁止 Go 在后续操作中校验模块版本,不推荐。


GONOPROXY/GONOSUMDB/GOPRIVATE

这三个环境变量都是用在当前项目依赖了私有模块,例如像是你公司的私有 git 仓库,又或是 github 中的私有库,都是属于私有模块,都是要进行设置的,否则会拉取失败。

更细致来讲,就是依赖了由 GOPROXY 指定的 Go 模块代理或由 GOSUMDB 指定 Go checksum database 都无法访问到的模块时的场景。

而一般建议直接设置 GOPRIVATE,它的值将作为 GONOPROXY 和 GONOSUMDB 的默认值,所以建议的最佳姿势是直接使用 GOPRIVATE。

并且它们的值都是一个以英文逗号 “,” 分割的模块路径前缀,也就是可以设置多个,例如:

$ go env -w GOPRIVATE="git.example.com,github.com/eddycjy/mquote"

如果不想每次都重新设置,还支持通配符:

$ go env -w GOPRIVATE="*.example.com"

设置后,后缀为 .example.com 的模块都会被认为是私有模块,都不会经过GOPROXY并经过GOSUMDB检验。需要注意的是不包括 example.com 本身



用Go Modules初始化项目

(1) 开启Go Modules

 $ go env -w GO111MODULE=on

又或是可以通过直接设置系统环境变量(写入对应的~/.bash_profile 文件亦可)来实现这个目的:

$ export GO111MODULE=on

(2) 初始化项目

创建项目目录

$ mkdir -p $HOME/aceld/modules_test
$ cd $HOME/aceld/modules_test

我们后面会在modules_test下写代码,首先要执行Go modules 初始化的工作,如下所示,会在本地创建一个go.mod文件。go mod init后面要跟一个当前模块的名称,这个名称是自定义写的,这个名称他决定于今后导包的时候,即其他人import的时候怎么写

$ go mod init github.com/aceld/modules_testgo: creating new go.mod: module github.com/aceld/modules_test

生成的 go.mod:

module github.com/aceld/modules_testgo 1.14

在执行 go mod init 命令时,我们指定了模块导入路径为 github.com/aceld/modules_test。接下来我们在该项目根目录下创建 main.go 文件,如下:

package mainimport ("fmt""github.com/aceld/zinx/znet""github.com/aceld/zinx/ziface"
)//ping test 自定义路由
type PingRouter struct {znet.BaseRouter
}//Ping Handle
func (this *PingRouter) Handle(request ziface.IRequest) {//先读取客户端的数据fmt.Println("recv from client : msgId=", request.GetMsgID(), ", data=", string(request.GetData()))//再回写ping...ping...pingerr := request.GetConnection().SendBuffMsg(0, []byte("ping...ping...ping"))if err != nil {fmt.Println(err)}
}func main() {//1 创建一个server句柄s := znet.NewServer()//2 配置路由s.AddRouter(0, &PingRouter{})//3 开启服务s.Serve()
}

OK, 我们先不要关注代码本身,我们看当前的main.go也就是我们的aceld/modules_test项目,是依赖一个叫github.com/aceld/zinx库的. znetziface只是zinx的两个模块.

明显我们的项目没有下载刚才代码中导入的那互联网上的两个包,我们只是import导入进来了,如果是之前GOPATH模式的话,应该去GOPATH下的src/git/github.com/aceldgo get下来,或者直接手动下载放在指定目录。
但是我们现在是Go Modules,接下来我们在$HOME/aceld/modules_test,本项目的根目录执行下面的命令,假设我们用到了znet包:

$ go get github.com/aceld/zinx/znetgo: downloading github.com/aceld/zinx v0.0.0-20200221135252-8a8954e75100
go: found github.com/aceld/zinx/znet in github.com/aceld/zinx v0.0.0-20200221135252-8a8954e75100

还有go get github.com/aceld/zinx/ziface,当然你也可以直接把整个模块下载下来:go get github.com/aceld/zinx

这样就会帮我们把代码下载下来了,我们会看到 我们的go.mod被修改,同时多了一个go.sum文件。同时go run main.go也能运行了。


(3) 查看go.mod文件

$HOME/aceld/modules_test/go.mod:

module github.com/aceld/modules_testgo 1.14require github.com/aceld/zinx v0.0.0-20200221135252-8a8954e75100 // indirect

发现多了一段require,表示项目需要一个库github.com/aceld/zinx,版本是v0.0.0-20200221135252-8a8954e75100

我们来简单看一下这里面的关键字

  • module: 用于定义当前项目的模块路径/模块名称,建议填写仓库实际地址
  • go:标识当前Go版本.即初始化版本
  • require: 列出所有直接和间接依赖模块版本
  • // indirect: 示该模块为间接依赖,也就是在当前应用程序中的 import 语句中,并没有发现这个模块的明确引用,有可能是你先手动 go get 拉取下来的,也有可能是你所依赖的模块所依赖的.我们的代码很明显是依赖的"github.com/aceld/zinx/znet"和"github.com/aceld/zinx/ziface",所以就间接的依赖了github.com/aceld/zinx

(4) 查看go.sum文件

在第一次拉取模块依赖后,会发现多出了一个 go.sum 文件,其详细罗列了当前项目直接或间接依赖的所有模块版本,并写明了那些模块版本的 SHA-256 哈希值以备 Go 在今后的操作中保证项目所依赖的那些模块版本不会被篡改。

github.com/aceld/zinx v0.0.0-20200221135252-8a8954e75100 h1:Ez5iM6cKGMtqvIJ8nvR9h74Ln8FvFDgfb7bJIbrKv54=
github.com/aceld/zinx v0.0.0-20200221135252-8a8954e75100/go.mod h1:bMiERrPdR8FzpBOo86nhWWmeHJ1cCaqVvWKCGcDVJ5M=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=

我们可以看到一个模块路径可能有如下两种:

h1:hash情况

github.com/aceld/zinx v0.0.0-20200221135252-8a8954e75100 h1:Ez5iM6cKGMtqvIJ8nvR9h74Ln8FvFDgfb7bJIbrKv54=

go.mod hash情况:

github.com/aceld/zinx v0.0.0-20200221135252-8a8954e75100/go.mod h1:bMiERrPdR8FzpBOo86nhWWmeHJ1cCaqVvWKCGcDVJ5M=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=

h1 hash 是 Go modules 将目标模块版本的 zip 文件开包后,针对所有包内文件依次进行 hash,然后再把它们的 hash 结果按照固定格式和算法组成总的 hash 值。

go.mod hash顾名思义就是对mod文件做一次hash。而 h1 hash 和 go.mod hash 两者,要不就是同时存在,要不就是只存在 go.mod hash。那什么情况下会不存在 h1 hash 呢,就是当 Go 认为肯定用不到某个模块版本的时候就会省略它的 h1 hash,就会出现不存在 h1 hash,只存在 go.mod hash 的情况。

那我们刚刚go get的文件下载到哪了呢?其实是给我们下载到了$GOPATH/pkg/mod/github.com/aceld下面,这样我



修改模块的版本依赖关系

为了作尝试,假定我们现在对zinx版本作了升级, 由zinx v0.0.0-20200221135252-8a8954e75100 升级到 zinx v0.0.0-20200306023939-bc416543ae24 (注意zinx是一个没有打版本tag打第三方库,如果有的版本号是有tag的,那么可以直接对应v后面的版本号即可)

那么,我们是怎么知道zinx做了升级呢, 我们又是如何知道的最新的zinx版本号是多少呢?

先回到$HOME/aceld/modules_test,本项目的根目录执行:

$ go get github.com/aceld/zinx/znet
go: downloading github.com/aceld/zinx v0.0.0-20200306023939-bc416543ae24
go: found github.com/aceld/zinx/znet in github.com/aceld/zinx v0.0.0-20200306023939-bc416543ae24
go: github.com/aceld/zinx upgrade => v0.0.0-20200306023939-bc416543ae24

这样我们,下载了最新的zinx, 版本是v0.0.0-20200306023939-bc416543ae24, 然后,我们看一下go.mod

module github.com/aceld/modules_testgo 1.14require github.com/aceld/zinx v0.0.0-20200306023939-bc416543ae24 // indirect

我们会看到,当我们执行go get 的时候, 会自动的将本地将当前项目的require更新了.变成了最新的依赖.

好了, 现在我们就要做另外一件事,就是,我们想用一个旧版本的zinx. 来修改当前zinx模块的依赖版本号.

目前我们在$GOPATH/pkg/mod/github.com/aceld(可以理解为本地仓库)下,已经有了两个版本的zinx库:

/go/pkg/mod/github.com/aceld$ ls
zinx@v0.0.0-20200221135252-8a8954e75100
zinx@v0.0.0-20200306023939-bc416543ae24

目前,我们/aceld/modules_test依赖的是zinx@v0.0.0-20200306023939-bc416543ae24 这个是最新版, 我们要改成之前的版本zinx@v0.0.0-20200306023939-bc416543ae24.

回到/aceld/modules_test项目目录下,执行:

$ go mod edit -replace=zinx@v0.0.0-20200306023939-bc416543ae24=zinx@v0.0.0-20200221135252-8a8954e75100

然后我们打开go.mod查看一下:

module github.com/aceld/modules_testgo 1.14require github.com/aceld/zinx v0.0.0-20200306023939-bc416543ae24 // indirectreplace zinx v0.0.0-20200306023939-bc416543ae24 => zinx v0.0.0-20200221135252-8a8954e75100

这里出现了replace关键字.用于将一个模块版本替换为另外一个模块版本。

replace和直接修改require的区别: 直接改require版本是可行的,前提是该版本能被正常下载;而replace不仅可以指定版本,也可以把模块替换到本地路径或 fork 地址,功能更强,适合调试/开发/本地模块。



Go Modules 版本号规范

Go Modules 遵循 语义化版本(Semantic Versioning,SemVer) 标准。

1.基本的语义化版本规则

SemVer 格式为:vMAJOR.MINOR.PATCH,如:v1.2.3

  • MAJOR(主版本号):发生不兼容 API 修改时递增;

  • MINOR(次版本号):向后兼容的新功能递增;

  • PATCH(修订号):向后兼容的问题修正递增。

例如:

版本号说明
v1.0.0稳定版本发布
v1.2.0增加了新功能,兼容老版本
v1.2.3修复了某个 bug,兼容老版本
v2.0.0存在破坏性改动,不兼容老版本

2.Go Modules 对 MAJOR 版本的特殊处理

Go Modules 在处理 主版本号 v2 及以上 时,有额外要求:主版本号 v2 及以上,必须在模块路径中加入版本后缀。

例如,假设你有一个库:仓库地址: github.com/foo/bar;当前版本: v1.5.0

当你要发布 v2.0.0 时,模块路径需修改为:module github.com/foo/bar/v2

否则,在使用时会导致依赖拉取异常或不兼容的问题。

# 例子# v1 版本 module 路径
module github.com/foo/bar# v2 版本及以上 module 路径
module github.com/foo/bar/v2

这种设计的好处:保持对旧版本的兼容性;明确标识重大版本分支;避免不同版本冲突。

实践建议:升级到 v2+ 时,务必修改 go.mod 中的 module 路径;发布新版本时,在 Git 中打上对应 tag,例如:v2.0.0;消费方导入时需使用完整路径:

import "github.com/foo/bar/v2/mypkg"

切勿随意跳过版本号规范,否则会导致下游依赖管理困难,尤其在企业内部的库管理中尤为重要。



vendor 模式实践

1.什么是 vendor 模式?

Go Modules 默认采用 proxy 模式 拉取依赖。但在某些场景下,vendor 模式更适合:企业内网,无法访问公网;离线部署,无法实时拉取依赖;安全审计,依赖需提前锁定;持续集成(CI/CD),确保构建稳定性。

vendor 模式即将所有依赖源码复制到本地的 vendor/ 目录中,构建时直接从本地依赖目录读取,无需访问外部网络。

2.如何启用 vendor 模式

生成 vendor 目录:go mod vendor

执行后,会将 go.modgo.sum 中声明的依赖下载并复制到项目下的 vendor/ 目录。

强制使用 vendor 编译:go build -mod=vendor 或者:GOFLAGS=-mod=vendor go build

测试时也可指定使用 vendor:go test -mod=vendor ./...

日常开发中,启用全局 vendor,可在项目根目录设置环境变量:export GOFLAGS=-mod=vendor,这样执行所有 go 命令时,默认启用 vendor 模式。

3.vendor 模式的优缺点

优点缺点
离线构建、部署更可靠占用磁盘空间
防止依赖失效、仓库被删需手动维护同步
方便代码安全审计依赖更新需重新执行 go mod vendor
加速 CI/CD 构建

4.实践建议

  • 建议在企业内网、私有部署等稳定环境下使用 vendor;

  • 建议将 vendor/ 目录纳入版本控制(如 Git);

  • 每次更新依赖后,务必重新执行 go mod vendor,确保同步;

  • 日常开发中,仍可在本地使用默认的 module 模式,避免频繁维护 vendor。



本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/87732.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/87732.shtml
英文地址,请注明出处:http://en.pswp.cn/pingmian/87732.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

无人机3控接力模式技术分析

一、运行方式 1. 接力控制流程 位置触发切换&#xff1a;飞控中心实时监测无人机位置&#xff0c;当进入预设的切换路线&#xff08;如靠近下一个机库或控制器覆盖范围&#xff09;时&#xff0c;触发切换流程。 控制权请求与验证&#xff1a; 当前控制器&#xff08…

Actor Critic对比PGValue-Based

目录 回顾一下policy gradient&#xff1a; QAC算法&#xff1a; A2C- advantage actor critic 问题&#xff1a; 1. 为什么要结合起来&#xff0c;能解决什么问题&#xff1f; 1. 策略梯度 (PG) 的优势与核心问题 2. 基于价值方法 (Value-Based) 的优势与局限性 3. 潜…

buuctf-re

1.findKey 打开是C而且有点乱,所以找关键步骤有一个加密进去是不能反编译的,有花指令, 这里有重复的部分把下面的NOP掉,重新定义函数’p’ 之后分析逻辑, // positive sp value has been detected, the output may be wrong! int __userpurge sub_40191F<eax>(int a1&l…

RuoYi、Vue CLI 和 uni-app 结合构建跨端全家桶方案

将 RuoYi、Vue CLI 和 uni-app 结合构建跨端全家桶方案&#xff0c;可以实现一套代码管理后台系统&#xff08;PC&#xff09;和移动端应用&#xff08;H5/小程序/App&#xff09;。以下是整合思路和关键步骤&#xff1a; 技术栈分工 RuoYi&#xff1a;后端框架&#xff08;Spr…

二十九、windows系统安全---windows注册表安全配置

环境 windows server 2012 原理 注册表简介: 注册表&#xff08;Registry&#xff0c;繁体中文版Windows操作系统称之为登录档&#xff09;是Microsoft Windows中的一个重要的数据库&#xff0c;用于存储系统和应用程序的设置信息。早在Windows 3.0推出OLE技术的时候&#…

Android 一帧绘制流程

Android 一帧绘制流程揭秘&#xff1a;主线程与 RenderThread 的双人舞 核心目标&#xff1a;60帧/秒的丝滑体验&#xff0c;意味着每帧必须在16.67ms内完成所有工作&#xff01; 想象一下屏幕刷新就像放映电影&#xff0c;一帧接一帧。Android系统为了播放这“电影”&#xff…

智能网盘检测软件,一键识别失效链接

软件介绍 今天为大家推荐一款由吾爱论坛大神开发的网盘链接检测工具&#xff0c;专为网络资源爱好者设计&#xff0c;可快速批量检测分享链接的有效性。 核心功能 这款工具能够智能识别各类网盘分享链接的有效状态&#xff0c;用户只需批量粘贴链接&#xff0c;软件便会自…

408第三季part2 - 计算机网络 - 应用层

理解 客户机不能直接通信&#xff0c;要通过服务器才行 P2P可以 先记个名字 看图记查询流程 然后迭代就是 主机到本地 本地先查根&#xff0c;然后返回&#xff0c;再查顶级&#xff0c;然后返回&#xff0c;再查权限 然后注意这里主机到本地都是递归查询&#xff0c;其他的…

Modern C++(七)类

7、类 7.1、类声明 前置声明&#xff1a;声明一个将稍后在此作用域定义的类类型。直到定义出现前&#xff0c;此类名具有不完整类型。当代码仅仅需要用到类的指针或引用时&#xff0c;就可以采用前置声明&#xff0c;无需包含完整的类定义。 前置声明有以下几个作用&#xf…

4-6WPS JS宏自定义函数变长参数函数(实例:自定义多功能数据统计函数)学习笔记

一、自定义函数:自定义多功能数据统计函数。示例1&#xff1a;function jia1(x,...arr){//自定义变长函数&#xff0c;X第一参数&#xff0c;...arr为变长参数可放入无数个参数&#xff0c;就像是数组return xWorksheetFunction.Sum(arr)//返回&#xff0c;X第一参数WorksheetF…

HDMI延长器 vs 分配器 vs KVM切换器 vs 矩阵:技术区别与应用场景

在音视频和计算机信号传输领域&#xff0c;延长器、分配器、切换器和矩阵是四种常见设备&#xff0c;它们的功能和应用场景有显著区别。以下是它们的核心差异对比&#xff1a; 1. 延长器&#xff08;Extender&#xff09; 功能&#xff1a; ▸ 将信号&#xff08;如HDMI、Displ…

从0到1解锁Element-Plus组件二次封装El-Dialog动态调用

技术难题初登场 家人们&#xff0c;最近在开发一个超复杂的后台管理系统项目&#xff0c;里面有各种数据展示、表单提交、权限控制等功能&#xff0c;在这个过程中&#xff0c;我频繁地使用到了element-plus组件库中的el-dialog组件 。它就像一个小弹窗&#xff0c;可以用来显示…

数据结构实验习题

codeblock F2是出控制台 1.1 /* by 1705 WYY */ #include <stdio.h> #include <stdlib.h> #define TRUE 1 #define FALSE 0 #define YES 1 #define NO 0 #define OK 1 #define ERROR 0 #define SUCCESS 1 #define UNSUCCESS 0 #define OVERFLOW -2 #define UNDERF…

PyTorch 2.7深度技术解析:新一代深度学习框架的革命性演进

引言:站在AI基础设施变革的历史节点 在2025年这个充满变革的年份,PyTorch团队于4月23日正式发布了2.7.0版本,随后在6月4日推出了2.7.1补丁版本,标志着这个深度学习领域最具影响力的框架再次迎来了重大突破。这不仅仅是一次常规的版本更新,而是一次面向未来计算架构和AI应…

LTspice仿真10——电容

电路1中电容下标m5&#xff0c;表示5个该电阻并联电路2中ic1.5v&#xff0c;表示电容初始自带电量&#xff0c;电压为1.5v

C#事件驱动编程:标准事件模式完全指南

事件驱动是GUI编程的核心逻辑。当程序被按钮点击、按键或定时器中断时&#xff0c;如何规范处理事件&#xff1f;.NET框架通过EventHandler委托给出了标准答案。 &#x1f50d; 一、EventHandler委托&#xff1a;事件处理的基石 public delegate void EventHandler(object se…

全面的 Spring Boot 整合 RabbitMQ 的 `application.yml` 配置示例

spring:rabbitmq:# 基础连接配置 host: localhost # RabbitMQ 服务器地址port: 5672 # 默认端口username: guest # 默认用户名password: guest # 默认密码virtual-host: / # 虚拟主机&#xff08;默认/&…

Win32 API实现串口辅助类

近期需要使用C++进行串口通讯,将Win32 API串口接口进行了下封装,可实现同步通讯,异步回调通讯 1、SerialportMy.h #pragma once #include <Windows.h> #include <thread> #include <atomic> #include <functional> #include <queue> #inclu…

Python-执行系统命令-subprocess

1 需求 2 接口 3 示例 4 参考资料 Python subprocess 模块 | 菜鸟教程

Web攻防-XMLXXE上传解析文件预览接口服务白盒审计应用功能SRC报告

知识点&#xff1a; 1、WEB攻防-XML&XXE-黑盒功能点挖掘 2、WEB攻防-XML&XXE-白盒函数点挖掘 3、WEB攻防-XML&XXE-SRC报告 一、演示案例-WEB攻防-XML&XXE-黑盒功能点挖掘 1、不安全的图像读取-SVG <?xml version"1.0" standalone"yes&qu…