Go Runtime 功能初探
題圖來自 Understand Compile Time && Runtime! Improving Golang Performance(1)[1]
以下內容,是對 運行時 runtime 的神奇用法 [2] 的學習與記錄
目錄:
-
- 獲取 GOROOT 環境變量
-
- 獲取 GO 的版本號
-
- 獲取本機 CPU 個數
-
- 設置最大可同時執行的最大 CPU 數
-
- 設置 cup profile 記錄的速錄
-
- 查看 cup profile 下一次堆棧跟蹤數據
-
- 立即執行一次垃圾回收
-
- 給變量綁定方法, 當垃圾回收的時候進行監聽
-
- 查看內存申請和分配統計信息
-
- 查看程序正在使用的字節數
-
- 查看程序正在使用的對象數
-
- 獲取調用堆棧列表
-
- 獲取內存 profile 記錄歷史
-
- 執行一個斷點
-
- 獲取程序調用 go 協程的棧蹤跡歷史
-
- 獲取當前函數或者上層函數的標識號、文件名、調用方法在當前文件中的行號
-
- 獲取與當前堆棧記錄相關鏈的調用棧蹤跡
-
- 獲取一個標識調用棧標識符 pc 對應的調用棧
-
- 獲取調用棧所調用的函數的名字
-
- 獲取調用棧所調用的函數的所在的源文件名和行號
-
- 獲取該調用棧的調用棧標識符
-
- 獲取當前進程執行的 cgo 調用次數
-
- 獲取當前存在的 go 協程數
-
- 終止掉當前的 go 協程
-
- 讓其他 go 協程優先執行, 等其他協程執行完後, 在執行當前的協程
-
- 獲取活躍的 go 協程的堆棧 profile 以及記錄個數
-
- 將調用的 go 協程綁定到當前所在的操作系統線程,其它 go 協程不能進入該線程
-
- 解除 go 協程與操作系統線程的綁定關係
-
- 獲取線程創建 profile 中的記錄個數
-
- 控制阻塞 profile 記錄 go 協程阻塞事件的採樣率
-
- 返回當前阻塞 profile 中的記錄個數
1. GOROOT() 獲取 GOROOT 環境變量
GOROOT() 返回 Go 的根目錄。如果存在 GOROOT 環境變量,返回該變量的值;否則,返回創建 Go 時的根目錄
package main
import (
"fmt"
"runtime"
)
func main() {
fmt.Println(runtime.GOROOT()) // /Users/fliter/.g/go
}
2. Version() 獲取 GO 的版本號
Version() 返回 Go 的版本字符串。要麼是提交的 hash 和創建時的日期;要麼是發行標籤如 "go1.20"
package main
import (
"fmt"
"runtime"
)
func main() {
fmt.Println(runtime.Version()) // go1.19
}
3. NumCPU() 獲取本機 CPU 個數
NumCPU 返回本地機器的邏輯 CPU 個數
package main
import (
"fmt"
"runtime"
)
func main() {
fmt.Println(runtime.NumCPU()) // 8
}
4. GOMAXPROCS() 設置最大可同時執行的最大 CPU 數
GOMAXPROCS() 設置可同時執行的最大 CPU 數,並返回先前的設置。 若 n < 1,則不會更改當前設置。
本地機器的邏輯 CPU 數可通過 NumCPU 查詢。該函數在調度程序優化後會去掉?(啥時候..)
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
runtime.GOMAXPROCS(1)
startTime := time.Now()
var s1 chan int64 = make(chan int64)
var s2 chan int64 = make(chan int64)
var s3 chan int64 = make(chan int64)
var s4 chan int64 = make(chan int64)
go calc(s1)
go calc(s2)
go calc(s3)
go calc(s4)
<-s1
<-s2
<-s3
<-s4
endTime := time.Now()
fmt.Println(endTime.Sub(startTime)) // 第一行註釋掉 耗時: 386.954625ms; 取消註釋 耗時: 1.34715s
}
func calc(s chan int64) {
var count int64 = 0
for i := 0; i < 1000000000; i++ {
count += int64(i)
}
s <- count
}
5. SetCPUProfileRate() 設置 cup profile 記錄的速錄
SetCPUProfileRate() 設置 CPU profile 記錄的速率爲平均每秒 hz 次。如果 hz<=0,SetCPUProfileRate 會關閉 profile 的記錄。如果記錄器在執行,該速率必須在關閉之後才能修改
絕大多數使用者應使用 runtime/pprof 包或 testing 包的-test.cpuprofile
選項而非直接使用 SetCPUProfileRate
6. CPUProfile 查看 cup profile 下一次堆棧跟蹤數據
func CPUProfile() []byte
已廢棄
7. GC() 立即執行一次垃圾回收
Go 三種觸發 GC 的方式之一 (另外兩種爲 2 分鐘固定一次 && 達到閾值時觸發)
package main
import (
"runtime"
"time"
)
type Student struct {
name string
}
func main() {
var i *Student = new(Student)
runtime.SetFinalizer(i, func(i interface{}) {
println("垃圾回收了") // 垃圾回收了
})
runtime.GC() // 如果將這行註釋,則上面不會輸出
time.Sleep(time.Second)
}
8. SetFinalizer() 給變量綁定方法, 當垃圾回收的時候進行監聽
func SetFinalizer(x, f interface{})
注意 x 必須是指針類型, f 函數的參數一定要和 x 保持一致, 或者寫 interface{}, 不然程序會報錯
代碼同上例
9. ReadMemStats() 查看內存申請和分配統計信息
可以獲得如下信息:
type MemStats struct {
// 一般統計
Alloc uint64 // 已申請且仍在使用的字節數
TotalAlloc uint64 // 已申請的總字節數(已釋放的部分也算在內)
Sys uint64 // 從系統中獲取的字節數(下面XxxSys之和)
Lookups uint64 // 指針查找的次數
Mallocs uint64 // 申請內存的次數
Frees uint64 // 釋放內存的次數
// 主分配堆統計
HeapAlloc uint64 // 已申請且仍在使用的字節數
HeapSys uint64 // 從系統中獲取的字節數
HeapIdle uint64 // 閒置span中的字節數
HeapInuse uint64 // 非閒置span中的字節數
HeapReleased uint64 // 釋放到系統的字節數
HeapObjects uint64 // 已分配對象的總個數
// L低層次、大小固定的結構體分配器統計,Inuse爲正在使用的字節數,Sys爲從系統獲取的字節數
StackInuse uint64 // 引導程序的堆棧
StackSys uint64
MSpanInuse uint64 // mspan結構體
MSpanSys uint64
MCacheInuse uint64 // mcache結構體
MCacheSys uint64
BuckHashSys uint64 // profile桶散列表
GCSys uint64 // GC元數據
OtherSys uint64 // 其他系統申請
// 垃圾收集器統計
NextGC uint64 // 會在HeapAlloc字段到達該值(字節數)時運行下次GC
LastGC uint64 // 上次運行的絕對時間(納秒)
PauseTotalNs uint64
PauseNs [256]uint64 // 近期GC暫停時間的循環緩衝,最近一次在[(NumGC+255)%256]
NumGC uint32
EnableGC bool
DebugGC bool
// 每次申請的字節數的統計,61是C代碼中的尺寸分級數
BySize [61]struct {
Size uint32
Mallocs uint64
Frees uint64
}
}
package main
import (
"fmt"
"runtime"
"time"
)
type Student2 struct {
name string
}
func main() {
var list = make([]*Student2, 0)
for i := 0; i < 100000; i++ {
var s *Student2 = new(Student2)
list = append(list, s)
}
memStatus := runtime.MemStats{}
runtime.ReadMemStats(&memStatus)
fmt.Printf("申請的內存:%d\n", memStatus.Mallocs) // 申請的內存:100250
fmt.Printf("釋放的內存次數:%d\n", memStatus.Frees) // 釋放的內存次數:45
time.Sleep(time.Second)
}
10. InUseBytes() 查看程序正在使用的字節數
func (r *MemProfileRecord) InUseBytes() int64
InUseBytes 返回正在使用的字節數(AllocBytes – FreeBytes)
11. InUseObjects() 查看程序正在使用的對象數
func (r *MemProfileRecord) InUseObjects() int64
InUseObjects 返回正在使用的對象數(AllocObjects - FreeObjects)
12. Stack() 獲取調用堆棧列表
func (r *MemProfileRecord) Stack() []uintptr
Stack 返回關聯至此記錄的調用棧蹤跡,即 r.Stack0 的前綴
13. MemProfile() 獲取內存 profile 記錄歷史
func MemProfile(p []MemProfileRecord, inuseZero bool) (n int, ok bool)
MemProfile 返回當前內存 profile 中的記錄數 n
-
若len(p)>=n,MemProfile會將此分析報告複製到p中並返回(n, true);
-
若len(p)<n,MemProfile則不會更改p,而只返回(n, false)
如果 inuseZero 爲 true,該 profile 就會包含無效分配記錄(其中 r.AllocBytes>0,而 r.AllocBytes==r.FreeBytes。這些內存都是被申請後又釋放回運行時環境的)
大多數調用者應當使用 runtime/pprof 包或 testing 包的-test.memprofile
標記,而非直接調用 MemProfile
14. Breakpoint() 執行一個斷點
runtime.Breakpoint()
15. Stack() 獲取程序調用 go 協程的棧蹤跡歷史
func Stack(buf []byte, all bool) int
Stack 將調用其的 go 程的調用棧蹤跡格式化後寫入到 buf 中並返回寫入的字節數
若 all 爲 true,函數會在寫入當前 go 程的蹤跡信息後,將其它所有 go 程的調用棧蹤跡都格式化寫入到 buf 中
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
go showRecord()
time.Sleep(time.Second)
buf := make([]byte, 10000000000)
runtime.Stack(buf, true)
fmt.Println(string(buf))
}
func showRecord() {
ticker := time.Tick(time.Second)
for t := range ticker {
fmt.Println(t)
}
}
輸出:
2023-04-19 17:25:26.386522 +0800 CST m=+1.001105543
2023-04-19 17:25:27.386892 +0800 CST m=+2.001489376
2023-04-19 17:25:28.386505 +0800 CST m=+3.001116043
2023-04-19 17:25:29.38553 +0800 CST m=+4.000154334
2023-04-19 17:25:30.385618 +0800 CST m=+5.000255584
2023-04-19 17:25:31.385817 +0800 CST m=+6.000468334
2023-04-19 17:25:32.385528 +0800 CST m=+7.000192918
2023-04-19 17:25:33.385646 +0800 CST m=+8.000323543
goroutine 1 [running]:
main.main()
/Users/fliter/runtime-demo/15Stack.go:13 +0x68
goroutine 4 [chan receive]:
main.showRecord()
/Users/fliter/runtime-demo/15Stack.go:19 +0xac
created by main.main
/Users/fliter/runtime-demo/15Stack.go:10 +0x24
16. Caller() 獲取當前函數或者上層函數的標識號、文件名、調用方法在當前文件中的行號
func Caller(skip int) (pc uintptr, file string, line int, ok bool)
package main
import (
"fmt"
"runtime"
)
func main() {
pc, file, line, ok := runtime.Caller(0)
fmt.Println(pc) // 4336410771
fmt.Println(file) // /Users/fliter/runtime-demo/16Caller.go
fmt.Println(line) //9
fmt.Println(ok) //true
}
pc = 4336410771 不是 main 函數的標識, 而是 runtime.Caller 方法的標識, line = 13 標識它在 main 方法中的第 13 行被調用
//package main
//
//import (
// "fmt"
// "runtime"
//)
//
//func main() {
// pc, file, line, ok := runtime.Caller(0)
// fmt.Println(pc) // 4336410771
// fmt.Println(file) // /Users/fliter/runtime-demo/16Caller.go
// fmt.Println(line) //9
// fmt.Println(ok) //true
//}
package main
import (
"fmt"
"runtime"
)
func main() {
pc, _, line, _ := runtime.Caller(1)
fmt.Printf("main函數的pc:%d\n", pc) // main函數的pc:4364609931
fmt.Printf("main函數被調用的行數:%d\n", line) // main函數被調用的行數:250
show()
}
func show() {
pc, _, line, _ := runtime.Caller(1)
fmt.Printf("show函數的pc:%d\n", pc) // show函數的pc:4364974271
fmt.Printf("show函數被調用的行數:%d\n", line) // show函數被調用的行數:27
// 這個是main函數的棧
pc, _, line, _ = runtime.Caller(2)
fmt.Printf("show的上層函數的pc:%d\n", pc) // show的上層函數的pc:4364609931
fmt.Printf("show的上層函數被調用的行數:%d\n", line) // show的上層函數被調用的行數:250
pc, _, _, _ = runtime.Caller(3)
fmt.Println(pc) //4364778899
pc, _, _, _ = runtime.Caller(4)
fmt.Println(pc) // 0
}
golang 獲取調用者的方法名及所在行數 [3]
runtime.Caller 的性能問題 [4]
17. Callers() 獲取與當前堆棧記錄相關鏈的調用棧蹤跡
func Callers(skip int, pc []uintptr) int
會把當前 go 程調用棧上的調用棧標識符填入切片 pc 中,返回寫入到 pc 中的項數。實參 skip 爲開始在 pc 中記錄之前所要跳過的棧幀數,0 表示 Callers 自身的調用棧,1 表示 Callers 所在的調用棧。返回寫入 p 的項數
package main
import (
"fmt"
"runtime"
)
func main() {
pcs := make([]uintptr, 10)
i := runtime.Callers(1, pcs)
fmt.Println(pcs[:i]) // [4311883569 4311525404 4311694372]
}
獲得了三個 pc 其中有一個是 main 方法自身的
18. FuncForPC() 獲取一個標識調用棧標識符 pc 對應的調用棧
func FuncForPC(pc uintptr) *Func
package main
import (
"runtime"
)
func main() {
pcs := make([]uintptr, 10)
i := runtime.Callers(1, pcs)
for _, pc := range pcs[:i] {
println(runtime.FuncForPC(pc))
}
}
輸出:
0x102f660f0
0x102f5c9f0
0x102f64840
用途見下
19. Name() 獲取調用棧所調用的函數的名字
func (f *Func) string
package main
import (
"runtime"
)
func main() {
pcs := make([]uintptr, 10)
i := runtime.Callers(1, pcs)
for _, pc := range pcs[:i] {
funcPC := runtime.FuncForPC(pc)
println(funcPC.Name())
}
}
輸出:
main.main
runtime.main
runtime.goexit
20. FileLine() 獲取調用棧所調用的函數的所在的源文件名和行號
func (f *Func) FileLine(pc uintptr) (file string, line int)
package main
import (
"runtime"
)
func main() {
pcs := make([]uintptr, 10)
i := runtime.Callers(1, pcs)
for _, pc := range pcs[:i] {
funcPC := runtime.FuncForPC(pc)
file, line := funcPC.FileLine(pc)
println(funcPC.Name(), file, line)
}
}
輸出:
main.main /Users/fliter/runtime-demo/20FileLine.go 9
runtime.main /Users/fliter/.g/go/src/runtime/proc.go 259
runtime.goexit /Users/fliter/.g/go/src/runtime/asm_arm64.s 1166
21. Entry() 獲取該調用棧的調用棧標識符
func (f *Func) Entry() uintptr
package main
import (
"runtime"
)
func main() {
pcs := make([]uintptr, 10)
i := runtime.Callers(1, pcs)
for _, pc := range pcs[:i] {
funcPC := runtime.FuncForPC(pc)
println(funcPC.Entry())
}
}
輸出:
4310699120
4310540672
4310690704
22. NumCgoCall() 獲取當前進程執行的 cgo 調用次數
獲取當前進程調用 C 方法的次數
func NumCgoCall() int64
package main
import (
"runtime"
)
/*
#include <stdio.h>
*/
import "C"
func main() {
println(runtime.NumCgoCall()) // 1
}
沒有調用 C 的方法爲什麼是 1 呢?因爲 import C 會調用 C 包中的 init 方法
package main
import (
"runtime"
)
/*
#include <stdio.h>
// 自定義一個c語言的方法
static void myPrint(const char* msg) {
printf("myPrint: %s", msg);
}
*/
import "C"
func main() {
// 調用c方法
C.myPrint(C.CString("Hello,C\n")) // myPrint: Hello,C
println(runtime.NumCgoCall()) // 3
}
23. NumGoroutine() 獲取當前存在的 go 協程數
func NumGoroutine() int
package main
import "runtime"
func main() {
go print()
print()
println(runtime.NumGoroutine()) // 2
}
func print() {
}
當前程序有 2 個 go 協程 一個是 main.go 主協程, 另外一個是 go print()
24. Goexit() 終止掉當前的 go 協程
func Goexit()
package main
import (
"fmt"
"runtime"
)
func main() {
print()
fmt.Println("繼續執行")
}
func print() {
fmt.Println("準備結束go協程")
runtime.Goexit()
defer fmt.Println("結束了")
}
輸出:
準備結束go協程
fatal error: no goroutines (main called runtime.Goexit) - deadlock!
exit status 2
Goexit 終止調用它的 go 協程, 其他協程不受影響, Goexit 會在終止該 go 協程前執行所有的 defer 函數,前提是 defer 必須在它前面定義,如下
package main
import (
"fmt"
"runtime"
)
func main() {
print()
fmt.Println("繼續執行")
}
func print() {
fmt.Println("準備結束go協程")
defer fmt.Println("結束了--會輸出出來")
runtime.Goexit()
//defer fmt.Println("結束了")
}
輸出:
準備結束go協程
結束了--會輸出出來
fatal error: no goroutines (main called runtime.Goexit) - deadlock!
exit status 2
如果在 main 主協程調用該方法, 會終止 主協程, 但不會讓 main 返回, 因爲 main 函數沒有返回,
程序會繼續執行其他 go 協程, 當其他 go 協程執行完畢後, 程序就會崩潰
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
start := time.Now()
go func() {
time.Sleep(3e9)
println("123")
}()
defer fmt.Println(time.Since(start))
runtime.Goexit()
}
輸出:
20.5µs
123
fatal error: no goroutines (main called runtime.Goexit) - deadlock!
exit status 2
25. Gosched() 讓其他 go 協程優先執行, 等其他協程執行完後, 再執行當前的協程
func Gosched()
package main
import (
"fmt"
)
func main() {
go print25()
fmt.Println("繼續執行")
}
func print25() {
fmt.Println("執行打印方法")
}
調用了 go print 方法, 但是還未執行, main 函數就執行完畢了(啓一個協程也是需要時間的,這個時間比 for 循環,比程序繼續執行要耗時多很多)
可以使用 channel,waitgroup 等,此處使用 runtime.Gosched()
package main
import (
"fmt"
"runtime"
)
func main() {
go print25()
runtime.Gosched()
fmt.Println("繼續執行")
}
func print25() {
fmt.Println("執行打印方法")
}
輸出:
執行打印方法
繼續執行
[Rust vs Go: 常用語法對比 (13)- 將優先權讓給其他線程](https://json.dashen.tech/2021/09/14/Rust-vs-Go-%E5%B8%B8%E7%94%A8%E8%AF%AD%E6%B3%95%E5%AF%B9%E6%AF%94-13/ "Rust vs Go: 常用語法對比 (13"Rust vs Go: 常用語法對比 (13)- 將優先權讓給其他線程 ")- 將優先權讓給其他線程")
Go 用兩個協程交替打印 100 以內的奇偶數 [5]
26. GoroutineProfile() 獲取活躍的 go 協程的堆棧 profile 以及記錄個數
func GoroutineProfile(p []StackRecord) (n int, ok bool)
// GoroutineProfile returns n, the number of records in the active goroutine stack profile.
// If len(p) >= n, GoroutineProfile copies the profile into p and returns n, true.
// If len(p) < n, GoroutineProfile does not change p and returns n, false.
//
// Most clients should use the runtime/pprof package instead
// of calling GoroutineProfile directly.
func GoroutineProfile(p []StackRecord) (n int, ok bool) {
return goroutineProfileWithLabels(p, nil)
}
package main
import (
"fmt"
"runtime"
)
func main() {
for i := 0; i < 10; i++ {
go func(k int) {
fmt.Println(i)
}(i)
}
fmt.Println("----------------")
p := make([]runtime.StackRecord, 10000)
fmt.Println(runtime.GoroutineProfile(p)) // 1 true
}
輸出:
10
10
10
10
10
10
10
----------------
7
10
10
1 true
pprof 裏面使用了此 func
Go 應用的性能優化 [6]
27. LockOSThread() 將調用的 go 協程綁定到當前所在的操作系統線程,其它 go 協程不能進入該線程
func LockOSThread()
將調用的 go 程綁定到它當前所在的操作系統線程。除非調用的 go 程退出或調用 UnlockOSThread,否則它將總是在該線程中執行,而其它 go 程不能進入該線程
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
go calcSum1()
go calcSum2()
time.Sleep(time.Second * 10)
}
func calcSum1() {
runtime.LockOSThread()
start := time.Now()
count := 0
for i := 0; i < 10000000000; i++ {
count += i
}
end := time.Now()
fmt.Println("calcSum1耗時")
fmt.Println(end.Sub(start))
defer runtime.UnlockOSThread()
}
func calcSum2() {
start := time.Now()
count := 0
for i := 0; i < 10000000000; i++ {
count += i
}
end := time.Now()
fmt.Println("calcSum2耗時")
fmt.Println(end.Sub(start))
}
輸出:
calcSum1耗時
3.295679583s
calcSum2耗時
3.296763125s
看起來沒有太大的差別;
但估計在很多個協程 (涉及到頻繁的調度和切換),但是有一項重要功能需獨佔一個核,可使用該 func
Go LockOSThread[7]
28. UnlockOSThread() 解除 go 協程與操作系統線程的綁定關係
func UnlockOSThread()
將調用此 func 的協程,解除和其綁定的操作系統線程
若調用的協程未調用 LockOSThread,UnlockOSThread 不做操作
從 1,10 之後,調用了多少次 LockOSThread,就要使用 UnlockOSThread 接觸綁定..
29. ThreadCreateProfile() 獲取線程創建 profile 中的記錄個數
func ThreadCreateProfile(p []StackRecord) (n int, ok bool)
返回線程創建 profile 中的記錄個數。
-
如果len(p)>=n,本func就會將profile中的記錄複製到p中並返回(n, true)
-
若len(p)<n,則不會更改p,而只返回(n, false)
絕大多數情況下應當使用 runtime/pprof 包,而非直接調用 ThreadCreateProfile
30. SetBlockProfileRate() 控制阻塞 profile 記錄 go 協程阻塞事件的採樣率
func SetBlockProfileRate(rate int)
SetBlockProfileRate 控制阻塞 profile 記錄 go 程阻塞事件的採樣頻率。對於一個阻塞事件,平均每阻塞 rate 納秒,阻塞 profile 記錄器就採集一份樣本。
-
要在profile中包括每一個阻塞事件,需傳入rate=1
-
要完全關閉阻塞profile的記錄,需傳入rate<=0
31. BlockProfile() 返回當前阻塞 profile 中的記錄個數
func BlockProfile(p []BlockProfileRecord) (n int, ok bool)
BlockProfile 返回當前阻塞 profile 中的記錄個數
-
如果len(p)>=n,本函數就會將此profile中的記錄複製到p中並返回(n, true)
-
如果len(p)<n,本函數則不會修改p,而只返回(n, false)
絕大多數情況應當使用 runtime/pprof 包或 testing 包的-test.blockprofile
標記, 而非直接調用 BlockProfile
參考資料
[1]
Understand Compile Time && Runtime! Improving Golang Performance(1): https://levelup.gitconnected.com/improving-golang-performance-1-must-master-two-key-concepts-238a2055c926
[2]
運行時 runtime 的神奇用法: https://blog.csdn.net/u011525168/article/details/88401166
[3]
golang 獲取調用者的方法名及所在行數: https://dashen.tech/2018/05/18/golang%E8%8E%B7%E5%8F%96%E8%B0%83%E7%94%A8%E8%80%85%E7%9A%84%E6%96%B9%E6%B3%95%E5%90%8D%E5%8F%8A%E6%89%80%E5%9C%A8%E8%A1%8C%E6%95%B0/
[4]
runtime.Caller 的性能問題: https://dashen.tech/2019/11/11/runtime-Caller%E7%9A%84%E6%80%A7%E8%83%BD%E9%97%AE%E9%A2%98/
[5]
Go 用兩個協程交替打印 100 以內的奇偶數: https://dashen.tech/2022/04/03/Go%E7%94%A8%E4%B8%A4%E4%B8%AA%E5%8D%8F%E7%A8%8B%E4%BA%A4%E6%9B%BF%E6%89%93%E5%8D%B0100%E4%BB%A5%E5%86%85%E7%9A%84%E5%A5%87%E5%81%B6%E6%95%B0/
[6]
Go 應用的性能優化: https://zhuanlan.zhihu.com/p/406826295
[7]
Go LockOSThread: https://dashen.tech/2017/07/11/Go-LockOSThread/
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/HP1075oY3xQ3CbTwZ0veeQ