gocyclo

Table of Contents

  1. 解決する問題
  2. 設定
  3. オプション
  4. 循環的複雑度の計算ルール
  5. サンプル
    1. 検出例
    2. 修正例
  6. 注意点
  7. 参考リンク

gocyclo は関数の循環的複雑度(Cyclomatic Complexity)を計測し、閾値を超えた関数を報告するツールです。

解決する問題

関数内の分岐が増えるほどテストすべきパスが増え、バグが混入しやすくなります。循環的複雑度は分岐の数を定量化する指標で、複雑すぎる関数を早期に発見して分割を促します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Before: 循環的複雑度が高い関数
func Validate(u User) error {
if u.Name == "" { // +1
return errors.New("name required")
}
if len(u.Name) > 100 { // +1
return errors.New("name too long")
}
if u.Email == "" { // +1
return errors.New("email required")
}
if !strings.Contains(u.Email, "@") { // +1
return errors.New("invalid email")
}
if u.Age < 0 || u.Age > 150 { // +1, +1 (||)
return errors.New("invalid age")
}
if u.Role == "" { // +1
return errors.New("role required")
}
for _, r := range validRoles { // +1
if r == u.Role { // +1
return nil
}
}
return errors.New("unknown role")
}
// 循環的複雑度: 10 (基底1 + 分岐9)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// After: バリデーションルールを分離して複雑度を低減
func Validate(u User) error {
if err := validateName(u.Name); err != nil {
return err
}
if err := validateEmail(u.Email); err != nil {
return err
}
if err := validateAge(u.Age); err != nil {
return err
}
return validateRole(u.Role)
}

func validateName(name string) error {
if name == "" {
return errors.New("name required")
}
if len(name) > 100 {
return errors.New("name too long")
}
return nil
}

設定

公式ドキュメント

1
2
3
4
5
6
7
8
9
# .golangci.yml 設定例
version: "2"

linters:
enable:
- gocyclo
settings:
gocyclo:
min-complexity: 15

オプション

オプション デフォルト 説明
min-complexity int 30 この値以上の循環的複雑度を持つ関数を報告する

循環的複雑度の計算ルール

基底値は 1 で、以下の構造ごとに +1 されます。

構造 加算
if +1
for +1
case(switch / select 内) +1
&& +1
|| +1

サンプル

検出例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// gocyclo が警告: cyclomatic complexity 12 of func ParseConfig is high (> 10)
func ParseConfig(data map[string]any) (*Config, error) {
cfg := &Config{}

if v, ok := data["host"]; ok { // +1, +1
if s, ok := v.(string); ok { // +1, +1
cfg.Host = s
}
}
if v, ok := data["port"]; ok { // +1, +1
if n, ok := v.(float64); ok { // +1, +1
cfg.Port = int(n)
}
}
if v, ok := data["debug"]; ok { // +1, +1
if b, ok := v.(bool); ok { // +1, +1
cfg.Debug = b
}
}
return cfg, nil
}

修正例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func ParseConfig(data map[string]any) (*Config, error) {
cfg := &Config{}

cfg.Host = getStringOr(data, "host", "")
cfg.Port = getIntOr(data, "port", 0)
cfg.Debug = getBoolOr(data, "debug", false)

return cfg, nil
}

func getStringOr(data map[string]any, key, fallback string) string {
if v, ok := data[key]; ok {
if s, ok := v.(string); ok {
return s
}
}
return fallback
}

注意点

  • gocyclo はネストの深さを考慮しない。ネストの深さも考慮したい場合は gocognit を併用する
  • 推奨閾値は一般的に 10〜20。Go の公式ツールでは 15 が目安とされることが多い
  • 循環的複雑度はテストで必要なパス数の下限を示す指標でもある

参考リンク