go语法大赏

前些日子单机房稳定性下降,找了好一会才找到真正的原因。这里面涉及到不少go语法细节,正好大家一起看一下。

一、仿真代码

这是仿真之后的代码

package mainimport ("fmt""go.uber.org/atomic""time"
)type StopSignal struct{}// RecvChannel is the wrapped channel for recv side.
type RecvChannel[T any] struct {// Data will be passed through the result channel.DataChannel <-chan T// Error will be passed through the error channel.ErrorChannel <-chan error// Stop signal will be passed through the stop signal channel,// when signal is sent or channel is closed, it means recv side requires send side to stop sending data.StopChannel chan<- StopSignalstopped     *atomic.Bool
}// Close sends stop signal to the sender side.
func (c *RecvChannel[T]) Close() {if !c.stopped.CompareAndSwap(false, true) {return}close(c.StopChannel)
}// Stopped returns whether the stop signal has been sent.
func (c *RecvChannel[T]) Stopped() bool {return c.stopped.Load()
}// GetError returns the last error, it waits at most 1s if the error channel is not closed.
func (c *RecvChannel[T]) GetError() error {select {case err := <-c.ErrorChannel:return errcase <-time.After(time.Second):return nil}
}// SendChannel is the wrapped channel for sender side.
type SendChannel[T any] struct {// Data will be passed through the result channel.DataChannel chan<- T// Error will be passed through the error channel.ErrorChannel chan<- error// Stop signal will be passed through the stop signal channel,// when signal is sent or channel is closed, it means recv side requires send side to stop sending data.StopChannel <-chan StopSignalstopped     *atomic.Bool
}// Close closes the result channel and error channel, so the recv will know the sending has been stopped.
func (c *SendChannel[T]) Close() {close(c.DataChannel)close(c.ErrorChannel)c.stopped = atomic.NewBool(true)
}// Stopped returns whether the stop signal has been sent.
func (c *SendChannel[T]) Stopped() bool {return c.stopped.Load()
}// Publish sends data to the data channel, does nothing if it is closed.
func (c *SendChannel[T]) Publish(t T) {if c.Stopped() {return}select {case <-c.StopChannel:case c.DataChannel <- t:}
}func (c *SendChannel[T]) PublishError(err error, close bool) {if c.Stopped() {return}select {case <-c.StopChannel:case c.ErrorChannel <- err:}if close {c.Close()}
}func NewChannel[T any](bufSize int) (*SendChannel[T], *RecvChannel[T]) {resultC := make(chan T, bufSize)errC := make(chan error, 1)stopC := make(chan StopSignal, 1)stopped := atomic.NewBool(false)sc := &SendChannel[T]{DataChannel:  resultC,ErrorChannel: errC,StopChannel:  stopC,stopped:      stopped,}rc := &RecvChannel[T]{DataChannel:  resultC,ErrorChannel: errC,StopChannel:  stopC,stopped:      stopped,}return sc, rc
}// SliceToChannel creates a channel and sends the slice's items into it.
// It ignores if the item in the slices is not a type T or error.
func SliceToChannel[T any](size int, s []any) *RecvChannel[T] {sc, rc := NewChannel[T](size)go func() {for _, item := range s {if sc.Stopped() {sc.Close()return}switch v := item.(type) {case T:sc.DataChannel <- vcase error:sc.ErrorChannel <- vdefault:continue}}sc.Close()}()return rc
}// /// 真正的处理逻辑
func Process(send *SendChannel[int]) {defer func() {if send != nil {fmt.Println("3 Process close defer")send.Close()}}()go func() {for {select {case <-send.StopChannel:fmt.Println("2 Process stop channel")send.Close()return}}}()send.ErrorChannel <- fmt.Errorf("0 Start error \n")fmt.Println("0 Start error")time.Sleep(1 * time.Second)
}func main() {send, recv := NewChannel[int](10)go func() {Process(send)}()for {fmt.Println("only once")select {case <-recv.ErrorChannel:fmt.Println("1 recv errorchannel ")recv.Close()break}break}//panic(1)time.Sleep(5 * time.Second)
}

执行结果如下:

➜  my go run main.go
only once
0 Start error
1 recv errorchannel
2 Process stop channel
3 Process close defer
panic: close of closed channelgoroutine 21 [running]:
main.(*SendChannel[...]).Close(...)/Users/bytedance/My/work/go/my/main.go:60
main.Process.func1()/Users/bytedance/My/work/go/my/main.go:147 +0x6c
main.Process(0x14000092020)/Users/bytedance/My/work/go/my/main.go:163 +0x118
main.main.func1()/Users/bytedance/My/work/go/my/main.go:168 +0x20
created by main.main in goroutine 1/Users/bytedance/My/work/go/my/main.go:167 +0x70
exit status 2

