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.pswp.cn/bicheng/81741.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/81741.shtml
英文地址,請注明出處:http://en.pswp.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…