《快学 Go 语言》第 12 课 —— 神秘的地下通道

基于消隐点的相机自标定技术实战详解 传统的棋盘格标定就像考试作弊——你知道答案格式,只需照着填。而基于消隐点的自标定,更像是在真实世界中“破译密码”,依靠的是对几何本质的理解。它的优势显而易见:✅ 无需额外设备✅ 适用于动态、不可控环境✅ 可嵌入 AR/SLAM 系统前端✅ 支持在线标定与校正当然也有局限:❌ 弱结构场景(森林、沙漠)效果差❌ 对检测精度高度敏感❌ 单视图易陷入病态求解但未来已在路上:🧠 结合深度学习预测虚拟平行线(如 HorizonNet) 阅读详情

640?wx_fmt=png

不同的并行协程之间交流的方式有两种,一种是通过共享变量,另一种是通过队列。Go 语言鼓励使用队列的形式来交流,它单独为协程之间的队列数据交流定制了特殊的语法 —— 通道。

通道是协程的输入和输出。作为协程的输出,通道是一个容器,它可以容纳数据。作为协程的输入,通道是一个生产者,它可以向协程提供数据。通道作为容器是有限定大小的,满了就写不进去,空了就读不出来。通道还有它自己的类型,它可以限定进入通道的数据的类型。

640?wx_fmt=png
图片

创建通道

创建通道只有一种语法,那就是 make 全局函数,提供第一个类型参数限定通道可以容纳的数据类型,再提供第二个整数参数作为通道的容器大小。大小参数是可选的,如果不填,那这个通道的容量为零,叫着「非缓冲型通道」,非缓冲型通道必须确保有协程正在尝试读取当前通道,否则写操作就会阻塞直到有其它协程来从通道中读东西。非缓冲型通道总是处于既满又空的状态。与之对应的有限定大小的通道就是缓冲型通道。在 Go 语言里不存在无界通道,每个通道都是有限定最大容量的。

// 缓冲型通道,里面只能放整数
var bufferedChannel = make(chan int, 1024)
// 非缓冲型通道
var unbufferedChannel = make(chan int)

读写通道

Go 语言为通道的读写设计了特殊的箭头语法糖 <-,让我们使用通道时非常方便。把箭头写在通道变量的右边就是写通道,把箭头写在通道的左边就是读通道。一次只能读写一个元素。

package main

import "fmt"

func main() {
    var ch chan int = make(chan int, 4)
    for i:=0; i<cap(ch); i++ {
        ch <- i   // 写通道
    }
    for len(ch) > 0 {
        var value int = <- ch  // 读通道
        fmt.Println(value)
    }
}

通道作为容器,它可以像切片一样,使用 cap() 和 len() 全局函数获得通道的容量和当前内部的元素个数。通道一般作为不同的协程交流的媒介,在同一个协程里它也是可以使用的。

读写阻塞

通道满了,写操作就会阻塞,协程就会进入休眠,直到有其它协程读通道挪出了空间,协程才会被唤醒。如果有多个协程的写操作都阻塞了,一个读操作只会唤醒一个协程。

通道空了,读操作就会阻塞,协程也会进入睡眠,直到有其它协程写通道装进了数据才会被唤醒。如果有多个协程的读操作阻塞了,一个写操作也只会唤醒一个协程。

package main

import "fmt"
import "time"
import "math/rand"

func send(ch chan int) {
    for {
        var value = rand.Intn(100)
        ch <- value
        fmt.Printf("send %d\n", value)
    }
}

func recv(ch chan int) {
    for {
        value := <- ch
        fmt.Printf("recv %d\n", value)
        time.Sleep(time.Second)
    }
}

func main() {
    var ch = make(chan int, 1)
    // 子协程循环读
    go recv(ch)
    // 主协程循环写
    send(ch)
}

--------
send 81
send 87
recv 81
recv 87
send 47
recv 47
send 59

关闭通道

Go 语言的通道有点像文件,不但支持读写操作, 还支持关闭。读取一个已经关闭的通道会立即返回通道类型的「零值」,而写一个已经关闭的通道会抛异常。如果通道里的元素是整型的,读操作是不能通过返回值来确定通道是否关闭的。

