Go語言自定義linter靜態(tài)檢查工具
前言
通常我們在業(yè)務(wù)項(xiàng)目中會(huì)借助使用靜態(tài)代碼檢查工具來保證代碼質(zhì)量,通過靜態(tài)代碼檢查工具我們可以提前發(fā)現(xiàn)一些問題,比如變量未定義、類型不匹配、變量作用域問題、數(shù)組下標(biāo)越界、內(nèi)存泄露等問題,工具會(huì)按照自己的規(guī)則進(jìn)行問題的嚴(yán)重等級(jí)劃分,給出不同的標(biāo)識(shí)和提示,靜態(tài)代碼檢查助我們盡早的發(fā)現(xiàn)問題,Go語言中常用的靜態(tài)代碼檢查工具有g(shù)olang-lint、golint,這些工具中已經(jīng)制定好了一些規(guī)則,雖然已經(jīng)可以滿足大多數(shù)場景,但是有些時(shí)候我們會(huì)遇到針對(duì)特殊場景來做一些定制化規(guī)則的需求,所以本文我們一起來學(xué)習(xí)一下如何自定義linter需求;
Go語言中的靜態(tài)檢查是如何實(shí)現(xiàn)?
眾所周知Go語言是一門編譯型語言,編譯型語言離不開詞法分析、語法分析、語義分析、優(yōu)化、編譯鏈接幾個(gè)階段,學(xué)過編譯原理的朋友對(duì)下面這個(gè)圖應(yīng)該很熟悉:

