在作業系統中,每個行程都有一個唯一的行程ID,每個執行緒也有自己唯一的執行緒ID。同樣,在Go語言中,每個Goroutine都有自己唯一的Go例程ID,這在panic等場景中經常遇到。雖然Goroutine有固有的ID,但是Go語言故意不提供取得這個ID的介面。這次我們將嘗試透過Go彙編語言來取得Goroutine ID。
1. 官方沒有goid的設計(https://github.com/golang/go/issues/22770)
根據官方相關資料,Go語言故意不提供goid的原因是為了避免濫用。因為大多數使用者在輕鬆獲得goid之後,在後續的程式設計中會不自覺地寫出強烈依賴goid的程式碼。對 goid 的強烈依賴會導致該程式碼難以移植,同時也會使並發模型變得複雜。同時,Go語言中可能存在大量的Goroutine,但每個Goroutine何時被銷毀並不容易即時監控,這也會導致依賴goid的資源無法自動回收(需要人工回收)。不過,如果你是 Go 組譯語言用戶,你完全可以忽略這些擔憂。
注意:如果強行獲得goid,可能會被「羞辱」? :
https://github.com/golang/go/blob/master/src/runtime/proc.go#L7120
2. Pure Go中獲取goid
為了方便理解,我們先嘗試取得純Go中的goid。雖然純Go中取得goid的效能比較低,但程式碼具有良好的可移植性,也可以用來測試驗證其他方法所取得的goid是否正確。
每個Go語言使用者都應該知道panic函數。呼叫panic函數會導致Goroutine異常。如果在到達 Goroutine 的根函數之前,recover 函數沒有處理恐慌,則運行時將列印相關異常和堆疊資訊並退出 Goroutine。
讓我們建構一個簡單的例子,透過panic輸出goid:
package main func main() { panic("leapcell") }
運作後會輸出以下資訊:
panic: leapcell goroutine 1 [running]: main.main() /path/to/main.go:4 +0x40
我們可以猜測Panic輸出訊息goroutine 1 [running]中的1就是goid。但是我們要如何取得程式中panic的輸出資訊呢?其實上面的資訊只是當前函數呼叫堆疊幀的文字描述。 runtime.Stack函數提供了取得這些資訊的功能。
我們來重建一個基於runtime.Stack函數的例子,透過輸出目前堆疊幀的資訊來輸出goid:
package main func main() { panic("leapcell") }
運作後會輸出以下資訊:
panic: leapcell goroutine 1 [running]: main.main() /path/to/main.go:4 +0x40
所以,從runtime.Stack得到的字串中解析出goid資訊就很容易了:
package main import "runtime" func main() { var buf = make([]byte, 64) var stk = buf[:runtime.Stack(buf, false)] print(string(stk)) }
GetGoid 函數的細節我們不再贅述。要注意的是,runtime.Stack函數不僅可以取得目前Goroutine的堆疊訊息,還可以取得所有Goroutine的堆疊資訊(由第二個參數控制)。同時Go語言中的net/http2.curGoroutineID函數取得goid的方式也是類似。
3. 從g結構中取得goid
根據Go官方彙編語言文檔,每個正在運行的Goroutine結構體的g指針都儲存在目前運行的Goroutine所在系統線程的本地儲存TLS中。我們可以先取得TLS線程本地存儲,然後從TLS中獲取g結構體的指針,最後從g結構體中提取goid。
下面是透過引用runtime套件中定義的get_tls巨集來取得g指標:
goroutine 1 [running]: main.main() /path/to/main.g
get_tls是runtime/go_tls.h頭檔中定義的巨集函數。
對於AMD64平台,get_tls宏函數定義如下:
import ( "fmt" "strconv" "strings" "runtime" ) func GetGoid() int64 { var ( buf [64]byte n = runtime.Stack(buf[:], false) stk = strings.TrimPrefix(string(buf[:n]), "goroutine") ) idField := strings.Fields(stk)[0] id, err := strconv.Atoi(idField) if err!= nil { panic(fmt.Errorf("can not get goroutine id: %v", err)) } return int64(id) }
擴充get_tls巨集函數後,取得g指標的程式碼如下:
get_tls(CX) MOVQ g(CX), AX // Move g into AX.
其實TLS類似執行緒本地儲存的位址,該位址對應的記憶體中的資料就是g指標。我們可以再直接一點:
#ifdef GOARCH_amd64 #define get_tls(r) MOVQ TLS, r #define g(r) 0(r)(TLS*1) #endif
基於上面的方法,我們可以封裝一個getg函數來取得g指標:
MOVQ TLS, CX MOVQ 0(CX)(TLS*1), AX
然後,在Go程式碼中,透過goid成員在g結構體中的偏移量來取得goid的值:
MOVQ (TLS), AX
這裡,g_goid_offset是goid成員的偏移量。 g結構指的是runtime/runtime2.go。
Go1.10版本中,goid的偏移量為152位元組。所以,上面的程式碼只能在 goid 偏移量也是 152 位元組的 Go 版本中正確運行。根據偉大湯普森的神諭,枚舉和蠻力是解決所有難題的靈丹妙藥。我們也可以將 goid 偏移量保存在一個表中,然後根據 Go 版本號查詢 goid 偏移量。
以下是改進後的程式碼:
// func getg() unsafe.Pointer TEXT ·getg(SB), NOSPLIT, <pre class="brush:php;toolbar:false">const g_goid_offset = 152 // Go1.10 func GetGroutineId() int64 { g := getg() p := (*int64)(unsafe.Pointer(uintptr(g) + g_goid_offset)) return *p }-8 MOVQ (TLS), AX MOVQ AX, ret+0(FP) RET
現在,goid 偏移量終於可以自動適配已發布的 Go 語言版本了。
4、取得g結構體對應的介面對象
雖然枚舉和暴力破解很簡單,但它們不能很好地支援正在開發的未發布的 Go 版本。我們無法事先知道 goid 成員在某個正在開發的版本中的偏移量。
如果是在runtime套件內部,我們可以透過unsafe.OffsetOf(g.goid)直接取得該成員的偏移量。我們也可以透過反射來取得g結構體的類型,然後透過該類型查詢某個成員的偏移量。由於g結構體是內部類型,Go程式碼無法從外部套件取得g結構體的類型資訊。然而,在Go彙編語言中,我們可以看到所有的符號,所以理論上,我們也可以得到g結構體的類型資訊。
定義任何型別後,Go語言都會產生該型別對應的型別資訊。例如,g結構體會產生一個type·runtime·g標識符來表示g結構體的值類型訊息,並且也會產生一個type·*runtime·g標識符來表示指標類型資訊。如果 g 結構體有方法,那麼也會產生 go.itab.runtime.g 和 go.itab.*runtime.g 類型訊息,用方法來表示類型資訊。
如果我們能得到代表g結構類型的type·runtime·g和g指針,那麼我們就可以建構g物件的介面了。下面是改進後的getg函數,回傳g指針對象的介面:
package main func main() { panic("leapcell") }
這裡,AX暫存器對應g指針,BX暫存器對應g結構體的型別。然後,使用runtime·convT2E函數將類型轉換為介面。因為我們沒有使用g結構的指標類型,所以傳回的介面表示g結構的值類型。理論上我們也可以建構一個g指標類型的接口,但是由於Go彙編語言的限制,我們無法使用type·*runtime·g標識符。
根據g回傳的接口,很容易取得goid:
panic: leapcell goroutine 1 [running]: main.main() /path/to/main.go:4 +0x40
以上程式碼直接透過反射取得goid。理論上只要反射介面的名稱和goid成員沒有變化,程式碼就可以正常運作。經過實際測試,上述程式碼在Go1.8、Go1.9、Go1.10版本中均能正確運作。樂觀地講,如果g結構類型的名稱不改變,Go語言的反射機制不改變,應該也能在未來的Go語言版本中運作。
雖然反射有一定的彈性,但是反射的表現卻一直被詬病。一個改進的想法是透過反射來取得goid的偏移量,然後透過g指標和偏移量來取得goid,這樣反射只需要在初始化階段執行一次。
以下是g_goid_offset變數的初始化程式碼:
package main import "runtime" func main() { var buf = make([]byte, 64) var stk = buf[:runtime.Stack(buf, false)] print(string(stk)) }
得到正確的goid偏移量後,按照前面提到的方式取得goid:
package main func main() { panic("leapcell") }
至此,我們取得goid的實作想法已經夠完整,但是彙編程式碼仍有嚴重的安全隱患。
雖然 getg 函數被宣告為禁止使用 NOSPLIT 標誌進行堆疊拆分的函數類型,但 getg 函數內部呼叫了更複雜的runtime·convT2E 函數。如果runtime·convT2E函數遇到堆疊空間不足,可能會觸發堆疊分裂操作。當堆疊分裂時,GC會移動函數參數、傳回值和局部變數中的堆疊指標。然而,我們的 getg 函數並沒有提供局部變數的指標資訊。
以下是改進後的getg函數的完整實作:
panic: leapcell goroutine 1 [running]: main.main() /path/to/main.go:4 +0x40
這裡,NO_LOCAL_POINTERS 表示函數沒有局部指標變數。同時對傳回的介面進行零值初始化,初始化完成後再用GO_RESULTS_INITIALIZED通知GC。這樣可以保證當堆疊分裂時,GC能夠正確處理傳回值和局部變數中的指標。
5. goid的應用:本地存儲
有了goid,建置Goroutine本地儲存就變得非常簡單。我們可以定義一個 gls 套件來提供 goid 功能:
package main import "runtime" func main() { var buf = make([]byte, 64) var stk = buf[:runtime.Stack(buf, false)] print(string(stk)) }
gls 套件變數只是包裝了一個映射,並支援透過sync.Mutex 互斥體進行並發存取。
然後定義一個內部 getMap 函數來取得每個 Goroutine 位元組的映射:
goroutine 1 [running]: main.main() /path/to/main.g
取得Goroutine的私有映射後,就是增刪改操作的正常介面:
import ( "fmt" "strconv" "strings" "runtime" ) func GetGoid() int64 { var ( buf [64]byte n = runtime.Stack(buf[:], false) stk = strings.TrimPrefix(string(buf[:n]), "goroutine") ) idField := strings.Fields(stk)[0] id, err := strconv.Atoi(idField) if err!= nil { panic(fmt.Errorf("can not get goroutine id: %v", err)) } return int64(id) }
最後,我們提供了一個Clean函數來釋放Goroutine對應的map資源:
get_tls(CX) MOVQ g(CX), AX // Move g into AX.
這樣,一個極簡的Goroutine本地儲存gls物件就完成了。
以下是使用本機儲存的簡單範例:
#ifdef GOARCH_amd64 #define get_tls(r) MOVQ TLS, r #define g(r) 0(r)(TLS*1) #endif
透過Goroutine本地存儲,不同層級的函數可以共享儲存資源。同時,為了避免資源洩漏,在Goroutine的根函數中,需要透過defer語句呼叫gls.Clean()函數來釋放資源。
Leapcell:用於託管 Golang 應用程式的高級無伺服器平台
最後推薦一個最適合部署Go服務的平台:leapcell
1. 多語言支持
- 使用 JavaScript、Python、Go 或 Rust 進行開發。
2.免費部署無限個項目
- 只需支付使用費用-無請求,不收費。
3. 無與倫比的成本效益
- 即用即付,無閒置費用。
- 範例:25 美元支援 694 萬個請求,平均回應時間為 60 毫秒。
4.簡化的開發者體驗
- 直覺的使用者介面,輕鬆設定。
- 完全自動化的 CI/CD 管道和 GitOps 整合。
- 即時指標和日誌記錄以獲取可操作的見解。
5. 輕鬆的可擴充性和高效能
- 自動擴充以輕鬆處理高並發。
- 零營運開銷-只需專注於建置。
在文件中探索更多內容!
Leapcell Twitter:https://x.com/LeapcellHQ
以上是如何取得Goroutine ID?的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本文演示了創建模擬和存根進行單元測試。 它強調使用接口,提供模擬實現的示例,並討論最佳實踐,例如保持模擬集中並使用斷言庫。 文章

OpenSSL,作為廣泛應用於安全通信的開源庫,提供了加密算法、密鑰和證書管理等功能。然而,其歷史版本中存在一些已知安全漏洞,其中一些危害極大。本文將重點介紹Debian系統中OpenSSL的常見漏洞及應對措施。 DebianOpenSSL已知漏洞:OpenSSL曾出現過多個嚴重漏洞,例如:心臟出血漏洞(CVE-2014-0160):該漏洞影響OpenSSL1.0.1至1.0.1f以及1.0.2至1.0.2beta版本。攻擊者可利用此漏洞未經授權讀取服務器上的敏感信息,包括加密密鑰等。

本文探討了GO的仿製藥自定義類型約束。 它詳細介紹了界面如何定義通用功能的最低類型要求,從而改善了類型的安全性和代碼可重複使用性。 本文還討論了局限性和最佳實踐

本文討論了GO的反思軟件包,用於運行時操作代碼,對序列化,通用編程等有益。它警告性能成本,例如較慢的執行和更高的內存使用,建議明智的使用和最佳

本文討論了GO中使用表驅動的測試,該方法使用測試用例表來測試具有多個輸入和結果的功能。它突出了諸如提高的可讀性,降低重複,可伸縮性,一致性和A

本文使用跟踪工具探討了GO應用程序執行流。 它討論了手冊和自動儀器技術,比較諸如Jaeger,Zipkin和Opentelemetry之類的工具,並突出顯示有效的數據可視化


熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

AI Hentai Generator
免費產生 AI 無盡。

熱門文章

熱工具

SAP NetWeaver Server Adapter for Eclipse
將Eclipse與SAP NetWeaver應用伺服器整合。

mPDF
mPDF是一個PHP庫,可以從UTF-8編碼的HTML產生PDF檔案。原作者Ian Back編寫mPDF以從他的網站上「即時」輸出PDF文件,並處理不同的語言。與原始腳本如HTML2FPDF相比,它的速度較慢,並且在使用Unicode字體時產生的檔案較大,但支援CSS樣式等,並進行了大量增強。支援幾乎所有語言,包括RTL(阿拉伯語和希伯來語)和CJK(中日韓)。支援嵌套的區塊級元素(如P、DIV),

記事本++7.3.1
好用且免費的程式碼編輯器

SublimeText3 Mac版
神級程式碼編輯軟體(SublimeText3)

Safe Exam Browser
Safe Exam Browser是一個安全的瀏覽器環境,安全地進行線上考試。該軟體將任何電腦變成一個安全的工作站。它控制對任何實用工具的訪問,並防止學生使用未經授權的資源。