package main

import "fmt"

func main() {
    var ch = make(chan int, 4)
    ch <- 1
    ch <- 2
    close(ch)

    value := <- ch
    fmt.Println(value)
    value = <- ch
    fmt.Println(value)
    value = <- ch
    fmt.Println(value)
}

-------
1
2
0

这时候就需要引入一个新的知识点 —— 使用 for range 语法糖来遍历通道

for range 语法我们已经见了很多次了,它是多功能的,除了可以遍历数组、切片、字典,还可以遍历通道,取代箭头操作符。当通道空了,循环会暂停阻塞,当通道关闭时,阻塞停止,循环也跟着结束了。当循环结束时,我们就知道通道已经关闭了。

package main

import "fmt"

func main() {
    var ch = make(chan int, 4)
    ch <- 1
    ch <- 2
    close(ch)

 // for range 遍历通道
    for value := range ch {
        fmt.Println(value)
    }
}

------
1
2

通道如果没有显式关闭,当它不再被程序使用的时候,会自动关闭被垃圾回收掉。不过优雅的程序应该将通道看成资源,显式关闭每个不再使用的资源是一种良好的习惯。

通道写安全

上面提到向一个已经关闭的通道执行写操作会抛出异常,这意味着我们在写通道时一定要确保通道没有被关闭。

package main

import "fmt"

func send(ch chan int) {
    i := 0
    for {
        i++
        ch <- i
    }
}

func recv(ch chan int) {
    value := <- ch
    fmt.Println(value)
    value = <- ch
    fmt.Println(value)
    close(ch)
}

func main() {
    var ch = make(chan int, 4)
    go recv(ch)
    send(ch)
}

---------
1
2
panic: send on closed channel

goroutine 1 [running]:
main.send(0xc42008a000)
    /Users/qianwp/go/src/github.com/pyloque/practice/main.go:9 +0x44
main.main()
    /Users/qianwp/go/src/github.com/pyloque/practice/main.go:24 +0x66
exit status 2

那如何确保呢?Go 语言并不存在一个内置函数可以判断出通道是否已经被关闭。即使存在这样一个函数,当你判断时通道没有关闭,并不意味着当你往通道里写数据时它就一定没有被关闭,并发环境下,它是可能被其它协程随时关闭的。

确保通道写安全的最好方式是由负责写通道的协程自己来关闭通道,读通道的协程不要去关闭通道。

package main

import "fmt"

func send(ch chan int) {
 ch <- 1
 ch <- 2
 ch <- 3
 ch <- 4
 close(ch)
}

func recv(ch chan int) {
 for v := range ch {
  fmt.Println(v)
 }
}

func main() {
 var ch = make(chan int, 1)
 go send(ch)
 recv(ch)
}

-----------
1
2
3
4

这个方法确实可以解决单写多读的场景,可要是遇上了多写单读的场合该怎么办呢?任意一个读写通道的协程都不可以随意关闭通道,否则会导致其它写通道协程抛出异常。这时候就必须让其它不相干的协程来干这件事,这个协程需要等待所有的写通道协程都结束运行后才能关闭通道。那其它协程要如何才能知道所有的写通道已经结束运行了呢?这个就需要使用到内置 sync 包提供的 WaitGroup 对象,它使用计数来等待指定事件完成。

package main

import "fmt"
import "time"
import "sync"

func send(ch chan int, wg *sync.WaitGroup) {
    defer wg.Done() // 计数值减一
    i := 0
    for i < 4 {
        i++
        ch <- i
    }
}

func recv(ch chan int) {
    for v := range ch {
        fmt.Println(v)
    }
}

func main() {
    var ch = make(chan int, 4)
    var wg = new(sync.WaitGroup)
    wg.Add(2) // 增加计数值
    go send(ch, wg)  // 写
    go send(ch, wg)  // 写
    go recv(ch)
    // Wait() 阻塞等待所有的写通道协程结束
 // 待计数值变成零,Wait() 才会返回
 wg.Wait()
    // 关闭通道
 close(ch)
 time.Sleep(time.Second)
}

---------
1
2
3
4
1
2
3
4