不知道大家是否能够比较快的看出来问题。

二、相关语法

2.1channel

知识点

在 Go 语言中,channel是用于在多个goroutine之间进行通信和同步的重要机制,以下是一些关于channel的重要知识点:

1. 基本概念
  • 定义channel可以被看作是一个类型安全的管道,用于在goroutine之间传递数据,遵循 CSP(Communicating Sequential Processes)模型,即 “通过通信来共享内存,而不是通过共享内存来通信”,从而避免了传统共享内存并发编程中的数据竞争等问题。
  • 声明与创建:使用make函数创建,语法为make(chan 数据类型, 缓冲大小)。缓冲大小是可选参数,省略时创建的是无缓冲channel;指定大于 0 的缓冲大小时创建的是有缓冲channel。例如:
unbufferedChan := make(chan int)      // 无缓冲channel
bufferedChan := make(chan int, 10)   // 有缓冲channel,缓冲大小为10
2. 操作方式
  • 发送数据:使用<-操作符将数据发送到channel中,语法为channel <- 数据。例如:
ch := make(chan int)
go func() {ch <- 42  // 发送数据42到ch中
}()
  • 接收数据:同样使用<-操作符从channel中接收数据,有两种形式。一种是将接收到的数据赋值给变量,如数据 := <-channel;另一种是只接收数据不赋值,如<-channel。例如:
ch := make(chan int)
go func() {ch <- 42
}()
value := <-ch  // 从ch中接收数据并赋值给value
  • 关闭channel:使用内置的close函数关闭channel,关闭后不能再向其发送数据,但可以继续接收已发送的数据。接收完所有数据后,再接收将得到该类型的零值。例如:
ch := make(chan int)
go func() {for i := 0; i < 5; i++ {ch <- i}close(ch)  // 关闭channel
}()
for {value, ok := <-chif!ok {break  // 当ok为false时,表示channel已关闭}fmt.Println(value)
}
3. 缓冲与非缓冲channel
  • 无缓冲channel:也叫同步channel,数据的发送和接收必须同时准备好,即发送操作和接收操作会互相阻塞,直到对方准备好。只有当有对应的接收者在等待时,发送者才能发送数据;反之,只有当有发送者发送数据时,接收者才能接收数据。这确保了数据的同步传递。
  • 有缓冲channel:内部有一个缓冲区,只要缓冲区未满,发送操作就不会阻塞;只要缓冲区不为空,接收操作就不会阻塞。当缓冲区满时,继续发送会阻塞;当缓冲区为空时,继续接收会阻塞。例如:
bufferedChan := make(chan int, 3)
bufferedChan <- 1
bufferedChan <- 2
bufferedChan <- 3
// 此时缓冲区已满,再发送会阻塞
// bufferedChan <- 4 
4. 单向channel
  • 单向channel只能用于发送或接收数据,分别为只写channelchan<- 数据类型)和只读channel<-chan 数据类型)。单向channel主要用于函数参数传递,限制channel的使用方向,增强代码的可读性和安全性。例如:
// 只写channel
func sendData(ch chan<- int) {ch <- 42
}// 只读channel
func receiveData(ch <-chan int) {data := <-chfmt.Println(data)
}
5. select语句与channel
  • select语句用于监听多个channel的操作,它可以同时等待多个channel的发送或接收操作。当有多个channel准备好时,select会随机选择一个执行。select语句还可以结合default分支实现非阻塞操作。例如:
ch1 := make(chan int)
ch2 := make(chan int)go func() {ch1 <- 1
}()select {
case data := <-ch1:fmt.Println("Received from ch1:", data)
case data := <-ch2:fmt.Println("Received from ch2:", data)
default:fmt.Println("No channel is ready")
}
6. channel的阻塞与死锁
  • 阻塞:发送和接收操作在channel未准备好时会阻塞当前goroutine。无缓冲channel在没有对应的接收者时发送会阻塞,没有发送者时接收会阻塞;有缓冲channel在缓冲区满时发送会阻塞,缓冲区空时接收会阻塞。
  • 死锁:如果在一个goroutine中,channel的发送和接收操作相互等待,且没有其他goroutine来打破这种等待,就会发生死锁。例如,一个goroutine向无缓冲channel发送数据,但没有其他goroutine接收;或者一个goroutine从无缓冲channel接收数据,但没有其他goroutine发送数据。运行时系统会检测到死锁并报错。
