analysis.Pass
中有一个 TypesInfo
字段,这个字段的保存这语法类型相关的信息
analysis.Pass.TypesInfo
:
Types
:把每个合法表达式与其类型关联起来,比如:变量、函数调用、操作符等- 表达式如果是一个常量,不仅会记录类型,还会记录值
- 表达式无效,会被忽略
- 如果标识符(如
len
)表示内建函数,那么Types
记录的函数签名(也就是参数和返回值类型)是基于你怎么调用它的。例如,len([]int{1, 2, 3})
会根据参数类型[]int
,推导出返回值是int
- 如果调用的结果是一个常量(比如
len([3]int{1, 2, 3})
的结果就是3
,它是一个常量),那么Types
记录的类型将不再是该函数的签名,而是一个无效的类型,因为调用的结果是固定的,不再依赖参数 - 选择器表达式
(x.f)
,f
的类型不是通过Types
记录的,而是在Selections
中 - 变量声明
(var z int)
,变量z
的类型不会记录在Types
中,而是在Defs
中 - 限定标识符中的包
(pkg.SomeType)
:如果标识符表示的是一个包(比如pkg
),它的类型信息会记录在Uses
中
Scopes
:是一个将抽象语法树的节点与作用域关联的映射- 作用域树
Universe
(全局作用域):这是最外层的作用域,包含所有Go
程序默认可用的标识符(比如int
、string
这些基本类型)- 包作用域:每个包都有自己的作用域,包内定义的标识符在整个包内可见
- 文件作用域:包中的每个文件有自己的文件作用域,文件作用域包含文件中的所有顶级声明(比如全局变量、函数等)
- 函数作用域:函数中的标识符有自己的函数作用域,函数内定义的标识符在函数内部可见
- 语句和函数字面量作用域:每个函数内部的语句块(比如
if
语句、for
循环等)或函数字面量(匿名函数)都有自己的作用域
- 节点类型
*ast.File
:文件节点,表示一个源文件*ast.FuncType
:函数类型节点,表示函数的签名*ast.TypeSpec
:类型声明节点,表示类型声明*ast.BlockStmt
:语句块节点,表示一组语句(通常是函数体或控制结构的语句块)*ast.IfStmt
:if
语句节点*ast.SwitchStmt
:switch
语句节点*ast.TypeSwitchStmt
:类型switch
语句节点*ast.CaseClause
:case
子句节点,通常出现在switch
语句中*ast.CommClause
:comm
子句节点,出现在select
语句中*ast.ForStmt
:for
循环节点*ast.RangeStmt
:range
语句节点,用于迭代数组、切片、map
等
- 作用域树
Uses
:标识符使用的地方的映射,比如:函数,变量-
用于记录标识符的使用情况,比如:
gox := 1 // x 不会被记录在 Uses 中 fmt.Println(x) // x 会被记录在 Uses 中
-
结构体中的嵌入字段,会被记录在
gotype A struct { B // B 会被记录在 Uses 中 }
-
id.Pos()
会返回标识符的位置,Uses[id].Pos()
会返回标识符的使用位置,这两个位置可能不一样
-
作用域
在 go
语言中,如何知道作用域的范围?
比如说下面的代码有几个作用域?
go
func Func() {}
你会说这有一个函数作用域
那在看下面的代码,有几个作用域?
go
if true {
fmt.Println(1)
}
你可能会说这有一个块级作用域
实际上在 go
中,上面这段代码还有一个作用域
叫做 if
作用域
go
if true { // if 作用域
fmt.Println(1) // if 语句作用域
}
go
中有一个包 golang.org/x/tools/go/analysis
,可以对 go
代码进行静态分析
核心代码:
go
func NewAnalyzer() *analysis.Analyzer {
return &analysis.Analyzer{
Name: "scope",
Doc: "test scope",
Run: run,
}
}
func run(pass *analysis.Pass) (interface{}, error) {
fmt.Println("scopes len: ", len(pass.TypesInfo.Scopes))
}
准备一个测试用例:
go
func TestErrorsAs(t *testing.T) {
analysistest.Run(t, analysistest.TestData(), analysis.NewAnalyzer(), "errorsas")
}
数据放在 testdata
目录下,其结构为:
markdown
- testdata
- src
- errorsas
- errorsas.go
我们的内容就放在 errorsas.go
文件中
go
package errorsas
import "fmt"
func TypeCheckGood1() {
if true {
fmt.Println(1)
}
}
比如说上面的代码内容,输出的作用域个数为 4
go
package errorsas // 包作用域
import "fmt"
func TypeCheckGood1() { // 函数作用域
if true { // if 作用域
fmt.Println(1) // if 语句作用域
}
}
if
语句的 else
分支和 else if
分支作用域是不一样的
go
if a { // if 作用域
fmt.Println(1) // if 语句作用域
} else if b{ // else if 作用域
fmt.Println(2) // else if 语句作用域
} else {
fmt.Println(3) // else 语句作用域
}
for
语句也有作用域
go
for i := 0; i < 10; i++ { // for 作用域
fmt.Println(i) // for 语句作用域
}
for _, v := range []int{1, 2, 3} { // for 作用域
fmt.Println(v) // for 语句作用域
}
switch
作用域
go
switch v { // switch 作用域
case 1: // case 作用域
fmt.Println(1)
case 2: // case 作用域
fmt.Println(2)
default: // default 作用域
fmt.Println(3)
}
select
作用域
go
select { // select 本身没有作用域
case <-ch: // case 作用域
fmt.Println(1)
}
nil 值断言
nil
是一个预声明的标识符,用于表示指针、切片、映射、通道、接口和函数的零值
判断 nil
的方式:
go
binExpr, ok := expr.(*ast.BinaryExpr)
isNil(binExpr.X)
func isNil(ex ast.Expr) bool {
ident, ok := ex.(*ast.Ident)
return ok && ident.Name == "nil"
}
nil 类型判断
go
switchStmt, ok := scope.(*ast.SwitchStmt)
isErrorType(info.TypesInfo, switchStmt.Tag)
func isErrorType(info *types.Info, ex ast.Expr) bool {
t := info.Types[ex].Type
return t != nil && t.String() == "error"
}
这两个 nil
判断的区别是:
go
var err error = nil // isErrorType=true, isNil=false
nil // isErrorType=false, isNil=true
switch 断言
switch
语句的断言有两种:SwitchStmt
和 TypeSwitchStmt
go
switchStmt, _ := scope.(*ast.SwitchStmt)
typeSwitch, _ := scope.(*ast.TypeSwitchStmt)
他们两个的区别是,SwitchStmt
是对值进行判断,TypeSwitchStmt
是对类型进行判断
什么意思呢?
也就是说 SwitchStmt
是对下面这种进行判断
go
switch x := 5; x {
case 1:
fmt.Println("one")
case 2:
fmt.Println("two")
default:
fmt.Println("other")
}
TypeSwitchStmt
是对下面这种进行判断
go
var i interface{} = "hello"
switch v := i.(type) {
case int:
fmt.Printf("Integer: %v\n", v)
case string:
fmt.Printf("String: %v\n", v)
default:
fmt.Printf("Unknown type\n")
}
SwitchStmt 的结构
go
SwitchStmt struct {
Switch token.Pos // switch 关键字的位置
Init Stmt // switch 语句前的初始化语句,比如:switch x := 5; x > 0 中的 x := 5 或者为 nil
Tag Expr // switch 判断的表达式,比如这里的 x > 0 或者为 nil
Body *BlockStmt // 包含所有 case 语句的代码块
}
BlockStmt struct {
Lbrace token.Pos // 左括号的位置
List []Stmt // 存放所有语句的切片,在 switch 中就是存放所有的 case 语句
Rbrace token.Pos // 右括号的位置
}
TypeSwitchStmt 的结构
go
TypeSwitchStmt struct {
Switch token.Pos // switch 关键字的位置
Init Stmt // switch 语句前的初始化语句,比如 switch y := 0; x := i.(type) 中的 y := 0
Assign Stmt // switch 判断的表达式,比如这里的 x := i.(type)
Body *BlockStmt // 包含所有 case 语句的代码块
}
CaseClause 的结构
go
CaseClause struct {
Case token.Pos // case 或者 defalt 关键字位置
List []Expr // case 语句后面的表达式,比如 case 1, 2, 3: 中的 1, 2, 3,如果是 default 语句,这里为 nil
Colon token.Pos // 冒号的位置
Body []Stmt // 包含所有 case 语句的代码块或者为 nil
}
CallExpr 的结构
CallExpr
表示一个函数调用解析后的结构
go
CallExpr struct {
Fun Expr // 函数调用的函数名,比如:fmt.Println
Lparen token.Pos // 左括号的位置
Args []Expr // 函数调用的参数列表,比如:fmt.Println(1, 2, 3) 中的 1, 2, 3 或者为 nil
Ellipsis token.Pos // 省略号的位置,如果参数是个可变参数,这里会有值,比如:fmt.Println(1, 2, 3...) 中的 ...,否则为 token.NoPos
Rparen token.Pos // 右括号的位置
}
BasicLit 的结构
BasicLit
表示一个基本的字面量结构
go
BasicLit struct {
ValuePos token.Pos // 字面量的位置
Kind token.Token // 字面量的类型,比如:token.INT, token.FLOAT, token.IMAG, token.CHAR, token.STRING
Value string // 字面量的值,比如:42, 3.14, 2.4i, 'a', "foo",以字符串的形式保存
}
NewMethodSet 方法
NewMethodSet
方法用于创建一个类型的方法集合
go
mset := types.NewMethodSet(t)
意思就是可以知道一个类型的所有方法,比如下面这种:
go
type A struct{}
func (a A) Method1() {}
func (*a A) Method2() {}
ms := types.NewMethodSet(A{}) // 只包含 Method1
msPtr := types.NewMethodSet(&A{}) // 包含 Method1 和 Method2
NewMethodSet
方法主要返回的是一个 MethodSet
结构,MethodSet
结构主要包含了一个 Selection
切片,Selection
结构如下:
go
type Selection struct {
kind SelectionKind // 选择器类型(字段访问/方法调用/方法表达式)
recv Type // 接收者类型
obj Object // 表示的具体对象(字段或方法)
index []int // 从 x 到 x.f 的索引
indirect bool // 如果是指针,设置为 true,需要间接寻址
}
Selection
有三种类型:
FieldVal
:字段访问MethodVal
:方法调用MethodExpr
:方法表达式
比如:
go
type T struct {
x int
E // 嵌入的 E 类型
}
type E struct{}
func (e E) m() {}
var p *T
p.x
:字段访问 (FieldVal
)
go
Selection{
kind: FieldVal,
recv: T, // 接收者类型是 T
obj: x, // x 字段
index: []int{0}, // x 是 T 的第一个字段
indirect: true // 因为 p 是指针,需要间接访问
}
p.m
:方法调用 (MethodVal
)
go
Selection{
kind: MethodVal,
recv: *T, // 接收者类型是 *T
obj: m, // m 方法
index: []int{1,0}, // 通过嵌入的 E(索引1)访问其方法 m(索引0)
indirect: true // 通过指针访问
}
T.m
:方法表达式 (MethodExpr
)
go
Selection{
kind: MethodExpr,
recv: T, // 接收者类型是 T
obj: m, // m 方法
index: []int{1,0}, // 同样通过 E 访问 m
indirect: false // 直接通过类型访问,不需要指针
}
错误修复
下面两个格式化方法的区别是啥?
go
func Good() error {
err := errors.New("oops")
return fmt.Errorf("error: %w", err)
}
func NonWrappingVerb() error {
err := errors.New("oops")
return fmt.Errorf("error: %v", err) // want "non-wrapping format verb for fmt.Errorf. Use `%w` to format errors"
}
%w
:保留原始错误的堆栈信息- 会保留原始错误
err
的完整上下文信息 - 允许使用
errors.Unwrap()
来获取原始错误 - 支持
errors.Is()
和errors.As()
进行错误类型检查
- 会保留原始错误
%v
:不保留原始错误的堆栈信息%v
只是简单地将错误转换为字符串- 丢失了原始错误的上下文和类型信息
- 无法通过
errors.Unwrap()
获取原始错误 - 不支持使用
errors.Is()
和errors.As()
在使用 lint.SuggestedFixes
时修复错误时,需要提供一个 xxx.go.gloden
文件
比如 fmterrorf.go.golden
文件内容如下:
go
func NonWrappingVerb() error {
err := errors.New("oops")
return fmt.Errorf("error: %w", err) // want "non-wrapping format verb for fmt.Errorf. Use `%w` to format errors"
}
对应的 fmterrorf.go
文件内容如下:
go
func NonWrappingVerb() error {
err := errors.New("oops")
return fmt.Errorf("error: %v", err) // want "non-wrapping format verb for fmt.Errorf. Use `%w` to format errors"
}
单个和多个错误
在一个格式化字符串 fmt.Errorf
中只能使用一个 %w
动词
在 Single()
函数,意味着只有 err1
可以通过 errors.Unwrap()
获取到
在 Multiple()
函数,会产生编译错误
go
func Single() error {
err1 := errors.New("oops1")
err2 := errors.New("oops2")
err3 := errors.New("oops3")
return fmt.Errorf("%w, %v, %v", err1, err2, err3)
}
func Multiple() error {
err1 := errors.New("oops1")
err2 := errors.New("oops2")
err3 := errors.New("oops3")
return fmt.Errorf("%w, %w, %w", err1, err2, err3) // want "only one %w verb is permitted per format string"
}