多路通道

在真实的世界中,还有一种消息传递场景,那就是消费者有多个消费来源,只要有一个来源生产了数据,消费者就可以读这个数据进行消费。这时候可以将多个来源通道的数据汇聚到目标通道,然后统一在目标通道进行消费。

package main

import "fmt"
import "time"

// 每隔一会生产一个数
func send(ch chan int, gap time.Duration) {
    i := 0
    for {
        i++
        ch <- i
        time.Sleep(gap)
    }
}

// 将多个原通道内容拷贝到单一的目标通道
func collect(source chan int, target chan int) {
    for v := range source {
        target <- v
    }
}

// 从目标通道消费数据
func recv(ch chan int) {
    for v := range ch {
        fmt.Printf("receive %d\n", v)
    }
}


func main() {
    var ch1 = make(chan int)
    var ch2 = make(chan int)
    var ch3 = make(chan int)
    go send(ch1, time.Second)
    go send(ch2, 2 * time.Second)
    go collect(ch1, ch3)
    go collect(ch2, ch3)
    recv(ch3)
}

---------
receive 1
receive 1
receive 2
receive 2
receive 3
receive 4
receive 3
receive 5
receive 6
receive 4
receive 7
receive 8
receive 5
receive 9
....

但是上面这种形式比较繁琐,需要为每一种消费来源都单独启动一个汇聚协程。Go 语言为这种使用场景带来了「多路复用」语法糖,也就是下面要讲的 select 语句,它可以同时管理多个通道读写,如果所有通道都不能读写,它就整体阻塞,只要有一个通道可以读写,它就会继续。下面我们使用 select 语句来简化上面的逻辑

package main

import "fmt"
import "time"

func send(ch chan int, gap time.Duration) {
    i := 0
    for {
        i++
        ch <- i
        time.Sleep(gap)
    }
}

func recv(ch1 chan int, ch2 chan int) {
    for {
        select {
            case v := <- ch1:
                fmt.Printf("recv %d from ch1\n", v)
            case v := <- ch2:
                fmt.Printf("recv %d from ch2\n", v)
        }
    }
}

func main() {
    var ch1 = make(chan int)
    var ch2 = make(chan int)
    go send(ch1, time.Second)
    go send(ch2, 2 * time.Second)
    recv(ch1, ch2)
}

------------
recv 1 from ch2
recv 1 from ch1
recv 2 from ch1
recv 3 from ch1
recv 2 from ch2
recv 4 from ch1
recv 3 from ch2
recv 5 from ch1

上面是多路复用 select 语句的读通道形式,下面是它的写通道形式,只要有一个通道能写进去,它就会打破阻塞。

select {
  case ch1 <- v:
      fmt.Println("send to ch1")
  case ch2 <- v:
      fmt.Println("send to ch2")
}

非阻塞读写

前面我们讲的读写都是阻塞读写,Go 语言还提供了通道的非阻塞读写。当通道空时,读操作不会阻塞,当通道满时,写操作也不会阻塞。非阻塞读写需要依靠 select 语句的 default 分支。当 select 语句所有通道都不可读写时,如果定义了 default 分支,那就会执行 default 分支逻辑,这样就起到了不阻塞的效果。下面我们演示一个单生产者多消费者的场景。生产者同时向两个通道写数据,写不进去就丢弃。

package main

import "fmt"
import "time"

func send(ch1 chan int, ch2 chan int) {
    i := 0
    for {
        i++
        select {
            case ch1 <- i:
                fmt.Printf("send ch1 %d\n", i)
            case ch2 <- i:
                fmt.Printf("send ch2 %d\n", i)
            default:
        }
    }
}

func recv(ch chan int, gap time.Duration, name string) {
    for v := range ch {
        fmt.Printf("receive %s %d\n", name, v)
        time.Sleep(gap)
    }
}

func main() {
 // 无缓冲通道
    var ch1 = make(chan int)
    var ch2 = make(chan int)
    // 两个消费者的休眠时间不一样,名称不一样
 go recv(ch1, time.Second, "ch1")
    go recv(ch2, 2 * time.Second, "ch2")
    send(ch1, ch2)
}