7. channel的底层实现
  • channel的底层实现基于一个名为hchan的结构体,它包含了当前队列中元素数量、环形队列大小(缓冲容量)、指向环形队列的指针、元素大小、关闭标志、元素类型信息、发送索引、接收索引、等待接收的协程队列、等待发送的协程队列以及一个互斥锁等字段。
  • 发送操作时,如果接收队列非空,直接将数据拷贝给第一个等待的接收者并唤醒该goroutine;如果缓冲区未满,将数据存入缓冲区;如果缓冲区已满或无缓冲channel,将当前goroutine加入发送队列并挂起。接收操作时,如果发送队列非空,直接从发送者获取数据并唤醒发送者;如果缓冲区不为空,从缓冲区取出数据;如果缓冲区为空且无缓冲channel,将当前goroutine加入接收队列并挂起。
8. channel误用导致的问题

在 Go 语言中,操作channel时可能导致panic或者死锁等:

  1. 多次关闭同一个channel

使用内置的close函数关闭channel后,如果再次调用close函数尝试关闭同一个channel,就会引发panic。这是因为channel的关闭状态是一种不可逆的操作,重复关闭没有实际意义,并且可能会导致难以调试的问题。例如:

ch := make(chan int)
close(ch)
close(ch) // 这里会导致panic
  1. 向已关闭的channel发送数据

当一个channel被关闭后,再向其发送数据会导致panic。因为关闭channel意味着不再有数据会被发送到该channel中,继续发送数据违反了这种约定。示例如下:

ch := make(chan int)
close(ch)
ch <- 1 // 向已关闭的channel发送数据,会导致panic
  1. 关闭未初始化(nil)的channel

如果尝试关闭一个值为nilchannel,会引发panicnilchannel没有实际的底层数据结构来支持关闭操作。例如:

var ch chan int
close(ch) // 这里会导致panic,因为ch是nil
  1. 死锁导致的panic

在操作channel时,如果多个goroutine之间的通信和同步设计不当,可能会导致死锁。死锁发生时,所有涉及的goroutine都在互相等待对方,从而导致程序无法继续执行,运行时系统会检测到这种情况。例如:

func main() {ch := make(chan int)ch <- 1 // 没有其他goroutine从ch中接收数据,这里会阻塞,导致死锁fmt.Println("This line will never be executed")
}
➜  my go run main.go
fatal error: all goroutines are asleep - deadlock!goroutine 1 [chan send]:
main.main()/Users/bytedance/My/work/go/my/main.go:172 +0x54
exit status 2
  1. 不恰当的select语句使用

select语句中,如果没有default分支,并且所有的case对应的channel操作都无法立即执行(阻塞),那么当前goroutine会被阻塞。如果在主goroutine中发生这种情况且没有其他goroutine可以运行,就会导致死锁。例如:

