go 第三方库源码解读---go-errorlint

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 程序默认可用的标识符(比如 intstring 这些基本类型)
      • 包作用域:每个包都有自己的作用域,包内定义的标识符在整个包内可见
      • 文件作用域:包中的每个文件有自己的文件作用域,文件作用域包含文件中的所有顶级声明(比如全局变量、函数等)
      • 函数作用域:函数中的标识符有自己的函数作用域,函数内定义的标识符在函数内部可见
      • 语句和函数字面量作用域:每个函数内部的语句块(比如 if 语句、for 循环等)或函数字面量(匿名函数)都有自己的作用域
    • 节点类型
      • *ast.File:文件节点,表示一个源文件
      • *ast.FuncType:函数类型节点,表示函数的签名
      • *ast.TypeSpec:类型声明节点,表示类型声明
      • *ast.BlockStmt:语句块节点,表示一组语句(通常是函数体或控制结构的语句块)
      • *ast.IfStmtif 语句节点
      • *ast.SwitchStmtswitch 语句节点
      • *ast.TypeSwitchStmt:类型 switch 语句节点
      • *ast.CaseClausecase 子句节点,通常出现在 switch 语句中
      • *ast.CommClausecomm 子句节点,出现在 select 语句中
      • *ast.ForStmtfor 循环节点
      • *ast.RangeStmtrange 语句节点,用于迭代数组、切片、map
  • Uses:标识符使用的地方的映射,比如:函数,变量
    • 用于记录标识符的使用情况,比如:

      go 复制代码
      x := 1          // x 不会被记录在 Uses 中
      fmt.Println(x)  // x 会被记录在 Uses 中
    • 结构体中的嵌入字段,会被记录在

      go 复制代码
      type 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 语句的断言有两种:SwitchStmtTypeSwitchStmt

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"
}
相关推荐
风象南22 分钟前
SpringBoot实现简易直播
java·spring boot·后端
这里有鱼汤30 分钟前
有人说10日低点买入法,赢率高达95%?我不信,于是亲自回测了下…
后端·python
武子康1 小时前
Java-39 深入浅出 Spring - AOP切面增强 核心概念 通知类型 XML+注解方式 附代码
xml·java·大数据·开发语言·后端·spring
米粉03051 小时前
SpringBoot核心注解详解及3.0与2.0版本深度对比
java·spring boot·后端
一只帆記2 小时前
SpringBoot EhCache 缓存
spring boot·后端·缓存
yuren_xia5 小时前
Spring Boot中保存前端上传的图片
前端·spring boot·后端
JohnYan8 小时前
Bun技术评估 - 04 HTTP Client
javascript·后端·bun
shangjg38 小时前
Kafka 的 ISR 机制深度解析:保障数据可靠性的核心防线
java·后端·kafka
青莳吖9 小时前
使用 SseEmitter 实现 Spring Boot 后端的流式传输和前端的数据接收
前端·spring boot·后端
我的golang之路果然有问题10 小时前
ElasticSearch+Gin+Gorm简单示例
大数据·开发语言·后端·elasticsearch·搜索引擎·golang·gin