------------
send ch1 27
send ch2 28
receive ch1 27
receive ch2 28
send ch1 6708984
receive ch1 6708984
send ch2 13347544
send ch1 13347775
receive ch2 13347544
receive ch1 13347775
send ch1 20101642
receive ch1 20101642
send ch2 26775795
receive ch2 26775795
...

从输出中可以明显看出有很多的数据都丢弃了,消费者读到的数据是不连续的。如果将 select 语句里面的 default 分支干掉,再运行一次,结果如下

send ch2 1
send ch1 2
receive ch1 2
receive ch2 1
receive ch1 3
send ch1 3
receive ch2 4
send ch2 4
send ch1 5
receive ch1 5
receive ch1 6
send ch1 6
receive ch1 7

可以看到消费者读到的数据都连续了,但是每个数据只给了一个消费者。select 语句的 default 分支非常关键,它是决定通道读写操作阻塞与否的关键。

Java 也有通道

通道在其它语言里面的表现形式是队列,在 Java 语言里,带缓冲通道就是并发包内置的 java.util.concurrent.ArrayBlockingQueue,无缓冲通道也是并发包内置的 java.util.concurrent.SynchronousQueue。ArrayBlockingQueue 的内部实现形式是一个数组,多线程读写时需要使用锁来控制并发访问。不过像 Go 语言提供的多路复用效果,Java 语言就没有内置的实现了。

通道内部结构

Go 语言的通道内部结构是一个循环数组,通过读写偏移量来控制元素发送和接受。它为了保证线程安全,内部会有一个全局锁来控制并发。对于发送和接受操作都会有一个队列来容纳处于阻塞状态的协程。

640?wx_fmt=png
图片
type hchan struct {
  qcount uint  // 通道有效元素个数
  dataqsize uint   // 通道容量,循环数组总长度
  buf unsafe.Pointer // 数组地址
  elemsize uint16 // 内部元素的大小
  closed uint32 // 是否已关闭 0或者1
  elemtype *_type // 内部元素类型信息
  sendx uint // 循环数组的写偏移量
  recvx uint // 循环数组的读偏移量
  recvq waitq // 阻塞在读操作上的协程队列
  sendq waitq // 阻塞在写操作上的协程队列

  lock mutex // 全局锁
}

这个循环队列和 Java 语言内置的 ArrayBlockingQueue 结构如出一辙。从这个数据结构中我们也可以得出结论:队列在本质上是使用共享变量加锁的方式来实现的,共享变量才是并行交流的本质。

class ArrayBlockingQueue extends AbstractQueue {
  Object[] items;
  int takeIndex;
  int putIndex;
  int count;
  ReentrantLock lock;
  ...
}

所以读者请不要认为 Go 语言的通道很神奇,Go 语言只是对通道设计了一套便于使用的语法糖,让这套数据结构显的平易近人。它在内部实现上和其它语言的并发队列大同小异。

640?wx_fmt=png

阅读《快学 Go 语言》更多章节,长按图片识别二维码关注公众号「码洞」

流媒体影视爆款预测数据集6000部剧集电影21特征 本数据集模拟流媒体平台影视内容特征,包含6000部剧集、电影与纪录片样本、21个字段,覆盖内容类型、题材、出品国家、上映年份与月份、内容分级、是否平台原创、是否系列续集、改编来源、时长、季数、明星影响力、制作与营销预算、观众评分、社交热度等维度。提供top10_hit分类目标(进入平台榜单前十并保持4周以上)与views_28d_m回归目标(上线28天观看量),结果字段独立分离,便于练习避免目标泄漏。数据干净完整、无缺失值、无需表关联,直接可用于分类、回归、探索性分析,以及发布档期选择、题材与续集表现、单位营销成本观看效率等研究,适合影视行业分析、推荐系统与机器学习教学。说明:本数据为Python生成的合成数据,仅反映生成规则,不含真实节目与个人信息。许可:CC0公有领域,可自由使用与二次分发。 立即下载

相关推荐

AI大模型安全架构落地解决方案.pptx

AI大模型安全架构落地解决方案.pptx

《快学 Go 语言》第 12 课 —— 通道

