一鍵定位Golang線上服務(wù)內(nèi)存泄露的秘籍
1.出現(xiàn)內(nèi)存泄漏
1.1 事發(fā)現(xiàn)場(chǎng)
在風(fēng)和日麗的一天,本人正看著需求、敲著代碼,展望美好的未來(lái)。突然收到一條內(nèi)存使用率過(guò)高的告警。
1.2 證人證詞
告警的這個(gè)項(xiàng)目,老代碼是python的,最近一直在go化。隨著go化率不斷上升,發(fā)現(xiàn)內(nèi)存的RSS使用率越飆越高。最終達(dá)到容器內(nèi)存限制后,進(jìn)程會(huì)自動(dòng)重啟。RSS如下圖所示:
2.排查內(nèi)存泄露
2.1 分析問(wèn)題
看到這種不正常的RSS增長(zhǎng),第一反應(yīng)是:是不是最近上的代碼有什么問(wèn)題?是不是發(fā)生了內(nèi)存泄露??jī)?nèi)存泄露可是大事,趕緊查查。于是將時(shí)間線拉長(zhǎng),看看是從哪天開(kāi)始的。結(jié)果,現(xiàn)實(shí)是很殘酷的。從項(xiàng)目剛上線的時(shí)候就有這個(gè)問(wèn)題了。由于項(xiàng)目是2周一個(gè)版本,以前是還沒(méi)達(dá)到內(nèi)存限制,所以沒(méi)有發(fā)出告警。
那么問(wèn)題應(yīng)該就是在最初的版本里。這個(gè)時(shí)候就想了想,難道是我們使用的框架本身存在缺陷?但是很快就否定了這個(gè)想法,因?yàn)槲覀兪褂玫目蚣苁瞧渌?xiàng)目已經(jīng)上線已久的成熟框架。不應(yīng)該有這個(gè)問(wèn)題。
顯然,看代碼這種本辦法是不可能發(fā)現(xiàn)問(wèn)題的。于是想到了golang的性能分析工具pprof。由于pprof線上環(huán)境是不開(kāi)啟的,所以排查我這里只能去預(yù)發(fā)環(huán)境。
2.2 尋找問(wèn)題
2.2.1 獲取內(nèi)存使用監(jiān)控
go tool pprof -source_path=/path/to/gopath -inuse_space https://target.service.url/debug/pprof/allocs
-source_path Search path for source files
是分析代碼時(shí),需要用到源碼路徑,這里就是你自己本地的gopath路徑/debug/pprof/allocs 用來(lái)指定分析的是內(nèi)存分配-inuse_space Same as -sample_index=inuse_space
是監(jiān)控使用中的內(nèi)存。因?yàn)槲覀兎治龅氖莾?nèi)存泄露,所以要查看的是實(shí)際占用的內(nèi)存
輸入以上命令,會(huì)出現(xiàn)以下界面的內(nèi)容:
2.2.2 分析內(nèi)存監(jiān)控
2.2.2.1 獲取top10的內(nèi)存占用
由于我們需要分析內(nèi)存占用,所以這個(gè)時(shí)候輸入一個(gè)**top10 **,看看占用內(nèi)存前10的都是哪些代碼。
(pprof) top 10 Showing nodes accounting for 145.07MB, 92.64% of 156.59MB total Dropped 163 nodes (cum <= 0.78MB) Showing top 10 nodes out of 157 flat flat% sum% cum cum% 117.36MB 74.95% 74.95% 117.36MB 74.95% github.com/beorn7/perks/quantile.newStream (inline) 14.55MB 9.29% 84.25% 134.42MB 85.84% github.com/prometheus/client_golang/prometheus.newSummary 3.53MB 2.25% 86.50% 4.06MB 2.59% compress/flate.NewWriter 2MB 1.28% 87.77% 2MB 1.28% github.com/prometheus/client_golang/prometheus.MakeLabelPairs 1.53MB 0.97% 88.75% 1.53MB 0.97% github.com/rcrowley/go-metrics.newExpDecaySampleHeap 1.50MB 0.96% 89.71% 1.50MB 0.96% go.opentelemetry.io/otel/sdk/trace.(*recordingSpan).snapshot 1.50MB 0.96% 90.67% 1.50MB 0.96% github.com/Shopify/sarama.(*TopicMetadata).decode 1.06MB 0.68% 91.35% 1.06MB 0.68% github.com/valyala/fasthttp/stackless.NewFunc 1.03MB 0.66% 92.00% 1.03MB 0.66% github.com/xdg-go/stringprep.init 1MB 0.64% 92.64% 1MB 0.64% strings.(*Builder).WriteByte
這個(gè)時(shí)候需要解釋一下顯示的指標(biāo)的含義
flat:函數(shù)在內(nèi)存上的占用flat%:函數(shù)在內(nèi)存占用上的占用百分比sum%:是從上往下到當(dāng)前行所有函數(shù)累加使用內(nèi)存的比例
如第二行,sum=84.25=74.95+9.29
cum:這個(gè)函數(shù)以及子函數(shù)運(yùn)行所占用的內(nèi)存,應(yīng)該大于等于flatcum%:這個(gè)函數(shù)以及子函數(shù)運(yùn)行所占用的內(nèi)存的比例,應(yīng)該大于等于flat%
2.2.2.2 查看占用函數(shù)調(diào)用棧
看完以上返回,明眼人應(yīng)該就能看出,第一行這個(gè)newStream問(wèn)題很大呀,讓我們進(jìn)去看看他哪行代碼出了問(wèn)題。需要用到一下命令
讓我們輸入list github.com/beorn7/perks/quantile.newStream一探究竟
(pprof) list:
Output annotated source for functions matching regexp
顯示具體調(diào)用的代碼塊并顯示相應(yīng)指標(biāo)
(pprof) list github.com/beorn7/perks/quantile.newStream Total: 156.59MB ROUTINE ======================== github.com/beorn7/perks/quantile.newStream in pkg/mod/github.com/beorn7/perks@v1.0.1/quantile/stream.go 117.36MB 117.36MB (flat, cum) 74.95% of Total . . 128: sorted bool . . 129:} . . 130: . . 131:func newStream(? invariant) *Stream { . . 132: x := &stream{?: ?} 117.36MB 117.36MB 133: return &Stream{x, make(Samples, 0, 500), true} . . 134:} . . 135: . . 136:// Insert inserts v into the stream. . . 137:func (s *Stream) Insert(v float64) { . . 138: s.insert(Sample{Value: v, Width: 1})
2.2.2.3 分析泄露原因
看到這里,應(yīng)該能看出這個(gè)newStream的內(nèi)存占用,主要是因?yàn)樯闪艘粋€(gè)容量為500的數(shù)組。那這個(gè)數(shù)組是什么樣的呢?
type Sample struct { Value float64 `json:",string"` Width float64 `json:",string"` Delta float64 `json:",string"` }
以上結(jié)構(gòu)可以看出,生成一次需要占用的內(nèi)存是50038字節(jié),那么一次就是12000個(gè)字節(jié),差不多是11.72kb。這么看來(lái),應(yīng)該是有個(gè)地方不停的調(diào)用,導(dǎo)致數(shù)據(jù)持續(xù)膨脹。看到這里,我們繼續(xù)往下追。
(pprof) list github.com/prometheus/client_golang/prometheus.newSummary Total: 156.59MB ROUTINE ======================== github.com/prometheus/client_golang/prometheus.newSummary in pkg/mod/github.com/prometheus/client_golang@v1.12.2/prometheus/summary.go 14.55MB 134.42MB (flat, cum) 85.84% of Total . . 220: } . . 221: s.init(s) // Init self-collection. . . 222: return s . . 223: } . . 224: 512.12kB 512.12kB 225: s := &summary{ . . 226: desc: desc, . . 227: . . 228: objectives: opts.Objectives, . . 229: sortedObjectives: make([]float64, 0, len(opts.Objectives)), . . 230: . 1MB 231: labelPairs: MakeLabelPairs(desc, labelValues), . . 232: 7.03MB 7.03MB 233: hotBuf: make([]float64, 0, opts.BufCap), 7.03MB 7.03MB 234: coldBuf: make([]float64, 0, opts.BufCap), . . 235: streamDuration: opts.MaxAge / time.Duration(opts.AgeBuckets), . . 236: } . . 237: s.headStreamExpTime = time.Now().Add(s.streamDuration) . . 238: s.hotBufExpTime = s.headStreamExpTime . . 239: . . 240: for i := uint32(0); i < opts.AgeBuckets; i++ { . 118.86MB 241: s.streams = append(s.streams, s.newStream()) . . 242: } . . 243: s.headStream = s.streams[0] . . 244: . . 245: for qu := range s.objectives { . . 246: s.sortedObjectives = append(s.sortedObjectives, qu)
由此看出,還不止使用一次newStream()。通過(guò)觀看代碼,我這里發(fā)現(xiàn),此處的opts.AgeBuckets是等于5的,那么就意味著,循環(huán)生成了5個(gè)stream,實(shí)際上占用的內(nèi)存是50038*5=60000字節(jié),也就是58.6kb。
2.2.2.4 分析調(diào)用鏈路
那么現(xiàn)在基本追溯完了大概的泄露原因。那怎么樣能尋找是具體的調(diào)用鏈的呢,總不能一層一層往上查找調(diào)用吧?這個(gè)時(shí)候pprof提供了一個(gè)命令,可以把整體調(diào)用生成一張圖片展示。命令如下:
go tool pprof -png -source_path=/path/to/gopath -inuse_space https://target.service.url/debug/pprof/allocs > heap.png
只需要在命令中加一個(gè)-png,那么就會(huì)生成一張圖片。當(dāng)然為了方便尋找,最后可以指定圖片生成地址。我這邊抓取了和本文有關(guān)的一段截圖,如下。
根據(jù)上圖鏈路,我們大致可以看出。應(yīng)該是mysql的調(diào)用,在OnFinished處,prometheus的上報(bào)的地方出現(xiàn)了內(nèi)存泄露。這個(gè)時(shí)候我們就可以追一下OnFinished處的代碼了,因?yàn)橹蟮亩际莗rometheus的調(diào)用,這是一個(gè)成熟的三方,理論不應(yīng)該是他這個(gè)點(diǎn)出問(wèn)題。
2.2.3 尋找泄露代碼
OnFinished的代碼如下:
label := append([]string{getOperation(db), s.host, s.database, tableName, hasErr, sqlState}, metrics.InjectTagValue(collector.MetricsTitle, db.Statement.Context, attachment)...) elapsed := time.Since(s.StartTime).Seconds() collector.DurationReporter.Collector.(prometheus.ObserverVec).WithLabelValues(label...).Observe(elapsed)
看到這里我想大家就應(yīng)該知道了,go代碼會(huì)為prometheus創(chuàng)建一個(gè)5*500的緩沖池,來(lái)記錄數(shù)據(jù),prometheus會(huì)周期性的調(diào)用/mertic來(lái)拉取對(duì)應(yīng)的內(nèi)容。那么這里是怎么造成內(nèi)存泄露的呢?這里就要分析上述代碼的這個(gè)label了。
func (m *MetricVec) GetMetricWithLabelValues(lvs ...string) (Metric, error) { h, err := m.hashLabelValues(lvs) if err != nil { return nil, err } return m.metricMap.getOrCreateMetricWithLabelValues(h, lvs, m.curry), nil } func (m *MetricVec) hashLabelValues(vals []string) (uint64, error) { if err := validateLabelValues(vals, len(m.desc.variableLabels)-len(m.curry)); err != nil { return 0, err } var ( h = hashNew() curry = m.curry iVals, iCurry int ) for i := 0; i < len(m.desc.variableLabels); i++ { if iCurry < len(curry) && curry[iCurry].index == i { h = m.hashAdd(h, curry[iCurry].value) iCurry++ } else { h = m.hashAdd(h, vals[iVals]) iVals++ } h = m.hashAddByte(h, model.SeparatorByte) } return h, nil }
2.3 發(fā)現(xiàn)問(wèn)題(偽)
通過(guò)查看函數(shù)調(diào)用,我這邊發(fā)現(xiàn)label最終進(jìn)入的是這個(gè)hashLabelValues中,如果已存在就返回對(duì)應(yīng)的metricMap中的內(nèi)容,如果不一樣,則會(huì)創(chuàng)建一個(gè)新的緩沖池。內(nèi)存泄露就出在這個(gè)創(chuàng)建中。
這個(gè)時(shí)候我就在想,難道是我們label采集的數(shù)據(jù)太多了?通過(guò)排列組合,我估算了一下內(nèi)存最大值
getOperation(db)=4(操作類型,增刪改查4種)
s.host=1
s.database=3(我們有3個(gè)db實(shí)例)
tableName=30(表名,保守估計(jì)最少30個(gè))
hasErr, sqlState=2 (報(bào)錯(cuò)與沒(méi)報(bào)錯(cuò)2個(gè)狀態(tài))
metrics.InjectTagValue(collector.MetricsTitle, db.Statement.Context, attachment)…
這里面記錄的是請(qǐng)求的endpoint和startpoint,保守估計(jì)最少40個(gè)接口
這樣算下來(lái):4133024055008*3=1648mb。再加上程序本身的一些內(nèi)存開(kāi)銷,感覺(jué)和我們碰到的問(wèn)題能對(duì)上了。
2.4 解決問(wèn)題(偽)
于是一拍腦袋覺(jué)得發(fā)現(xiàn)了問(wèn)題,但是又無(wú)法解決問(wèn)題(抓的指標(biāo)無(wú)法修改)。于是屁顛屁顛的升了服務(wù)器配置,將4c2g升為了4c4g。
3.解決內(nèi)存泄漏
3.1 發(fā)現(xiàn)問(wèn)題(真)
沒(méi)錯(cuò),當(dāng)你看到這里的時(shí)候,就知道,升配這件事情并沒(méi)有結(jié)束。現(xiàn)實(shí)給了我一記響亮的耳光。
因?yàn)樯湟院罂傆X(jué)得還是哪里有問(wèn)題。于是還是每天都在不停的觀察RSS情況。結(jié)果,還真發(fā)現(xiàn)問(wèn)題了。因?yàn)閮?nèi)存還在坐火箭,這不科學(xué)啊。
當(dāng)我準(zhǔn)備繼續(xù)深入研究代碼的時(shí)候,我的一位同事提醒了我,你可以去看下/metrics具體上報(bào)了什么。說(shuō)時(shí)遲那時(shí)快。于是抓取了/metrics里的上報(bào)數(shù)據(jù),看到了以下數(shù)據(jù):
go_mysql_execute_count_total{command="SELECT",db="db_xxxxxx",endpoint="[DELETE]/url/:id",error="false",host="xxxxxx",main_table="table_xxxxxx",sql_state="0",startpoint="/url/49630" } 1 go_mysql_execute_count_total{command="SELECT",db="db_xxxxxx",endpoint="[DELETE]/url/:id",error="false",host="xxxxxx",main_table="table_xxxxxx",sql_state="0",startpoint="/url/49631" } 1 go_mysql_execute_count_total{command="SELECT",db="db_xxxxxx",endpoint="[DELETE]/url/:id",error="false",host="xxxxxx",main_table="table_xxxxxx",sql_state="0",startpoint="/url/49668" } 1 go_mysql_execute_count_total{command="SELECT",db="db_xxxxxx",endpoint="[DELETE]/url/:id",error="false",host="xxxxxx",main_table="table_xxxxxx",sql_state="0",startpoint="/url/49673" } 1
這不看不要緊,一看——原來(lái)startpoint里上報(bào)的是restful風(fēng)格的請(qǐng)求地址。那么上面的計(jì)算緩沖池的算法,就要再乘一個(gè)無(wú)限膨脹的startpoint。這給多少個(gè)G內(nèi)存也都不夠。
于是繼續(xù)查看代碼,看能不能關(guān)閉startpoint上報(bào)。這一查,果然有:
3.2 解決問(wèn)題(真)
看到這個(gè)設(shè)置START_POINT的環(huán)境變量,能關(guān)閉startpoint上報(bào)。于是立馬加到生產(chǎn)環(huán)境后重啟服務(wù)器。上線后觀察了一段時(shí)間,RSS使用量如下圖所示:
到此,此次內(nèi)存泄露問(wèn)題終于排查并修復(fù)完成。真是有驚無(wú)險(xiǎn)。
4.內(nèi)存泄露問(wèn)題總結(jié)
這邊大致歸納下go語(yǔ)言中有哪些常見(jiàn)的內(nèi)存泄露。
常見(jiàn)內(nèi)存泄露
4.1 Goroutine泄漏
goroutine泄露是開(kāi)發(fā)過(guò)程中碰到最常見(jiàn)、最頻繁的。一般經(jīng)常碰到的是以下幾種,由于網(wǎng)上相關(guān)的文章太多了,就不用代碼舉例了。
4.1.1 協(xié)程無(wú)法退出
鎖占用channel無(wú)法讀取或?qū)懭雲(yún)f(xié)程中邏輯有死循環(huán)
4.1.2 協(xié)程阻塞
協(xié)程業(yè)務(wù)邏輯時(shí)間長(zhǎng),釋放速度跟不上生成速度
4.1.3 內(nèi)存使用不當(dāng)
持續(xù)增長(zhǎng)的常駐協(xié)程,申請(qǐng)了大量?jī)?nèi)存空間,由于是常駐的協(xié)程,不會(huì)釋放內(nèi)存造成泄露
并發(fā)申請(qǐng)大量?jī)?nèi)存后,未達(dá)到GC時(shí)間或GC閾值,未觸發(fā)GC,導(dǎo)致內(nèi)存泄露
4.2 結(jié)構(gòu)使用不當(dāng)
結(jié)構(gòu)使用不當(dāng)也是開(kāi)發(fā)中常見(jiàn)的,只是可能并發(fā)不高,或者內(nèi)存泄露的不多,導(dǎo)致使用者容易忽視掉。
4.2.1 字符串、切片截取
func main() { var str0 = "1234567890" str1 := str0[:5] } func main() { var s0 = []int{1,2,3,4,5,6,7,8,9,0} s1 := s0[:5] }
上面兩段代碼,會(huì)有5個(gè)字節(jié)的泄露,因?yàn)樽址颓衅膬蓚€(gè)變量,底層是共享內(nèi)存的。只要str1或s1一直在用,str0和s0就不會(huì)回收。這樣剩下的5個(gè)字節(jié)或者5個(gè)int就會(huì)有臨時(shí)的泄露。這個(gè)場(chǎng)景,如果在高并發(fā),并且數(shù)據(jù)夠大的情況下,就算是臨時(shí)的泄露,也可能對(duì)性能有極大的影響。
4.2.2 指針類型
func main() { var prt0 = []*int{new(int), new(int), new(int), new(int), new(int)} ptr1 := prt0[:3] }
指針類型的這段代碼,其實(shí)和上面字符串、切片的例子很像,指針是指向內(nèi)存地址的。只要ptr1沒(méi)釋放,前面的指針數(shù)組中未被用的指針就不會(huì)釋放,從而導(dǎo)致臨時(shí)的內(nèi)存泄露。
4.2.3 數(shù)組傳參
func main() { var arr1 = [3]int{1,2,3} var arr2 = [3]int{} arr2 = arr1 fmt.Printf("array address :%p, array : %+v \n", &arr1, arr1) fmt.Printf("array address :%p, array : %+v \n", &arr2, arr2) test(arr1) } func test(arr [3]int) { fmt.Printf("array address :%p, array : %+v \n", &arr, arr) }
打印結(jié)果如下:
array address :0xc000122030, array : [1 2 3 4 5] array address :0xc000122060, array : [1 2 3 4 5] array address :0xc0001220f0, array : [1 2 3 4 5]
看結(jié)果可知,三條打印的地址各不相同,說(shuō)明數(shù)組是值傳遞的,那這會(huì)有什么問(wèn)題呢?畢竟我們很多代碼都是這么寫的。
問(wèn)題在于,只要傳遞的這個(gè)數(shù)組足夠大,那么調(diào)用一次就會(huì)生成一個(gè)一樣大小的新地址,這樣會(huì)消耗大量?jī)?nèi)存。如果短時(shí)間內(nèi)無(wú)法GC,會(huì)產(chǎn)生臨時(shí)的內(nèi)存泄露。這種泄露對(duì)于高并發(fā)是致命的。
4.2.4 定時(shí)器
func main() { chi := make(chan int) go func() { for { timer := time.After(10 * time.Second) select { case <-ch: fmt.Println("get it") case <-timer: fmt.Println("end") } } }() for i:= 1; i< 1000000; i++ { chi <- i time.sleep(time.Millisecond) } }
以上代碼,之所以會(huì)造成內(nèi)存泄露。是因?yàn)閠ime.After的底層是實(shí)現(xiàn)了一個(gè)timer,只要定時(shí)器未到時(shí)間,這個(gè)定時(shí)器就不會(huì)被gc回收,從而造成臨時(shí)的內(nèi)存泄露。如果這里的代碼沒(méi)寫好,定時(shí)器都是新創(chuàng)建的,那么就會(huì)造成永久性的泄露。
其實(shí)golang中的內(nèi)存泄露遠(yuǎn)不止上文提到的這些。有些可能甚至連查都查不到。這個(gè)時(shí)候還是要提醒大家,不僅要了解問(wèn)題,還要學(xué)會(huì)查找問(wèn)題。這樣不管遇到什么問(wèn)題,都能發(fā)現(xiàn)蛛絲馬跡,問(wèn)題也將迎刃而解。
原文地址:https://tech.dewu.com/article?id=11
到此這篇關(guān)于一鍵定位Golang線上服務(wù)內(nèi)存泄露的秘籍的文章就介紹到這了,更多相關(guān)Golang線上服務(wù)內(nèi)存泄露排查內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
goland中導(dǎo)包報(bào)紅和go mod問(wèn)題
這篇文章主要介紹了goland中導(dǎo)包報(bào)紅和go mod問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-03-03Go語(yǔ)言自帶測(cè)試庫(kù)testing使用教程
這篇文章主要為大家介紹了Go語(yǔ)言自帶測(cè)試庫(kù)testing使用教程,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07Go語(yǔ)言數(shù)據(jù)結(jié)構(gòu)之單鏈表的實(shí)例詳解
鏈表由一系列結(jié)點(diǎn)(鏈表中每一個(gè)元素稱為結(jié)點(diǎn))組成,結(jié)點(diǎn)可以在運(yùn)行時(shí)動(dòng)態(tài)生成。本文將通過(guò)五個(gè)例題帶大家深入了解Go語(yǔ)言中單鏈表的用法,感興趣的可以了解一下2022-08-08go語(yǔ)言優(yōu)雅地處理error工具及技巧詳解
這篇文章主要為大家介紹了go語(yǔ)言優(yōu)雅地處理error工具及技巧詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11Go語(yǔ)言模擬while語(yǔ)句實(shí)現(xiàn)無(wú)限循環(huán)的方法
這篇文章主要介紹了Go語(yǔ)言模擬while語(yǔ)句實(shí)現(xiàn)無(wú)限循環(huán)的方法,實(shí)例分析了for語(yǔ)句模擬while語(yǔ)句的技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-02-02Go語(yǔ)言中的Slice學(xué)習(xí)總結(jié)
這篇文章主要介紹了Go語(yǔ)言中的Slice學(xué)習(xí)總結(jié),本文講解了Slice的定義、Slice的長(zhǎng)度和容量、Slice是引用類型、Slice引用傳遞發(fā)生“意外”等內(nèi)容,需要的朋友可以參考下2014-11-117分鐘讀懂Go的臨時(shí)對(duì)象池pool以及其應(yīng)用場(chǎng)景
這篇文章主要給大家介紹了關(guān)于如何通過(guò)7分鐘讀懂Go的臨時(shí)對(duì)象池pool以及其應(yīng)用場(chǎng)景的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或使用Go具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起看看吧2018-11-11以Golang為例詳解AST抽象語(yǔ)法樹(shù)的原理與實(shí)現(xiàn)
AST?使用樹(shù)狀結(jié)構(gòu)來(lái)表達(dá)編程語(yǔ)言的結(jié)構(gòu),樹(shù)中的每一個(gè)節(jié)點(diǎn)都表示源碼中的一個(gè)結(jié)構(gòu),本文將以GO語(yǔ)言為例,為大家介紹一下AST抽象語(yǔ)法樹(shù)的原理與實(shí)現(xiàn),希望對(duì)大家有所幫助2024-01-01