﻿# Go vet can't go: How PVS\-Studio analyzes Go projects

Docker, Kubernetes, Gitea, and other projects—they all have one thing in common: they're written in Go\. Perhaps you have now realized that this is what we are talking about today\. We've never described Golang errors before, but now we're making up for it\. PVS\-Studio Go static analyzer is coming soon\.

![1342_Golang_errors/image1.png](https://import.viva64.com/docx/blog/1342_Golang_errors/image1.png)

## Intro

Static analyzers are rather common development tools, and Go has a built\-in static analysis mechanism, `go vet`\. However, standard linters don't always work as planned\. For newcomers, we're PVS\-Studio, a company that develops a static analyzer for C, C\+\+, C\#, and Java\. Recently, we've been actively working on a Go analyzer and are planning to introduce its beta soon\.

We've already written an [article](https://pvs-studio.com/en/blog/posts/go/1329/) about how to create your own Go static analyzer, so now it's time to show how our analyzer shines in real\-world projects\.

The article may seem quite odd because it's our first try at writing about Go projects\. In this article, we describe some diagnostic rules of the upcoming Go analyzer and also show some warnings issued for real\-world projects\.

## Project analysis

Any PVS\-Studio analyzer on any language—it doesn't matter C, C\+\+, C\#, Java, or Go—claims [diagnostic rules](https://pvs-studio.com/en/docs/warnings/) because they determine the logic of certain analyzer warnings\.

Before we begin reviewing the errors, let us tell you a little bit about how we develop diagnostic rules\.

First, we look for ideas for diagnostic rules\. We read forums, explore open\-source codebases, process customer feedback, and study error patterns\. 

The developer then gets a task to implement specific diagnostic rules and does their own research: where the analyzer should issue warnings, and where it should stay silent? After that comes writing a large set of tests—both positive ones \(where the analyzer should issue warnings\) and negative ones \(where it definitely shouldn't\)\. And, of course, the diagnostic rule itself has to be implemented\.

It sounds easy, but it's not that simple\. The tests used during development are synthetic, and real\-world projects often behave very differently\. Therefore, it's necessary to test the diagnostic rule on real projects, which is why we analyze many open\-source projects from GitHub\. Depending on the results, developers either refine the diagnostic rule or send it for code review\.

So what's the point of all this? All described warnings are taken from real projects\. 

With that out of the way, let's move on to the review\.

### Repeated subexpressions

The V8001 diagnostic rule warns developers when the binary expression contains identical subexpressions\. This kind of issue usually shows up after a careless copy\-paste—the built\-in `go vet` linter can detect it too\. For example:

```cpp
func rgb1(r float32, g float32, b float32) {
  if r > 1 || g > 1 || r > 1 {
    ....
  }
}
```

In this case, the standard tool outputs `redundant or: r > 1 || r > 1`\. However, if we swap `1` and `r` and change the comparison operator, we get the following code:

```cpp
func rgb(r float32, g float32, b float32) {
  if r > 1 || g > 1 || 1 < r {
    ....
  }
}
```

Standard `go vet` no longer issues a warning about this code, although the problem remains: the `r > 1` subexpression is equivalent to `1 < r`\.

A small spoiler: all potential errors in the article slip past the built\-in `go vet`\.

#### Nuclei

Let's look at an example of such an error in the [Nuclei](https://github.com/projectdiscovery/nuclei) project, a vulnerability scanner that allows creating user\-defined templates\.

```cpp
func NewEntityParser(dir string) (*EntityParser, error) {

  cfg := &packages.Config{
    Mode: packages.NeedName | packages.NeedFiles | packages.NeedImports |
      packages.NeedTypes | packages.NeedSyntax | packages.NeedTypes |
      packages.NeedModule | packages.NeedTypesInfo,
    Tests: false,
    Dir:   dir,
    ParseFile: func(....) (*ast.File, error) {
      return parser.ParseFile(fset, filename, src, parser.ParseComments)
    },
  }
}
```

The PVS\-Studio warning: V8001 There are identical sub\-expressions 'packages\.NeedTypes' to the left and to the right of the '\|' operator\. [parser\.go 33](https://github.com/projectdiscovery/nuclei/blob/ee8287a7b756d07e09e22b8945cddeb58e727a22/pkg/js/devtools/tsgen/parser.go#L33)

The `packages.NeedTypes` flag is repeated for no reason\. Most likely, the error occurred due to code autocompletion, and in reality, there should be the `packages.NeedTypesSizes` flag here:

```cpp
const (
  ....
  // NeedTypes adds Types, Fset, and IllTyped.
  NeedTypes

  // NeedSyntax adds Syntax and Fset.
  NeedSyntax

  // NeedTypesInfo adds TypesInfo and Fset.
  NeedTypesInfo

  // NeedTypesSizes adds TypesSizes.
  NeedTypesSizes

  ...
)
```

It'd seem to be a trivial error, but it can't be detected using the standard `go vet`\.

#### Mediamtx

The analyzer detected the following code fragment in the [Mediamtx](https://github.com/bluenviron/mediamtx) media server and media proxy project using the analyzer:

```cpp
func (p *Core) closeResources(....) {
  ....
  closeRTSPServer := newConf == nil ||
    newConf.RTSP != p.conf.RTSP ||
    ....
    newConf.RTSPAddress != p.conf.RTSPAddress ||            // <=
    ....
    newConf.RTSPUDPReadBufferSize != p.conf.RTSPUDPReadBufferSize ||
    newConf.UDPReadBufferSize != p.conf.UDPReadBufferSize ||
    newConf.ReadTimeout != p.conf.ReadTimeout ||
    newConf.WriteTimeout != p.conf.WriteTimeout ||
    newConf.WriteQueueSize != p.conf.WriteQueueSize ||
    newConf.RTPAddress != p.conf.RTPAddress ||
    newConf.RTCPAddress != p.conf.RTCPAddress ||
    newConf.MulticastIPRange != p.conf.MulticastIPRange ||
    newConf.MulticastRTPPort != p.conf.MulticastRTPPort ||
    ....
    newConf.RTSPAddress != p.conf.RTSPAddress ||            // <=
    ....
    newConf.RunOnConnect != p.conf.RunOnConnect ||
    newConf.RunOnConnectRestart != p.conf.RunOnConnectRestart ||
    newConf.RunOnDisconnect != p.conf.RunOnDisconnect ||
    closeMetrics ||
    closePathManager ||
    closeLogger
}
```

The PVS\-Studio warning: V8001 There are identical sub\-expressions 'newConf\.RTSPAddress \!\= p\.conf\.RTSPAddress' to the left and to the right of the '\!\=' operator\. [core\.go 790](https://github.com/bluenviron/mediamtx/blob/aa6dc4ea9f68f9f7f0de9eefe71f5dd1ba7e75cb/internal/core/core.go#L822)

Would you spot such an error yourself, even in this shortened example? Yes, code review is undoubtedly a powerful way to find bugs, but it may not always be able to help\. Besides, even a built\-in tool can't handle everything\!

#### Tidb

A similar issue was also found in [Tidb](https://github.com/pingcap/tidb):

```cpp
func ProcessModifyColumnOptions(....) error {
  var sb strings.Builder
  restoreFlags := format.RestoreStringSingleQuotes |
    format.RestoreKeyWordLowercase | 
    format.RestoreNameBackQuotes |
    format.RestoreSpacesAroundBinaryOperation |
    format.RestoreWithoutSchemaName |                      // <=
    format.RestoreWithoutSchemaName                        // <=
    restoreCtx := format.NewRestoreCtx(restoreFlags, &sb)
  ....
}
```

The PVS\-Studio warning: V8001 There are identical sub\-expressions 'format\.RestoreWithoutSchemaName' to the left and to the right of the '\|' operator\. [modify\_column\.go 2073](https://github.com/pingcap/tidb/blob/93c65e2234ad4bdfccfc0c95433131dfa453d1c9/pkg/ddl/modify_column.go#L2045)

Although this project is popular enough \(40,000 stars on GitHub\), there are still trivial errors in the codebase\. 

#### Wox

In this [project](https://github.com/Wox-launcher/Wox), the analyzer detected two instances of the same error:

```cpp
func (s *Store) GetStorePluginManifests(....) []StorePluginManifest {
  ....
  for _, manifest := range pluginManifest {
      existingManifest, found := lo.Find(storePluginManifests, 
        func(manifest StorePluginManifest) bool {
          return manifest.Id == manifest.Id
      })
      if found {
        ....
      }

      storePluginManifests = append(storePluginManifests, manifest)
    }
  }
}
```

The PVS\-Studio warning: V8001 There are identical sub\-expressions 'manifest\.Id' to the left and to the right of the '\=\=' operator\. [store\.go](https://github.com/Wox-launcher/Wox/blob/d35cc06fc294a9f526e5be486e0f357130607142/wox.core/plugin/store.go#L172) 100

Comparing `manifest.Id` with identical subexpression doesn't make sense\. In our case, the `manifest` name is used both for iterating through `pluginManifest`:

```cpp
for _, manifest := range pluginManifest { .... }
```

The same applies to the parameters for the function:

```cpp
func(manifest StorePluginManifest) bool { .... }
```

The intent was to compare the `Id` field of elements from `pluginManifest` with the same field of the anonymous function parameter\. Instead, it turned out that identical fields of the same parameter are being compared:

```cpp
func(manifest StorePluginManifest) bool {
  return manifest.Id == manifest.Id
}
```

There is the same error in another place:

```cpp
func (s *Store) GetStoreThemes(ctx context.Context) []common.Theme {
  ....
  for _, manifest := range themeManifest {
    _, found := lo.Find(storeThemeManifests, 
      func(manifest common.Theme) bool {
        return manifest.ThemeId == manifest.ThemeId    // <=
    })
    if found {
      //skip duplicated theme
      continue
    }

    storeThemeManifests = append(storeThemeManifests, manifest)
  }
  ....
}
```

The PVS\-Studio warning: V8001 There are identical sub\-expressions 'manifest\.ThemeId' to the left and to the right of the '\=\=' operator\. [store\.go](https://github.com/Wox-launcher/Wox/blob/d35cc06fc294a9f526e5be486e0f357130607142/wox.core/ui/store.go#L74) 72

When we posted this article, the errors had been fixed in this [commit](https://github.com/Wox-launcher/Wox/commit/734373645db2e196951b3c32ee66262834359f01#diff-8622e27d5198eb5cfd06eadf237232b7f1cdbf76f58521f1b770f59299fa5ad6L121) and in this [one](https://github.com/Wox-launcher/Wox/commit/9fa2f82567bbf2b450942043b0e92954dd69aaa3#diff-34196fbd61ee4230a4bf617fbbe9b3da3eb284c5e10fa413d95c345b4dc0bcdeL72)\. Although judging by the changelog, the errors persisted for about three years\.

These kinds of issues can be avoided entirely by using static analysis [regularly](https://pvs-studio.com/en/blog/posts/1218/), for example, by integrating it into a [quality gate](https://pvs-studio.com/en/blog/posts/1264/) before each commit\.

### String format error

Most format string errors are caused by a mismatch between the number of arguments and the number of placeholders\. However, we found this bug in the [Gitea](https://github.com/go-gitea/gitea) project:

```cpp
func DropTableColumns(...) (err error) {
  switch {
  ....
  case setting.Database.Type.IsMSSQL():
    ....
    for _, constraint := range constraints {
      if _, err := sess.Exec(fmt.Sprintf("DROP INDEX `%[2]s` ON `%[1]s`", 
        tableName, constraint)); err != nil {
        return fmt.Errorf("Drop index `%[2]s` on `%[1]s`: %v", 
          tableName, constraint, err)
      }
    }
    ....
  }
  ....
}
```

The PVS\-Studio warning: V8013 Incorrect format\. A different number of format items is expected while calling the 'fmt\.Errorf' function\. Expected: 2, present: 3\. [db\.go](https://github.com/go-gitea/gitea/blob/52c3a7d3ceb99fbc45bf5a69bdbbd5974ed8d707/models/migrations/base/db.go#L469) 469

Let's look at this format string with piercing eyes:

```cpp
"Drop index `%[2]s` on `%[1]s`: %v"
```

Seems fine, yeah? And the number of arguments matches the number of placeholders in the string\. It's assumed that `%[2]s` refers to the `constraint` variable, since there is a `[2]` index, `%[1]s` will correspond to `tableName`, and the last remaining `%v` will correspond to the `err` variable\. But in reality, instead of `err`, the value `constraint` will be substituted for `%v`\.

Why does this happen? 

In Go, we can directly specify the argument that should be used in the actual placeholder\. Note that next argument number for substitution is evaluated based on the number of the previous substitution argument, except for the first argument\. Therefore, if we directly specify that the `n` argument should be substituted, the `n + 1` will be substituted after it\.

In this case, `%[1]s` points to the first argument, `tableName`, and implicitly shifts the argument number for the next placeholder by `1` from the current one—so the second argument, `constraint`, is used instead of the intended `err`\.

### if A \{ \} else if A \{ \}

The `if A { } else if A { }` pattern often indicates a copy\-paste error because the second `A` condition is always false during the check, and so, the code within the `then` branch will never be executed\.

#### Glance

We found this in the [Glance](https://github.com/glanceapp/glance) project:

```cpp
func parseCliOptions() (*cliOptions, error) {
  var args []string

  args = os.Args[1:]
  if len(args) == 0 {
    intent = cliIntentServe
  } else if len(args) == 1 {
    ....
  } else if len(args) == 2 {
    if args[0] == "password:hash" {
      intent = cliIntentPasswordHash
    } else {
      return nil, unknownCommandErr
    }
  } else if len(args) == 2 {
    if args[0] == "mountpoint:info" {
      intent = cliIntentMountpointInfo
    } else {
      return nil, unknownCommandErr
    }
  } else {
    return nil, unknownCommandErr
  }
}
```

The PVS\-Studio warning: V8004 The use of 'if A \{\.\.\.\} else if A \{\.\.\.\}' pattern was detected\. Potential logical error is present\. Check lines: 92, 86\. [cli\.go 92](https://github.com/glanceapp/glance/blob/6c5b7a3f4cc409e31739b2914bb6636d08299126/internal/glance/cli.go#L92)

Here's another case of copy\-paste gone wrong: the `then` branches of both `if` statements are very similar, but most likely, someone copied and pasted them and forgot to change the second condition\.

#### Havoc

A similar error was found in the [Havoc](https://github.com/HavocFramework/Havoc) framework codebase:

```cpp
var (
  Status  = Parser.ParseInt32()
  Message = make(map[string]string)
)

logger.Debug(fmt.Sprintf(....))

if Status == INJECT_ERROR_SUCCESS {
  Message["Type"] = "Good"
  Message["Message"] = "Successful injected shellcode"
} else if Status == INJECT_ERROR_FAILED {               // <=
  Message["Type"] = "Error"
  Message["Message"] = "Failed to inject shellcode"
} else if Status == INJECT_ERROR_INVALID_PARAM {
  Message["Type"] = "Error"
  Message["Message"] = "Invalid parameter specified"
} else if Status == INJECT_ERROR_PROCESS_ARCH_MISMATCH {
  Message["Type"] = "Error"
  Message["Message"] = "Process architecture mismatch"
} else if Status == INJECT_ERROR_FAILED {               // <=
  Message["Type"] = "Error"
  Message["Message"] = "Failed to inject shellcode"
}
```

The PVS\-Studio warning: V8004 The use of 'if A \{\.\.\.\} else if A \{\.\.\.\}' pattern was detected\. Potential logical error is present\. Check lines: 3637, 3628\. [demons\.go 3637](https://github.com/HavocFramework/Havoc/blob/c84f7755a8b1c0f737576bdd3a0e982a296012f1/teamserver/pkg/agent/demons.go#L3637)

In the code snippet, the condition and the `then` branch of the `if` statements are identical, so it's possible that this is just redundant code\. However, it's also possible that the second `INJECT_ERROR_FAILED` should be something else, and now, part of the program's logic simply doesn't work\.

### Duplicate cases

Indeed, the standard linter doesn't permit you to write multiple `case` statements with duplicated expressions, but sometimes it can fail\. 

For example, the standard tool will issue a warning:

```cpp
const (
  MONDAY    = 1
  TUESDAY   = 2
  WEDNESDAY = 3
  THURSDAY  = 4
  FRIDAY    = 5
)
switch day {
case MONDAY:
  ....
case TUESDAY:
  ....
case WEDNESDAY:
  ....
case THURSDAY:
  ....
case MONDAY:
  ....
}
```

`MONDAY` is used twice as an expression for `case.`

But here, `go vet` won't find any errors:

```cpp
switch {
case day == MONDAY:
  ....
case day == TUESDAY:
  ....
case day == WEDNESDAY:
  ....
case day == THURSDAY:
  ....
case day == MONDAY:
  ....
}
```

However, the V8010 diagnostic rule of PVS\-Studio analyzer can detect various arrangements within binary expressions\.

Let's take a look at this rule in real\-world projects\.

#### AdGuardHome

The following potential bug was found in the code of the popular ad blocker, [AdGuardHome](https://github.com/AdguardTeam/AdGuardHome):

```cpp
func validateVersion(current, target uint) (err error) {
  switch {
  case current > target:
    return fmt.Errorf("unknown current schema version %d", current)
  case target > LastSchemaVersion:
    return fmt.Errorf("unknown target schema version %d", target)
  case target < current:
    return fmt.Errorf("target schema version %d lower than current %d", 
                       target, current)
  default:
    return nil
  }
}
```

The PVS\-Studio warning: V8010 Two or more case branches have equivalent expressions\. [migrator\.go 95](https://github.com/AdguardTeam/AdGuardHome/blob/4c360e4ae8be1891028b7033bc4c047dae7edb76/internal/configmigrate/migrator.go#L95)

It's very suspicious code: at first glance, it seems that assignment operator caused a fuss, and in the last case, we should get `target > current`\. However, judging by the line `"target schema version %d lower than current %d"` inside `fmt.Errorf` in the body of this case, the condition is correct\.

Most likely, the condition in the first `case` is written incorrectly, which is very odd\. Ususally, It's the other way around: the first line is correct, and developers make mistakes later \(the so\-called [last line effect](https://pvs-studio.com/en/blog/posts/cpp/0260/)\)\.

### Redundant compound assignment

V8011 indicates that the same variable surrounds a compound assignment operator\. This may mean that developers use a compound assignment operator \(e\.g\., `+=`\), and then wrote the same variable to which the assignment is made on the right side, but as if using a regular assignment operator `=`\. For example:

```cpp
x -= x - 2
```

Obviously, there is no point in subtracting `x` from itself, and we can write it like this:

```cpp
x = 2
```

However, this is most likely an error, and developers intended to subtract `2` from `x`\.

#### Gitea

A simple example like this can be found in the [Gitea](https://github.com/go-gitea/gitea) project codebase:

```cpp
func (s *linkifyParser) Parse(....) ast.Node {
  ....
  if m != nil {
    lastChar := line[m[1]-1]
    if lastChar == '.' {
      m[1]--
    } else if lastChar == ')' {
      ....
    } else if lastChar == ';' {
      i := m[1] - 2
        for ; i >= m[0]; i-- {
          if util.IsAlphaNumeric(line[i]) {
            continue
          }
          break
        }
        if i != m[1]-2 {
          if line[i] == '&' {
            m[1] -= m[1] – i               // <=
          }
        }
      }
    }
  ....
}
```

The PVS\-Studio warning: V8011 Identical expression 'm\[1\]' to the left and to the right of compound assignment\. [linkify\.go 108](https://github.com/go-gitea/gitea/blob/d46021a83ad511ef2df8050cb6f2f4076ee366ba/modules/markup/common/linkify.go#L108)

Obviously, subtracting its own value from `m[1]` makes no sense\. Most likely, devs would like to subtract `i` from `m[1]`, but there was a typo and it turned out to be something between `m[1] = m[1] – i` and `m[1] -= i`\.

A similar error was found in the same project:

```cpp
func (p *Paginator) Pages() []*Page {
  ....
  previousNum := getMiddleIdx(p.numPages) - 1
  if previousNum > p.current-1 {
    previousNum -= previousNum - (p.current - 1)
  }
  ....
}
```

The PVS\-Studio warning: V8011 Identical expression to the left and to the right of compound assignment\. [paginator\.go 177](https://github.com/go-gitea/gitea/blob/d46021a83ad511ef2df8050cb6f2f4076ee366ba/modules/paginator/paginator.go#L177)

#### Gopay

```cpp
func (c *Client) InvoiceSearch(....) (....) {
  uri := searchInvoice
  if page != 0 && pageSize != 0 {
    uri += uri + "?page=" +                   // <=
     strconv.Itoa(page) + 
     "&page_size=" + 
    strconv.Itoa(pageSize)
  }
  if totalRequired {
    if page != 0 && pageSize != 0 {
      uri += uri + "&total_required=true"     // <=
    } else {
      uri += uri + "?total_required=true"     // <=
    }
  }
  ....
}
```

The PVS\-Studio warnings:

V8011 Identical expression 'uri' to the left and to the right of compound assignment\. [invoice\.go 362](https://github.com/go-pay/gopay/blob/9d0006967d9428de349bb4b4e659010fdc558ae8/paypal/invoice.go#L362)

V8011 Identical expression 'uri' to the left and to the right of compound assignment\. [invoice\.go 366](https://github.com/go-pay/gopay/blob/9d0006967d9428de349bb4b4e659010fdc558ae8/paypal/invoice.go#L366)

V8011 Identical expression 'uri' to the left and to the right of compound assignment\. [invoice\.go 368](https://github.com/go-pay/gopay/blob/9d0006967d9428de349bb4b4e659010fdc558ae8/paypal/invoice.go#L368C4-L368C39)

The `+=` operator is used on a `string` type, which is very strange to add the `uri` variable to itself\.

#### OpenDiablo2

Parentheses can also be a source of confusion\. For example, this happens in the [OpenDiablo2](https://github.com/OpenDiablo2/OpenDiablo2) project:

```cpp
func (wg *WidgetGroup) adjustSize(w Widget) {
  x, y := w.GetPosition()
  width, height := w.GetSize()

  if x+width > wg.x+wg.width {
    wg.width += (x + width) - (wg.x + wg.width)     // <= (1)
  }

  if wg.x > x {
    wg.width += wg.x - x
    wg.x = x
  }

  if y+height > wg.y+wg.height {
    wg.height += (y + height) - (wg.y + wg.height)  // <= (2)
  }

  if wg.y > y {
    wg.height += wg.y - y
    wg.y = y
  }
}
```

The PVS\-Studio warnings:

V8011 Identical expression 'wg\.width' to the left and to the right of compound assignment\. [widget\_group\.go 55](https://github.com/OpenDiablo2/OpenDiablo2/blob/7f92c571bf04057a7fbdfb5d25a486f7d775e3c3/d2core/d2ui/widget_group.go#L55)

V8011 Identical expression 'wg\.height' to the left and to the right of compound assignment\. [widget\_group\.go 64](https://github.com/OpenDiablo2/OpenDiablo2/blob/7f92c571bf04057a7fbdfb5d25a486f7d775e3c3/d2core/d2ui/widget_group.go#L64)

Here, the errors aren't so obvious, but if we remove the brackets, we get:

```cpp
wg.width += -wg.width + ....
```

And:

```cpp
wg.height += -wg.height + ....
```

## PVS\-Studio Go: Beta version

In April 2026, we plan to release an open beta version of PVS\-Studio for Go\. To make sure you don't miss it, we highly recommend subscribing to our [newsletter](https://pvs-studio.com/en/subscribe/)\.

Looking ahead, we plan to actively develop our Go analyzer, so we'd be glad if you try [PVS\-Studio](https://pvs-studio.com/en/pvs-studio/try-free/) and give us your feedback\.

## Conclusion

That's it\! We've looked at a few examples of how our analyzer can work with Go projects\. As we can see, even large projects have no immunity to errors\. Therefore, it's important to use tools to control the code quality\.

And don't forget—you can use PVS\-Studio for free\! If you're contributing to an open\-source project \(like the ones we mentioned\), you can get a free license\. Check out our [article](https://pvs-studio.com/en/blog/posts/1286/) on free licensing for the details\.

Take care of yourself and your code\!