不同的并行协程之间交流的方式有两种,一种是通过共享变量,另一种是通过队列。Go 语言鼓励使用队列的形式来交流,它单独为协程之间的队列数据交流定制了特殊的语法 —— 通道。 通道是协程的输入和输出。作为协程的输出,通道是一个容器,它可以容纳数据。作为协程的输入,通道是一个生产者,它可以向协程提供数据。通道作为容器是有限定大小的,满了就写不进去,空了就读不出来。通道还有它自己的类型,它可以限定进入通...

weixin_34303897的博客 216

国央企创新负责人如何提升企业创新与技术合作的精准度?.docx

国央企创新负责人如何提升企业创新与技术合作的精准度?

16-Go语言并发之goroutine,channel

首先我们先了解几个与并发编程相关的概念。进程:程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立的单位。线程:操作系统调度执行的最小单位。协程:非操作系统提供而是由用户自行创建和控制的用户态线程,比线程更轻量级。Go语言中的并发程序主要是通过基于CSP的goroutine和channel来实现,当然也支持使用传统的多线程共享内存的并发方式。Goroutine是GO语言支持并发的核心,在一个GO程序中同时创建成百上千个goroutine是非常普遍的,一个goroutine会以一个很小的栈开始其生

weixin_38753143的博客 977

科技中介服务机构在协助企业对接科研成果时,如何提升服务响应速度与专业化水平.docx

科技中介服务机构在协助企业对接科研成果时,如何提升服务响应速度与专业化水平

啊算法撒上v的咋撒方式

啊算法撒上v的咋撒方式

go语言基础-----17-----channel创建、读写、安全关闭、多路复用select

1 通道channel介绍 1)channel,可译为通道,是go语言协程goroutine之间的通信方式。 2)channel通信可以想象成从管道的一头塞进数据,从另一头读取数据。通道作为容器是有限定大小的,满了就写不进去,空了就读不出来。 3)channel是拥有数据类型的,channel只能传递指定的数据类型的值。 4)多协程操作时(即多个写多个读),它是协程安全的,不需要额外加锁。 2 创建通道 创建通道只有一种语法,使用make 函数。 创建的通道有两种通道类型: 1)缓冲型通道。 //语

weixin_44517656的博客 4211

高校技术转移办公室人员如何利用智能工具简化技术成果的推介与转化流程?.docx

科易网基于AI模型+行业大数据,在技术转移、成果转化、技术经纪、知识产权、产业创新、科技招商等垂直领域的多样化应用场景,推出科技创新领域的AI+工具与数智化解决方案,推动科技创新与产业创新智能化发展。

Go 通道(channel)

不带缓冲的通道 无缓冲`channel`的发送操作将导致发送者`goroutine`阻塞,直到另一个`goroutine`在相同的`channel`上执行了接收操作 如果接收操作先发生,那么接收者`goroutine`也将阻塞,直到有另一个`goroutine`在相同的`channel`上执行发送操作 当发送的值通过`channel`成功传输后,两个`goroutine`可以继续执行后面的语句 ch := make(chan int) ch := make(chan int, 0) ...

weixin_40123451的博客 472

solong 图像处理器 - v3.4 - 吾爱大佬 by 金牌软件 .exe

solong 图像处理器 _ v3.4 _ 吾爱大佬 by 金牌软件 .exe

西门子 S7 PLC 对接 MES 系统:HTTP/JSON 通信实施指南

内容概要:本文介绍了通过IGT-DSER智能网关实现西门子S7-1500 PLC(采用优化访问DB数据块)与MES系统的对接方案。重点阐述了使用S7-PLUS标签方式读取PLC变量标签数据,结合网关边缘处理能力进行数据换算与JSON封装,并通过HTTP/GET或POST方式将数据上传至MES系统的技术路径。文中详细说明了网络配置、变量映射、触发逻辑设置、JSON结构生成、联调验收流程及常见故障排查方法,强调无需修改PLC程序即可完成集成,适用于工业现场快速部署。; 适合人群:自动化工程师、MES实施工程师、工业网络与设备运维人员; 使用场景及目标:①实现西门子S7-1500等支持优化DB的PLC与MES系统高效对接;②在不改动原有PLC程序的前提下完成数据采集与上传;③通过标准化JSON格式与HTTP协议实现与各类业务平台的数据交互;④提升系统集成效率并确保数据传输的准确性与可监控性; 阅读建议:此资源以实际工程案例为基础,建议读者结合IGT-DSER网关参数管理软件操作实践,重点关注标签配置、触发条件设定与JSON字段映射的细节,并在测试环境中验证请求与响应闭环后再投入正式运行。