編譯器將高級(jí)語言翻譯成機(jī)器語言,會(huì)先對(duì)源代碼做詞法分析,詞法分析是將字符序列轉(zhuǎn)換為Token序列的過程,Token一般分為這幾類:關(guān)鍵字、標(biāo)識(shí)符、字面量(包含數(shù)字、字符串)、特殊符號(hào)(如加號(hào)、等號(hào)),生成Token序列后,需要進(jìn)行語法分析,進(jìn)一步處理后,生成一棵以 表達(dá)式為結(jié)點(diǎn)的 語法樹,這個(gè)語法樹就是我們常說的AST,在生成語法樹的過程就可以檢測一些形式上的錯(cuò)誤,比如括號(hào)缺少,語法分析完成后,就需要進(jìn)行語義分析,在這里檢查編譯期所有能檢查靜態(tài)語義,后面的過程就是中間代碼生成、目標(biāo)代碼生成與優(yōu)化、鏈接,這里就不詳細(xì)描述了,這里主要是想引出抽象語法樹(AST),我們的靜態(tài)代碼檢查工具就是通過分析抽象語法樹(AST)根據(jù)定制的規(guī)則來做的;那么抽象語法樹長什么樣子呢?我們可以使用標(biāo)準(zhǔn)庫提供的go/ast、go/parser、go/token包來打印出AST,
查看AST,具體AST長什么樣我們可以看下文的例子;
制定linter規(guī)則
假設(shè)我們現(xiàn)在要在我們團(tuán)隊(duì)制定這樣一個(gè)代碼規(guī)范,所有函數(shù)的第一個(gè)參數(shù)類型必須是Context,不符合該規(guī)范的我們要給出警告;好了,現(xiàn)在規(guī)則已經(jīng)定好了,現(xiàn)在我們就來想辦法實(shí)現(xiàn)它;先來一個(gè)有問題的示例:
// example.go
package main
func add(a, b int) int {
return a + b
}對(duì)應(yīng)AST如下:
*ast.FuncDecl {
8 . . . Name: *ast.Ident {
9 . . . . NamePos: 3:6
10 . . . . Name: "add"
11 . . . . Obj: *ast.Object {
12 . . . . . Kind: func
13 . . . . . Name: "add" // 函數(shù)名
14 . . . . . Decl: *(obj @ 7)
15 . . . . }
16 . . . }
17 . . . Type: *ast.FuncType {
18 . . . . Func: 3:1
19 . . . . Params: *ast.FieldList {
20 . . . . . Opening: 3:9
21 . . . . . List: []*ast.Field (len = 1) {
22 . . . . . . 0: *ast.Field {
23 . . . . . . . Names: []*ast.Ident (len = 2) {
24 . . . . . . . . 0: *ast.Ident {
25 . . . . . . . . . NamePos: 3:10
26 . . . . . . . . . Name: "a"
27 . . . . . . . . . Obj: *ast.Object {
28 . . . . . . . . . . Kind: var
29 . . . . . . . . . . Name: "a"
30 . . . . . . . . . . Decl: *(obj @ 22)
31 . . . . . . . . . }
32 . . . . . . . . }
33 . . . . . . . . 1: *ast.Ident {
34 . . . . . . . . . NamePos: 3:13
35 . . . . . . . . . Name: "b"
36 . . . . . . . . . Obj: *ast.Object {
37 . . . . . . . . . . Kind: var
38 . . . . . . . . . . Name: "b"
39 . . . . . . . . . . Decl: *(obj @ 22)
40 . . . . . . . . . }
41 . . . . . . . . }
42 . . . . . . . }
43 . . . . . . . Type: *ast.Ident {
44 . . . . . . . . NamePos: 3:15
45 . . . . . . . . Name: "int" // 參數(shù)名
46 . . . . . . . }
47 . . . . . . }
48 . . . . . }
49 . . . . . Closing: 3:18
50 . . . . }
51 . . . . Results: *ast.FieldList {
52 . . . . . Opening: -
53 . . . . . List: []*ast.Field (len = 1) {
54 . . . . . . 0: *ast.Field {
55 . . . . . . . Type: *ast.Ident {
56 . . . . . . . . NamePos: 3:20
57 . . . . . . . . Name: "int"
58 . . . . . . . }
59 . . . . . . }
60 . . . . . }
61 . . . . . Closing: -
62 . . . . }
63 . . . }方式一:標(biāo)準(zhǔn)庫實(shí)現(xiàn)custom linter
通過上面的AST結(jié)構(gòu)我們可以找到函數(shù)參數(shù)類型具體在哪個(gè)結(jié)構(gòu)上,因?yàn)槲覀兛梢愿鶕?jù)這個(gè)結(jié)構(gòu)寫出解析代碼如下:
package main
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"log"
"os"
)
func main() {
v := visitor{fset: token.NewFileSet()}
for _, filePath := range os.Args[1:] {
if filePath == "--" { // to be able to run this like "go run main.go -- input.go"
continue
}
f, err := parser.ParseFile(v.fset, filePath, nil, 0)
if err != nil {
log.Fatalf("Failed to parse file %s: %s", filePath, err)
}
ast.Walk(&v, f)
}
}
type visitor struct {
fset *token.FileSet
}
func (v *visitor) Visit(node ast.Node) ast.Visitor {
funcDecl, ok := node.(*ast.FuncDecl)
if !ok {
return v
}
params := funcDecl.Type.Params.List // get params
// list is equal of zero that don't need to checker.
if len(params) == 0 {
return v
}
firstParamType, ok := params[0].Type.(*ast.SelectorExpr)
if ok && firstParamType.Sel.Name == "Context" {
return v
}
fmt.Printf("%s: %s function first params should be Context\n",
v.fset.Position(node.Pos()), funcDecl.Name.Name)
return v
}然后執(zhí)行命令如下:
$ go run ./main.go -- ./example.go ./example.go:3:1: add function first params should be Context
通過輸出我們可以看到,函數(shù)add()第一個(gè)參數(shù)必須是Context;這就是一個(gè)簡單實(shí)現(xiàn),因?yàn)锳ST的結(jié)構(gòu)實(shí)在是有點(diǎn)復(fù)雜,就不在這里詳細(xì)介紹每個(gè)結(jié)構(gòu)體了,可以看曹大之前寫的一篇文章:golang
和 ast
方式二:go/analysis
看過上面代碼的朋友肯定有點(diǎn)抓狂了,有很多實(shí)體存在,要開發(fā)一個(gè)linter,我們需要搞懂好多實(shí)體,好在go/analysis進(jìn)行了封裝,go/analysis為linter
提供了統(tǒng)一的接口,它簡化了與IDE,metalinters,代碼Review等工具的集成。如,任何go/analysislinter都可以高效的被go
vet執(zhí)行,下面我們通過代碼方式來介紹go/analysis的優(yōu)勢;
新建一個(gè)項(xiàng)目代碼結(jié)構(gòu)如下:
.
├── firstparamcontext
│ └── firstparamcontext.go
├── go.mod
├── go.sum
└── testfirstparamcontext
├── example.go
└── main.go添加檢查模塊代碼,在firstparamcontext.go添加如下代碼:
package firstparamcontext
import (
"go/ast"
"golang.org/x/tools/go/analysis"
)
var Analyzer = &analysis.Analyzer{
Name: "firstparamcontext",
Doc: "Checks that functions first param type is Context",
Run: run,
}
func run(pass *analysis.Pass) (interface{}, error) {
inspect := func(node ast.Node) bool {
funcDecl, ok := node.(*ast.FuncDecl)
if !ok {
return true
}
params := funcDecl.Type.Params.List // get params
// list is equal of zero that don't need to checker.
if len(params) == 0 {
return true
}
firstParamType, ok := params[0].Type.(*ast.SelectorExpr)
if ok && firstParamType.Sel.Name == "Context" {
return true
}
pass.Reportf(node.Pos(), "''%s' function first params should be Context\n",
funcDecl.Name.Name)
return true
}
for _, f := range pass.Files {
ast.Inspect(f, inspect)
}
return nil, nil
}然后添加分析器:
package main
import (
"asong.cloud/Golang_Dream/code_demo/custom_linter/firstparamcontext"
"golang.org/x/tools/go/analysis/singlechecker"
)
func main() {
singlechecker.Main(firstparamcontext.Analyzer)
}命令行執(zhí)行如下:
$ go run ./main.go -- ./example.go /Users/go/src/asong.cloud/Golang_Dream/code_demo/custom_linter/testfirstparamcontext/example.go:3:1: ''add' function first params should be Context
如果我們想添加更多的規(guī)則,使用golang.org/x/tools/go/analysis/multichecker追加即可。
集成到golang-cli
我們可以把golang-cli的代碼下載到本地,然后在pkg/golinters 下添加firstparamcontext.go,
代碼如下:
import (
"golang.org/x/tools/go/analysis"
"github.com/golangci/golangci-lint/pkg/golinters/goanalysis"
"github.com/fisrtparamcontext"
)
func NewfirstparamcontextCheck() *goanalysis.Linter {
return goanalysis.NewLinter(
"firstparamcontext",
"Checks that functions first param type is Context",
[]*analysis.Analyzer{firstparamcontext.Analyzer},
nil,
).WithLoadMode(goanalysis.LoadModeSyntax)
}然后重新make一個(gè)golang-cli可執(zhí)行文件,加到我們的項(xiàng)目中就可以了;
到此這篇關(guān)于Go語言自定義linter靜態(tài)檢查工具的文章就介紹到這了,更多相關(guān)Go自定義linter內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Go語言學(xué)習(xí)網(wǎng)絡(luò)編程與Http教程示例
這篇文章主要為大家介紹了Go語言學(xué)習(xí)網(wǎng)絡(luò)編程與Http教程示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03
Go語言使用HTTP包創(chuàng)建WEB服務(wù)器的方法
這篇文章主要介紹了Go語言使用HTTP包創(chuàng)建WEB服務(wù)器的方法,結(jié)合實(shí)例形式分析了Go語言基于HTTP包創(chuàng)建WEB服務(wù)器客戶端與服務(wù)器端的實(shí)現(xiàn)方法與相關(guān)注意事項(xiàng),需要的朋友可以參考下2016-07-07
Gorm存在時(shí)更新,不存在時(shí)創(chuàng)建的問題
這篇文章主要介紹了Gorm存在時(shí)更新,不存在時(shí)創(chuàng)建的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-08-08
golang并發(fā)之使用sync.Pool優(yōu)化性能
在Go提供如何實(shí)現(xiàn)對(duì)象的緩存池功能,常用一種實(shí)現(xiàn)方式是sync.Pool,?其旨在緩存已分配但未使用的項(xiàng)目以供以后重用,從而減輕垃圾收集器(GC)的壓力,下面我們就來看看具體操作吧2023-10-10
Go語言時(shí)間相關(guān)操作合集(超詳細(xì))
在開發(fā)應(yīng)用程序的過程中,經(jīng)常需要記錄某些操作的時(shí)間或者格式化時(shí)間戳,因此大部分編程語言都會(huì)有操作時(shí)間的庫,Go語言當(dāng)然也不例外,本文我們就一起來學(xué)習(xí)一下time包的使用2023-08-08
Go語言實(shí)現(xiàn)websocket推送程序
這篇文章主要介紹了Go語言實(shí)現(xiàn)websocket推送程序,WebSocket是基于TCP的一個(gè)雙向傳輸數(shù)據(jù)的協(xié)議,和HTTP協(xié)議一樣,是在應(yīng)用層的,他的出現(xiàn),是為了解決網(wǎng)頁進(jìn)行持久雙向傳輸數(shù)據(jù)的問題2023-01-01

