golangci-lint问题优化

golangci-lint的使用介绍一文中我们提到了linter常见的几种配置,例如,gocyclo(圈复杂度),dupl(重复代码)等。本文主要介绍这几种常见linter的问题如何优化。

1. gocyclo(圈复杂度)

在 Go 中,圈复杂度(Cyclomatic Complexity) 是一种衡量代码复杂度的指标,表示代码中可能的独立执行路径的数量。它反映了程序的逻辑复杂度,也就是代码有多少个分支或决策点(如 ifforswitch 等)。圈复杂度越高,代码的理解和维护成本就越大。

.golangci.yml中圈复杂度的配置如下:

linters:
	enabel:
	  - gocycle

linters-settings:
  gocyclo:
    # Minimal code complexity to report.
    # Default: 30 (but we recommend 10-20)
    min-complexity: 10

1.1. 计算规则

圈复杂度的计算公式为:

M=E−N+2P

其中:

  • E:代码中的边数(控制流图中的边,表示程序中的控制流路径,例如从一条语句跳转到下一条语句)。
  • N:代码中的节点数(控制流图中的节点,表示程序中的基本块或语句)。
  • P:控制流图中连通部分的数量(通常为 1)。

简单来说,圈复杂度可以通过统计代码中的以下决策点来估算,其中初始值为1,即默认返回路径,再根据以下规则累加

  • 每个 ifelse if 增加 1。
  • 每个 forwhile 循环增加 1。
  • 每个 case(不包括 default)增加 1。
  • 每个 &&|| 表达式增加 1。

举例说明:

func Example(a, b int) int {
    if a > 0 {  // if语句 +1
        if b > 0 {  // if语句 +1
            return a + b
        } else {
            return a - b
        }
    }
    return 0   初始值为1 
}

控制流路径为:

  • 初始值为1
  • 2个if语句 + 2

因此该函数的圈复杂度为3。

1.2. 常见标准

通常,圈复杂度以合理的范围为佳(例如:10-15),过低的圈复杂度会导致拆分函数过多,代码可读性变差

以下是圈复杂度的参考标准:

  • 1-10:代码逻辑简单,可维护性高。
  • 11-20:代码逻辑较复杂,可能需要重构。
  • 21+:代码逻辑非常复杂,维护成本高,建议拆分函数。

1.3. 优化圈复杂度

  1. 拆分函数:(最常见的方式)将复杂的函数拆分成多个小函数,每个函数只处理一种逻辑。
  2. 减少嵌套:使用 early return 减少嵌套层级。
  3. 简化逻辑:合并条件表达式,避免重复的分支逻辑。

优化圈复杂度的关键在于:

  1. 减少嵌套,增加代码的扁平化。
  2. 提高代码的模块化和复用性。

2. dupl(重复代码)

Golang 的静态分析工具(如 golangci-lint)能够检测代码中的重复部分。这类工具通常使用重复代码检测算法,通过对代码块进行特定分析,找到相似或完全相同的代码片段。

golangci-lint中检查重复代码的配置如下:

linters:
	enabel:
	  - dupl

linters-settings:
  dupl:
    # Tokens count to trigger issue.
    # Default: 150
    threshold: 100

2.1. 计算规则

Tokenization(代码标记化)

工具会将源代码分解为标记(Token),比如关键字、标识符、操作符等。通过这种方式,它可以忽略注释和格式差异,只关注代码的语义。

Token 是代码的最小组成单元,包括:

  • 关键字:如 ifforswitch
  • 标识符:变量名、函数名、类型名等。
  • 操作符:如 +-*/ 等。
  • 常量值:如 123"text"
  • 界符:如 {}()

threshold 参数的作用

  • 定义: threshold 是重复代码块的最小 Token 数。
  • 如果两个代码块中有 相同 Token 的数量 >= threshold,则会被认为是重复代码,并报告。

示例:threshold: 50

  • 如果两个代码块中有 50 个或更多的 Token 是相同的,dupl 会报告它们为重复代码。
  • 小于 50 个 Token 的重复代码不会被报告。

2.2. 配置方式

通过配置threshold的大小可以控制重复代码的粒度。threshold默认值是150,可根据需要调整大小。

  • 阈值越小:越容易检测到小的重复代码,但可能导致误报增多。

  • 阈值越大:检测更宽松,只报告较大的重复代码块。

当发现重复代码时,golangci-lint 会输出类似的报告:

main.go:10-20: duplicated code found in main.go:30-40 (dupl)

如果要忽略特定代码,在代码中添加注释:

//nolint:dupl
func SomeFunction() {
    // 重复代码块
}

2.3. 优化重复代码

  1. 提取函数:将重复逻辑抽象为通用函数。
  2. 映射表代替分支: 用键值对简化条件逻辑。
  3. 利用泛型: 合并不同类型的重复逻辑。

示例1:

如果重复代码包含多个 if-elseswitch,尝试用映射表或配置文件替代。

// Before: 重复代码块
func GetDiscount(category string) float64 {
    switch category {
    case "student":
        return 0.15
    case "veteran":
        return 0.2
    case "senior":
        return 0.25
    default:
        return 0.0
    }
}

// After: 使用映射表
func GetDiscount(category string) float64 {
    discounts := map[string]float64{
        "student": 0.15,
        "veteran": 0.2,
        "senior":  0.25,
    }
    return discounts[category]
}

示例2:

使用 泛型

// Before: 重复代码块
func FindMaxInt(a, b int) int {
    if a > b {
        return a
    }
    return b
}

func FindMaxFloat(a, b float64) float64 {
    if a > b {
        return a
    }
    return b
}

// After: 使用泛型
func FindMax[T constraints.Ordered](a, b T) T {
    if a > b {
        return a
    }
    return b
}

3. 总结

本文主要分析了gocyclo(圈复杂度)dupl(重复代码)这2种linter配置的检测、配置和优化方式。因为在多人团队的开发中经常会因为开发者的水平不一,标准不一导致无法开发出统一且较高质量的代码。虽然代码的质量不会严格影响功能的运行,但可以为未来的开发者提供可读性更强,更方便接手开发的代码。同样通过这种方式也能初步看出一个开发者代码质量水平的高低。

参考:


最后修改 November 23, 2024: fix golangci-lint (94ad727)