golang中的collection

append Append挂载一个元素到当前Collection,如果挂载的元素类型不一致,则会在Collection中产生Error sSlice := []int{1, 2} //没变 intColl := NewIntCollection(sSlice) intColl.Append(3) if intColl.Err() == nil { intColl.DD() } IsEmpty IsEmpty() bool 判断一个Collection是否为空,为空返回true, 否则

1582

PSIM-串联谐振半桥LLC开关电源+PFM变频闭环控制【仿真+计算书+参考文献】

内容概要:本文详细介绍了PSIM-串联谐振半桥LLC开关电源结合PFM变频闭环控制的设计与仿真,系统阐述了LLC谐振变换器的建模方法、软开关实现机制及基于变频控制的闭环调节策略。资源包涵盖完整的PSIM仿真模型、详细的计算书与参考文献,重点展示了在宽负载范围内通过调节开关频率实现高效能运行的技术路径。系统利用串联谐振腔的频率特性,在不同负载条件下实现零电压开通(ZVS)或零电流关断(ZCS),显著降低开关损耗;同时,闭环控制通过反馈输出电压动态调整工作频率,确保输出稳定性和动态响应性能。该设计特别适用于对高效率、高功率密度有严格要求的应用场景。; 适合人群:具备电力电子、电源设计或自动化相关背景的本科高年级学生、研究生及从事开关电源研发的工程师。; 使用场景及目标:①掌握LLC谐振变换器的工作原理及其在PSIM中的建模与仿真方法;②学习PFM(脉冲频率调制)变频控制在开关电源中的闭环实现技术;③应用于高频高效电源系统的设计与优化,如通信电源、新能源变换器、车载充电机等;④为科研项目或毕业设计提供完整的仿真参考、计算依据与技术支持。; 阅读建议:建议读者结合提供的PSIM仿真文件、计算书与参考文献同步操作,深入理解谐振参数设计、变频控制逻辑与闭环反馈的实现细节,重点关注开关频率与增益曲线的关系、谐振元件参数匹配对效率的影响,并通过改变输入电压和负载条件验证系统的稳态与动态性能表现。

golang之channel学习总结

概述 本篇目的是对go中的channel做一个总结。 主要参考https://www.jianshu.com/p/76acce09da09 环境 $ uname -a Linux gl.com 5.4.50-amd64-desktop #74 SMP Mon Aug 24 20:15:37 CST 2020 x86_64 GNU/Linux $ go version go version go1.15.2 linux/amd64 channel的用途 主要用于goroutine之间通信 channel的

开心就好的专栏 1215

国央企创新负责人在推动技术转化过程中,如何更高效地识别高质量专利并实现价值转化?.docx

科易网基于AI模型+行业大数据,在技术转移、成果转化、技术经纪、知识产权、产业创新、科技招商等垂直领域的多样化应用场景,推出科技创新领域的AI+工具与数智化解决方案,推动科技创新与产业创新智能化发展。

从零开始VCS+Verdi 安装过程

从零开始VCS+Verdi 安装及破解过程 主要参考:VCS+Verdi 安装及破解过程(CentOS7)-----FPGA开发 1. 安装文件结构 –vcs-mx_vL-2016.06 –verdi_vL-2016.06-1 –scl_v11.9 #此程序统一管理license等破解文件 –SynopsysInstaller.rar #此程序用于安装以上3个程序 –sc...

Ztrans的博客 2万+
上一篇: 码洞原创深度技术文章大全 —— 高端面试必备
下一篇: 推荐几个牛叉技术大神的高质量免费技术专栏,码洞垫底
codehole_
博客等级 码龄7年 34粉丝 132原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值