go语法大赏

news2025/5/19 22:03:06

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

一、仿真代码

这是仿真之后的代码

package main

import (
	"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<- StopSignal
	stopped     *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 err
	case <-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 StopSignal
	stopped     *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 <- v
			case error:
				sc.ErrorChannel <- v
			default:
				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 channel

goroutine 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 := <-ch
    if!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 := <-ch
    fmt.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 main

import (
    "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.coloradmin.cn/o/2379553.html

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

相关文章

软件工程各种图总结

目录 1.数据流图 2.N-S盒图 3.程序流程图 4.UML图 UML用例图 UML状态图 UML时序图 5.E-R图 首先要先了解整个软件生命周期&#xff1a; 通常包含以下五个阶段&#xff1a;需求分析-》设计-》编码 -》测试-》运行和维护。 软件工程中应用到的图全部有&#xff1a;系统…

AAAI2024 | 基于特征多样性对抗扰动攻击 Transformer 模型

Attacking Transformers with Feature Diversity Adversarial Perturbation 摘要-Abstract引言-Introduction相关工作-Related Work方法-Methodology实验-Experiments结论-Conclusion 论文链接 本文 “Attacking Transformers with Feature Diversity Adversarial Perturbatio…

关于数据湖和数据仓的一些概念

一、前言 随着各行业数字化发展的深化,数据资产和数据价值已越来越被深入企业重要发展的战略重心,海量数据已成为多数企业生产实际面临的重要问题,无论存储容量还是成本,可靠性都成为考验企业数据治理的考验。本文来看下海量数据存储的数据湖和数据仓,数据仓库和数据湖,…

常用的Java工具库

1. Collections 首先是 java.util 包下的 Collections 类。这个类主要用于操作集合&#xff0c;我个人非常喜欢使用它。以下是一些常用功能&#xff1a; 1.1 排序 在工作中&#xff0c;经常需要对集合进行排序。让我们看看如何使用 Collections 工具实现升序和降序排列&…

R S的EMI接收机面板

图片摘自R & S官网。 根据您提供的第一张图&#xff08;设备前面板带屏幕的图像&#xff09;&#xff0c;这是 Rohde & Schwarz ESRP7 EMI Test Receiver 的正面显示界面&#xff0c;我将对屏幕上显示的参数逐项进行解读&#xff1a; &#x1f5a5;️ 屏幕参数解读 左…

[ctfshow web入门] web122

信息收集 这一题把HOME开放了&#xff0c;把#和PWD给过滤了 <?php error_reporting(0); highlight_file(__FILE__); if(isset($_POST[code])){$code$_POST[code];if(!preg_match(/\x09|\x0a|[a-z]|[0-9]|FLAG|PATH|BASH|PWD|HISTIGNORE|HISTFILESIZE|HISTFILE|HISTCMD|US…

Java虚拟机 - JVM与Java体系结构

Java虚拟机 JVM与Java体系结构为什么要学习JVMJava与JVM简介Java 语言的核心特性JVM&#xff1a;Java 生态的基石JVM的架构模型基于栈的指令集架构&#xff08;Stack-Based&#xff09;基于寄存器的指令集架构&#xff08;Register-Based&#xff09;JVM生命周期 总结 JVM与Jav…

灌区量测水自动化监测解决方案

一、方案背景 随着社会发展和人口增长&#xff0c;水资源需求不断增大。我国水资源总量虽然丰富&#xff0c;但时空分布不均&#xff0c;加之农业用水占比大且效率偏低&#xff0c;使得水资源短缺问题日益凸显。农业用水一直是我国的耗水大户&#xff0c;占全部耗水总量的60%以…

界面控件DevExpress WinForms v24.2 - 数据处理功能增强

DevExpress WinForms拥有180组件和UI库&#xff0c;能为Windows Forms平台创建具有影响力的业务解决方案。DevExpress WinForms能完美构建流畅、美观且易于使用的应用程序&#xff0c;无论是Office风格的界面&#xff0c;还是分析处理大批量的业务数据&#xff0c;它都能轻松胜…

Linux的MySQL头文件和找不到头文件问题解决

头文件 #include <iostream> #include <mysql_driver.h> #include <mysql_connection.h> #include <cppconn/statement.h> #include <cppconn/resultset.h> #include <cppconn/prepared_statement.h> #include <cppconn/exception.h&g…

wps excel将表格输出pdf时所有列在一张纸上

记录&#xff1a;wps excel将表格输出pdf时所有列在一张纸上 1&#xff0c;调整缩放比例&#xff0c;或选择将所有列打印在一页 2&#xff0c;将表格的所有铺满到这套虚线

zabbix7.2最新版本 nginx自定义监控(三) 设置触发器

安装zabbix-get服务 在zabbix-server端口安装zabbix-get服务 [rootlocalhost ~]# dnf install -y zabbix-get Last metadata expiration check: 1:55:49 ago on Wed 14 May 2025 09:24:49 AM CST. Dependencies resolved. Package Architectur…

缓存的相关内容

缓存是一种介于数据永久存储介质与数据应用之间数据临时的存储介质 实用化保存可以有效地减少低俗数据读取的次数 (例如磁盘IO), 提高系统性能 缓存不仅可以用于提高永久性存储介质的数据读取效率&#xff0c;还可以提供临时的数据存储空间 spring boot中提供了缓存技术, 方便…

[ctfshow web入门] web77

信息收集 上一题的读取flag方式不能用了&#xff0c;使用后的回显是&#xff1a;could not find driver 解题 同样的查目录方法 cvar_export(scandir("glob:///*"));die();cforeach(new DirectoryIterator("glob:///*") as $a){echo($a->__toString…

C++学习-入门到精通-【7】类的深入剖析

C学习-入门到精通-【7】类的深入剖析 类的深入剖析 C学习-入门到精通-【7】类的深入剖析一、Time类的实例研究二、组成和继承三、类的作用域和类成员的访问类作用域和块作用域圆点成员选择运算符(.)和箭头成员选择运算符(->)访问函数和工具函数 四、具有默认实参的构造函数重…

主成分分析的应用之sklearn.decomposition模块的PCA函数

主成分分析的应用之sklearn.decomposition模块的PCA函数 一、模型建立整体步骤 二、数据 2297.86 589.62 474.74 164.19 290.91 626.21 295.20 199.03 2262.19 571.69 461.25 185.90 337.83 604.78 354.66 198.96 2303.29 589.99 516.21 236.55 403.92 730.05 438.41 225.80 …

1. Go 语言环境安装

&#x1f451; 博主简介&#xff1a;高级开发工程师 &#x1f463; 出没地点&#xff1a;北京 &#x1f48a; 人生目标&#xff1a;自由 ——————————————————————————————————————————— 版权声明&#xff1a;本文为原创文章&#xf…

IP协议深度解析:互联网世界的核心基石

作为互联网通信的基础协议&#xff0c;IP&#xff08;Internet Protocol&#xff09;承载着全球99%的网络数据流量。本文将深入剖析IP协议的核心特性、工作原理及演进历程&#xff0c;通过技术原理、协议对比和实战案例分析&#xff0c;为您揭示这个数字世界"隐形交通规则…

Oracle DBMS_STATS.GATHER_DATABASE_STATS 默认行为

Oracle DBMS_STATS.GATHER_DATABASE_STATS 默认行为 DBMS_STATS.GATHER_DATABASE_STATS的默认选项究竟是’GATHER’还是’GATHER AUTO’&#xff1f;这个问题非常重要&#xff0c;因为理解默认行为直接影响统计信息收集策略。 一 官方文档确认 根据Oracle 19c官方文档&#…

C++天空之城的树 全国信息素养大赛复赛决赛 C++小学/初中组 算法创意实践挑战赛 内部集训模拟题详细解析

C++天空之城的树 全国青少年信息素养大赛 C++复赛/决赛模拟练习题 博主推荐 所有考级比赛学习相关资料合集【推荐收藏】1、C++专栏 电子学会C++一级历年真题解析