func main() {ch1 := make(chan int)ch2 := make(chan int)select {case <-ch1:// 没有数据发送到ch1,这里会阻塞case <-ch2:// 没有数据发送到ch2,这里会阻塞}
}

要避免这些panic情况,编写代码时需要仔细设计channel的使用逻辑,合理处理channel的关闭、数据的发送和接收,以及确保goroutine之间的同步和通信正确无误。

解析

在NewChannel函数中,send和recv channel被赋值的是同一个ErrorChannel,而send和recv都是单向channel,一个只写,一个只读。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

所以当Process里send.ErrorChannel <- fmt.Errorf(“0 Start error \n”)执行的时候,main中的case <-recv.ErrorChannel被立即触发,然后执行recv.Close()函数,该函数执行了close(c.StopChannel),又触发了Process中的case <-send.StopChannel,执行了send.Close()。对于Process退出的时候,有defer,再次执行send.Close(),导致channel被多次关闭。

2.2defer

知识点

以前写过Go defer的一些神奇规则,你了解吗?,这次主要关注

  1. defer(延迟函数)执行按后进先出顺序执行,即先出现的 defer最后执行。
  2. Process中的defer的执行顺序与Process中的goroutine里的defer(如果有的话)执行顺序无关。
解析

其实这两个Close位置都有可能panic,主要看谁被先执行到。我是为了演示让Process sleep了1s。

defer func() {if send != nil {fmt.Println("3 Process close defer")send.Close()}}()go func() {for {select {case <-send.StopChannel:fmt.Println("2 Process stop channel")send.Close()return}}}()

2.3recover

知识点

在 Go 语言中,recover只能用于捕获当前goroutine内的panic,它的作用范围仅限于当前goroutine。具体说明如下:

只能捕获当前goroutinepanic:当一个goroutine发生panic时,该goroutine会沿着调用栈向上展开,执行所有已注册的defer函数。如果在这些defer函数中调用recover,则可以捕获到该goroutine内的panic,并恢复正常执行流程。而对于其他goroutine中发生的panic,当前goroutine无法通过recover捕获。例如:

package mainimport ("fmt""time"
)func worker() {defer func() {if r := recover(); r != nil {fmt.Println("Recovered in worker:", r)}}()panic("Worker panicked")
}func main() {go worker()time.Sleep(1 * time.Second)fmt.Println("Main goroutine continues")
}

在上述代码中,worker函数中的defer语句里使用recover捕获了该goroutine内的panicmain函数中的goroutine并不会受到影响,继续执行并打印出 “Main goroutine continues”。

解析

当时之所以查的比较困难,主要是发现Process中go func里配置了recover,报了很多错,但感觉没有大问题。加上代码不熟悉,没有发现有概率触发Process的defer中的panic。而且公司的监控没有监控到自建goroutine的panic情况。

三、解决方案

在Process中添加recover

defer func() {if r := recover(); r != nil {fmt.Println("Recovered in worker:", r)}}()

其实比较建议在涉及channel相关的地方,都加个recover,尤其是不太熟悉的时候。

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

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

相关文章

Android 14 解决打开app出现不兼容弹窗的问题

应用安装到 Android 14 上&#xff0c;出现如下提示 This app isn’t compatible with the latest version of Android. Check for an update or contact the app’s developer. 通过源码找原因。 提示的字符 根据字符找到 ./frameworks/base/core/res/res/values/strings.xm…

Linux句柄数过多问题排查

以下是Linux句柄数过多问题的排查与解决方法整理&#xff1a; 一、检测句柄使用情况 1‌.查看系统限制‌ 单个进程限制&#xff1a;ulimit -n 系统级总限制&#xff1a;cat /proc/sys/fs/file-max 2‌.统计进程占用量‌ 查看指定进程&#xff1a;lsof -p <PID> | wc -…

matlab插值方法(简短)

在MATLAB中&#xff0c;可以使用interp1函数快速实现插值。以下代码展示了如何使用spline插值方法对给定数据进行插值&#xff1a; x1 [23,56]; y1 [23,56]; X 23:1:56*4; Y interp1(x1,y1,X,spline);% linear、 spline其中&#xff0c;x1和y1是已知数据点&#xff0c;X是…

时间筛掉了不够坚定的东西

2025年5月17日&#xff0c;16~25℃&#xff0c;还好 待办&#xff1a; 《高等数学1》重修考试 《高等数学2》备课 《物理[2]》备课 《高等数学2》取消考试资格学生名单 《物理[2]》取消考试资格名单 职称申报材料 2024年税务申报 5月24日、25日监考报名 遇见&#xff1a;敲了一…

hexo博客搭建使用

搭建 Hexo 演示主题为&#xff1a;Keep 使用 文章 创建新文章 ➜ zymore-blog-keep git:(main) ✗ hexo new "告别H5嵌入&#xff01;uniApp小程序文件下载与分享完整解决方案" INFO Validating config INFO Created: ~/Desktop/HelloWorld/zymore-blog-k…

React组件开发流程-03.1

此章先以一个完整的例子来全面了解下React组件开发的流程&#xff0c;主要是以代码为主&#xff0c;在不同的章节中会把重点标出来&#xff0c;要完成的例子如下&#xff0c;也可从官网中找到。 React组件开发流程 这只是一个通用流程&#xff0c;在熟悉后不需要完全遵从。 …

Cloudflare防火墙拦截谷歌爬虫|导致收录失败怎么解决?

许多站长发现网站突然从谷歌搜索结果中“消失”&#xff0c;背后很可能是Cloudflare防火墙误拦截了谷歌爬虫&#xff08;Googlebot&#xff09;&#xff0c;导致搜索引擎无法正常抓取页面。 由于Cloudflare默认的防护规则较为严格&#xff0c;尤其是针对高频访问的爬虫IP&…

Ubuntu系统安装VsCode

在Linux系统中&#xff0c;可以通过.deb文件手动安装Visual Studio Code&#xff08;VS Code&#xff09;。以下是详细的安装步骤&#xff1a; 下载.deb文件 访问Visual Studio Code的官方网站。 在下载页面中&#xff0c;找到适用于Linux的.deb文件。 根据你的系统架构&…

降本增效双突破:Profinet转Modbus TCP助力包布机产能与稳定性双提升

在现代工业自动化领域&#xff0c;ModbusTCP和Profinet是两种常见的通讯协议。它们在数据传输、设备控制等方面有着重要作用。然而&#xff0c;由于这两种协议的工作原理和应用环境存在差异&#xff0c;直接互联往往会出现兼容性问题。此时&#xff0c;就需要一种能够实现Profi…

Python对JSON数据操作

在Python中&#xff0c;对JSON数据进行增删改查及加载保存操作&#xff0c;主要通过内置的json模块实现。 一、基础操作 1. 加载JSON数据 • 从文件加载 使用json.load()读取JSON文件并转换为Python对象&#xff08;字典/列表&#xff09;&#xff1a; import json with open…

Linux详解基本指令(一)

✨✨ 欢迎大家来到小伞的大讲堂✨✨ &#x1f388;&#x1f388;养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; 所属专栏&#xff1a;LInux_st 小伞的主页&#xff1a;xiaosan_blog 制作不易&#xff01;点个赞吧&#xff01;&#xff01;谢谢喵&#xff01;&a…

Node-Red通过Profinet转ModbusTCP采集西门子PLC数据配置案例

一、内容简介 本篇内容主要介绍Node-Red通过node-red-contrib-modbus插件与ModbusTCP设备进行通讯&#xff0c;这里Profinet转ModbusTCP网关作为从站设备&#xff0c;Node-Red作为主站分别从0地址开始读取10个线圈状态和10个保持寄存器&#xff0c;分别用Modbus-Read、Modbus-…

React方向:react的基本语法-数据渲染

1、安装包(js库) yarn add babel-standalone react react-dom 示例图.png 2、通过依赖包导入js库文件 <script src"../node_modules/babel-standalone/babel.js"></script> <script src"../node_modules/react/umd/react.development.js"&g…

k8s部署grafana

部署成功截图&#xff1a; 要在 Kubernetes (K8s) 集群中拉取 Grafana 镜像并创建 Grafana 容器&#xff0c;您可以按照以下步骤使用命令行完成操作。下面是完整的命令步骤&#xff0c;包括如何创建 Deployment 和 Service&#xff0c;以及如何将 Grafana 容器暴露给外部。1. 创…

基于注意力机制与iRMB模块的YOLOv11改进模型—高效轻量目标检测新范式

随着深度学习技术的发展,目标检测在自动驾驶、智能监控、工业质检等场景中得到了广泛应用。针对当前主流目标检测模型在边缘设备部署中所面临的计算资源受限和推理效率瓶颈问题,YOLO系列作为单阶段目标检测框架的代表,凭借其高精度与高速度的平衡优势,在工业界具有极高的应…

uniapp运行到微信开发者工具报错“更改appid失败touristappidError:tourist appid”

原因分析 因为项目还没配置自己的 小程序 AppID&#xff0c;导致微信开发者工具拒绝运行。 解决办法&#xff1a;在 HBuilderX 中设置 AppID 打开你的项目 在左侧找到并点击 manifest.json 文件 切换到上方的 tab&#xff1a;「小程序配置」标签页 找到微信小程序区域&#…

使用Thrust库实现异步操作与回调函数

文章目录 使用Thrust库实现异步操作与回调函数基本异步操作插入回调函数更复杂的回调示例注意事项 使用Thrust库实现异步操作与回调函数 在Thrust库中&#xff0c;你可以通过CUDA流(stream)来实现异步操作&#xff0c;并在适当的位置插入回调函数。以下是如何实现的详细说明&a…

mysql-Java手写分布式事物提交流程

准备 innodb存储引擎开启支持分布式事务 set global innodb_support_axon分布式的流程 详细流程&#xff1a; XA START ‘a’; 作用&#xff1a;开始一个新的XA事务&#xff0c;并分配一个唯一的事务ID ‘a’。 说明&#xff1a;在这个命令之后&#xff0c;所有后续的SQL操…

算法练习:19.JZ29 顺时针打印矩阵

错误原因 总体思路有&#xff0c;但不够清晰&#xff0c;一直在边调试边完善。这方面就养成更好的构思习惯&#xff0c;以及涨涨经验吧。 分析&#xff1a; 思路&#xff1a;找规律 两个坑&#xff1a; 一次循环的后半段是倒着遍历的是矩阵不是方阵&#xff0c;要考虑行列…

计算机组成与体系结构:缓存设计概述(Cache Design Overview)

目录 Block Placement&#xff08;块放置&#xff09; Block Identification&#xff08;块识别&#xff09; Block Replacement&#xff08;块替换&#xff09; Write Strategy&#xff08;写策略&#xff09; 总结&#xff1a; 高速缓存设计包括四个基础核心概念&#xf…