<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          深入Go代碼覆蓋率使用、場景與原理

          共 3537字,需瀏覽 8分鐘

           ·

          2022-05-11 01:59

          一般我們會使用代碼覆蓋率來判斷代碼書寫的質(zhì)量,識別無效代碼。

          識別靜態(tài)靜態(tài)的代碼

          對于靜態(tài)的代碼,要識別代碼沒有被使用,可以使用golangci-lint工具

          golangci-lint  run --disable-all  --enable unused

          對于通過單元測試 測試函數(shù)代碼的覆蓋率,在go生態(tài)中,go1.2提供了cover工具。

          cover 基本用法

          首先來看看go test -cover 統(tǒng)計代碼覆蓋率做的事情,借鑒一下它的思路。go test -cover 能夠統(tǒng)計出代碼的覆蓋率,這是一種比統(tǒng)計函數(shù)是否被調(diào)用更強悍的手法。

          % go test -cover
          PASS
          coverage: 42.9% of statements
          ok size 0.026s
          %

          另外,還可以收集覆蓋率進行,并進行可視化的展示。

          go test -coverprofile=coverage.out

          如下,使用 go tool cover 可視化分析代碼覆蓋率信息。

          $ go tool cover -html=coverage.out

          能夠識別和統(tǒng)計出未調(diào)用過的分支。

          測試環(huán)境下代碼覆蓋率

          go test -cover 本身是運行xxx_test.go文件的測試函數(shù)使用的,但是我們可以使用一些奇淫技巧將這種方式用于測試環(huán)境下的代碼覆蓋率測試。假設(shè)我們的代碼是一個web服務(wù)器,我們可以新建一個main_test.go文件,執(zhí)行main()函數(shù),就好像在執(zhí)行程序一樣。并在退出程序后,正常完成coverprofile文件的生成。這種好處是可以在測試環(huán)境調(diào)用http請求,并最終統(tǒng)計代碼覆蓋率。如下為一段main_test.go代碼示例


          func TestSystem(t *testing.T) {
          handleSignals()

          endRunning = make(chan bool, 1)

          go func() {

          main()

          }()

          <-endRunning
          }

          線上代碼覆蓋率的思路

          假設(shè)我們的需求是想看線上的代碼哪一些函數(shù)沒有被調(diào)用?

          運行中的程序要檢測某一個函數(shù)是否被調(diào)用似乎沒有什么好的辦法,原因是沒有好的手段能夠檢測并記錄當前函數(shù)已經(jīng)被調(diào)用了。

          可以將線上的代碼覆蓋率分為傾入性和非傾入性兩種方式。

          傾入性方案一種最直接的方式是為代碼打樁,在每一個函數(shù)開頭埋點記錄下調(diào)用信息。我們可以參考下go test -cover

          的實現(xiàn)方案。

          cover代碼覆蓋率的原理

          go test -cover 會對代碼進行打樁。

          package size

          func Size(a int) string {
          switch {
          case a < 0:
          return "negative"
          case a == 0:
          return "zero"
          case a < 10:
          return "small"
          case a < 100:
          return "big"
          case a < 1000:
          return "huge"
          }
          return "enormous"
          }

          如上代碼可以通過下面的命令生成打樁后的結(jié)果

          go tool cover -mode=set  -var=size  xxx.go

          打樁后的結(jié)果如下:

          //line xxx.go:1
          package tt

          func Size(a int) string {size.Count[0] = 1;
          switch {
          case a < 0:size.Count[2] = 1;
          return "negative"
          case a == 0:size.Count[3] = 1;
          return "zero"
          case a < 10:size.Count[4] = 1;
          return "small"
          case a < 100:size.Count[5] = 1;
          return "big"
          case a < 1000:size.Count[6] = 1;
          return "huge"
          }
          size.Count[1] = 1;return "enormous"
          }

          var size = struct {
          Count [7]uint32
          Pos [3 * 7]uint32
          NumStmt [7]uint16
          } {
          Pos: [3 * 7]uint32{
          3, 4, 0x90019, // [0]
          16, 16, 0x130002, // [1]
          5, 6, 0x14000d, // [2]
          7, 8, 0x10000e, // [3]
          9, 10, 0x11000e, // [4]
          11, 12, 0xf000f, // [5]
          13, 14, 0x100010, // [6]
          },
          NumStmt: [7]uint16{
          1, // 0
          1, // 1
          1, // 2
          1, // 3
          1, // 4
          1, // 5
          1, // 6
          },
          }

          而要實現(xiàn)go test -coverprofile=coverage.out打樁略微復雜一些,改命令會生成一個_testmain.go的中間文件,將文件名信息以及花括號的信息記錄并注冊,并最終生成coverprofile協(xié)議文件。具體的執(zhí)行過程可以通過如下命令查看。

          go test -n -x  -test.coverprofile=coverage.out

          coverprofile文件的協(xié)議遵循如下格式:

          第一行為"mode: foo", foo is "set", "count", or "atomic".
          中間為記錄的位置:name.go:line.column,line.column numberOfStatements count
          // 例如:encoding/base64/base64.go:34.44,37.40 3 1

          具體協(xié)議可以參考:go1.16.14/src/cmd/cover/profile.go:44

          coverprofile文件協(xié)議需要的信息可以通過go test -cover自動生成的代碼進行整理。在這個地方,可以參考開源項目https://github.com/qiniu/goc 的實現(xiàn)邏輯,goc 本身就是對go test -cover 命令的封裝。

          借助如下命令得到的在臨時目錄生成的文件,可以查看到如何將go test -cover對應的結(jié)構(gòu)轉(zhuǎn)換為coverprofile 文件的協(xié)議。

          goc build --debug

          cover 打樁原理:AST語法樹打樁

          go test -cover 打樁的原理是借助AST語法樹遍歷整個文件,在識別到花括號、swith、select等地方,插入一行。

          // go1.16.14/src/cmd/cover/cover.go:202
          func (f *File) Visit(node ast.Node) ast.Visitor {
          switch n := node.(type) {
          case *ast.BlockStmt:
          // If it's a switch or select, the body is a list of case clauses; don't tag the block itself.
          if len(n.List) > 0 {
          switch n.List[0].(type) {
          case *ast.CaseClause: // switch
          for _, n := range n.List {
          clause := n.(*ast.CaseClause)
          f.addCounters(clause.Colon+1, clause.Colon+1, clause.End(), clause.Body, false)
          }
          return f
          case *ast.CommClause: // select
          for _, n := range n.List {
          clause := n.(*ast.CommClause)
          f.addCounters(clause.Colon+1, clause.Colon+1, clause.End(), clause.Body, false)
          }
          return f
          }
          }
          f.addCounters(n.Lbrace, n.Lbrace+1, n.Rbrace+1, n.List, true) // +1 to step past closing brace.
          ...

          借助于操作AST語法樹這種思路,我們也可以做相應改造,完成在代碼特定位置插入一行用于統(tǒng)計,等其他功能。

          如果只是統(tǒng)計函數(shù)是否被調(diào)用,那么可以借助AST語法樹,在識別到函數(shù)的下一行,插入一行統(tǒng)一的函數(shù)調(diào)用,全局Record函數(shù)可以是一個map,用于全局函數(shù)的統(tǒng)計。參考資料中給出了操作AST的2篇文章。

          func A(){
          Record("A")
          ...
          }

          腳本

          如果我們只是想實現(xiàn)簡單的功能,例如在函數(shù)下方插入一行,也許我們可以用時間成本最小的方式來處理。如下的sed命令,直接在文件中函數(shù)下方插入一行,這種方式最快。

          sed -i 's/.*\(func \([a-z].*\)()\).*/\1\n Record \2/g' xxx.go

          其適用于大部分函數(shù)場景:但是上面的這種方法不適用于有換行的函數(shù),如下統(tǒng)計個數(shù)。

          find . -type f -name '*.go' -not -path "./vendor/*" | xargs ggrep -oP "func \w*\(.*?,$" | wc -l

          非傾入性方案-pprof

          上面的方式比較直接,但是代碼有侵入性容易出錯。不想有傾入性可以借助于中斷信號處理的方式。

          Go pprof CPU 分析器使用 定時發(fā)送SIGPROF 信號中斷代碼執(zhí)行。當調(diào)用 pprof.StartCPUProfile 函數(shù)時,SIGPROF 信號處理程序?qū)⒈蛔詾槟J每 10 毫秒間隔調(diào)用一次(100 Hz)。在 Unix 上,它使用 setitimer(2) 系統(tǒng)調(diào)用來設(shè)置信號計時器。當內(nèi)核態(tài)返回到用戶態(tài)調(diào)用注冊好的sighandler 函數(shù),sighandler 函數(shù)識別到信號為_SIGPROF 時,執(zhí)行sigprof 函數(shù)記錄該CPU樣本,并以此機會獲取當前代碼的棧幀數(shù)據(jù)。

          這些堆棧信息可以合并為profile文件并最終被pprof程序分析處理。

          這種方式記錄了每一個堆棧樣本

          并且通過合并統(tǒng)計,借助概率的方式,能夠知道程序大部分時間在哪個函數(shù)運行。

          但是如果要統(tǒng)計哪一個函數(shù)沒有被調(diào)用卻比較困難,原因是如果某一個A函數(shù)執(zhí)行的時間很短,ns級別的,那么當觸發(fā)定時任務(wù)時,有多大的概率能夠在該A函數(shù)停留?這種不確定性增加了識別不可用函數(shù)或者代碼的難度。

          由于頻繁的中斷,pprof 不能一直打開,一般是抽樣60ms以下的數(shù)據(jù)。一種思路是定時抽樣并合并profile數(shù)據(jù)。

          curl -o cpu.out  http://localhost:9981/debug/pprof/profile?seconds=30 
          go tool pprof -http=localhost:8000 cpu.out

          總結(jié)

          代碼覆蓋率是判斷代碼書寫的質(zhì)量,識別無效代碼的重要工具。在go生態(tài)中,go1.2提供了測試代碼覆蓋率的cover工具。

          • 靜態(tài)代碼

          對于靜態(tài)的代碼,要識別代碼沒有被使用,可以使用golangci-lint工具

          golangci-lint  run --disable-all  --enable unused
          • 對于線下的單元測試

          可以使用go test -cover工具

          • 測試環(huán)境下的代碼

          對于測試環(huán)境下有請求或長時間運行程序的單元覆蓋率,可以借助cover工具使用文中巧妙的方式來測試。如果測試環(huán)境不充分完備,沒有辦法測試出來。線上統(tǒng)計函數(shù)是否被調(diào)用有兩種方式傾入性和非傾入性。

          • 對于線上傾入性的方式

          主要是在關(guān)鍵位置注入一行函數(shù)進行統(tǒng)計??梢允呛瘮?shù)級別的,甚至可以借助go test -cover 在每一個邏輯分支注入了函數(shù)。

          如果只是考慮函數(shù)級別的,可以考慮直接shel腳本注入函數(shù),這樣做時間成本最低。也可以考慮借助AST抽象語法樹的方式,成本略高。

          如果是邏輯分支級別的,可以考慮借鑒開源庫https://github.com/qiniu/goc 的手法來生成打樁后的go文件。它是對go test -cover 代碼的封裝,借助AST抽象語法樹,在特定位置插入一行。

          • 對于線上非傾入性的方式

          通過pprof,信號中斷處理的手法,抽樣獲取堆棧信息。一些處理時間很短的函數(shù),將很難被檢測到。這種方式對于檢測到經(jīng)常使用的函數(shù)有用,但是不適合推斷沒有使用過的函數(shù)。

          參考資料

          官方文檔:https://go.dev/blog/cover

          合并profile:https://github.com/rakyll/pprof-merge

          七牛云goc:https://github.com/qiniu/goc

          利用 go/ast 語法樹做代碼生成: https://segmentfault.com/a/1190000039215176

          Golang AST語法樹使用教程及示例:https://juejin.cn/post/6844903982683389960

          hook思路:https://github.com/brahma-adshonor/gohook



          推薦閱讀


          福利

          我為大家整理了一份從入門到進階的Go學習資料禮包,包含學習建議:入門看什么,進階看什么。關(guān)注公眾號 「polarisxu」,回復?ebook?獲??;還可以回復「進群」,和數(shù)萬 Gopher 交流學習。

          瀏覽 408
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  久久免费福利视频 | 国产27区 | 最新中文字幕av 67194国产 | 操日本逼 | 三级片网站欧美 |