﻿# Go vet не поможет\. Статический анализ Golang проектов с помощью PVS\-Studio

На нём написан Docker, Kubernetes, Gitea и многие другие проекты самых разных масштабов\. Наверное, вы догадались, что речь идёт о Go\. Мы никогда не писали об ошибках в Golang\-проектах, но настало время это исправить, ведь скоро выйдет анализатор PVS\-Studio для Go\!

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

## Введение

Статические анализаторы являются довольно распространёнными инструментами в разработке\. В Golang есть встроенный механизм статического анализа — `go vet`\. Однако стандартные линтеры не всегда справляются\. Для тех, кто с нами не знаком, мы — компания PVS\-Studio, занимаемся разработкой одноименного статического анализатора для C, C\+\+, C\# и Java\. В последнее время мы активно занимаемся разработкой анализатора для Go и уже скоро планируем выпустить открытую бета\-версию\.

Мы уже писали [статью](https://pvs-studio.ru/ru/blog/posts/go/1329/) про то, как сделать свой статический анализатор на Go\. И теперь наступило время показать результат работы нашего анализатора на реальных проектах\.

Это статья немного необычная, поскольку мы впервые пишем об ошибках в проектах, написанных на Go\. В этой статье будут описаны некоторые диагностические правила Golang анализатора, а также будут приведены их срабатывания на реальных проектах\.

## Проверка проектов

Любой анализатор PVS\-Studio, будь то C, C\+\+, C\#, Java или Go, состоит из [диагностических правил](https://pvs-studio.ru/ru/docs/warnings/)\. Именно диагностические правила определяют логику тех или иных срабатываний анализатора\.

Прежде чем приступить к обзору ошибок, давайте я вам немного расскажу, как же у нас выглядит процесс разработки диагностических правил\.

Прежде всего мы ищем идеи для диагностик \(читаем форумы, кодовые базы открытых проектов, обрабатываем обратную связь от клиентов\) и исследуем паттерны ошибок\. 

Затем разработчик получает задание на реализацию определённой диагностики и проводит собственное исследование: в каких ситуациях анализатор должен ругаться, а в каких случаях этого делать не стоит\. Затем идёт написание множества позитивных \(когда анализатор должен ругаться\) и негативных \(когда ругаться НЕ надо\) тестов\. Ну и, само собой, реализация самого диагностического правила\.

Казалось бы, всё, но не так\-то просто\. Те тесты, которые написал разработчик и на которые опирался при реализации, являются синтетическими, и на реальных проектах ситуация может выглядеть совсем иначе\. Поэтому требуется проверить диагностическое правило на реальных проектах, поэтому мы проводим анализ большого количества открытых проектов с GitHub\. В зависимости от полученных срабатываний разработчик либо дорабатывает диагностическое правило, либо отправляет код на ревью\.

Так к чему я это всё? К тому, что все срабатывания, описанные в этой статье, как раз\-таки взяты из реальных проектов\. 

Давайте перейдём к обзору\.

### Повторяющиеся подвыражения

Диагностика V8001 сообщает о наличии одинаковых подвыражений в бинарном выражении\. Эта проблема может возникнуть из\-за copy\-paste и может быть обнаружена с помощью встроенного линтера `go vet`\. Например:

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

В этом случае стандартный инструмент напишет нам `redundant or: r > 1 || r > 1`\. Однако если мы поменяем местами `1` и `r`, а также сменим оператор сравнения, то получим такой код:

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

И на такой код стандартный `go vet` ругаться уже не будет, хотя проблема осталась: подвыражение `r > 1`, по сути, эквивалентно подвыражению `1 < r`\.

Небольшой спойлер: все потенциальные ошибки в статье нельзя найти с помощью встроенного `go vet`\.

#### Nuclei

Рассмотрим пример подобной ошибки на проекте [Nuclei](https://github.com/projectdiscovery/nuclei)\. Это сканер уязвимостей, который позволяет создавать кастомные шаблоны\.

```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)
    },
  }
}
```

Предупреждение PVS\-Studio: 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)

Здесь лишний раз повторяется флаг `packages.NeedTypes`\. Возможно, ошибка произошла из\-за автоподстановки кода, и на самом деле тут должен быть флаг `packages.NeedTypesSizes`:

```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

  ...
)
```

Казалось бы, ошибка примитивная, но с помощью стандартного `go vet` найти её не получится\.

#### Mediamtx

В проекте медиасервера и медиапрокси [Mediamtx](https://github.com/bluenviron/mediamtx) с помощью анализатора был найден следующий фрагмент кода:

```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
}
```

Предупреждение PVS\-Studio: 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)

Смогли бы вы сами найти глазами подобную ошибку, ещё и в сокращённом примере? Код\-ревью, безусловно, мощный способ поиска ошибок, но его не получится применять абсолютно везде\. И ладно бы только это, но встроенный инструмент тоже не справляется\!  

#### Tidb

Похожая ошибка нашлась и в открытом проекте [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)
  ....
}
```

Предупреждение PVS\-Studio: 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)

Несмотря на то, что это достаточно популярный проект \(40 тыс\. звёзд на GitHub\), всё равно в кодовой базе встречаются тривиальные ошибки\. 

#### Wox

В этом [проекте](https://github.com/Wox-launcher/Wox) два срабатывания с одинаковой ошибкой:

```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)
    }
  }
}
```

Предупреждение PVS\-Studio: 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

Очевидно, что сравнивать `manifest.Id` с самим собой не имеет смысла\. В нашем случае имя `manifest` используется как для итерирования по `pluginManifest`:

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

Так и в качестве параметров для функции:

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

Предполагалось сравнивать поле `Id` у элементов из `pluginManifest` с тем же полем параметра анонимной функции\. Но получилось, что сравниваются одинаковые поля одного и того же параметра:

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

Такая же ошибка в другом месте:

```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)
  }
  ....
}
```

Предупреждение PVS\-Studio: 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

Стоит отметить, что к моменту выхода этой статьи ошибки успели исправить в этом [коммите](https://github.com/Wox-launcher/Wox/commit/734373645db2e196951b3c32ee66262834359f01#diff-8622e27d5198eb5cfd06eadf237232b7f1cdbf76f58521f1b770f59299fa5ad6L121) и в [этом](https://github.com/Wox-launcher/Wox/commit/9fa2f82567bbf2b450942043b0e92954dd69aaa3#diff-34196fbd61ee4230a4bf617fbbe9b3da3eb284c5e10fa413d95c345b4dc0bcdeL72)\. Хотя, судя по истории изменений, ошибки продержались около трёх лет\.

Таких ошибок можно полностью избежать, если использовать статический анализ [регулярно](https://habr.com/ru/companies/pvs-studio/articles/879256/?ysclid=mkqudd8c4o728900128), например, в CI/CD системах как [quality gate](https://pvs-studio.ru/ru/blog/posts/1264/) перед каждым коммитом\.

### Ошибка в строке формата

Большинство ошибок, связанных со строками формата, заключается в несовпадении количества аргументов с количеством плейсхолдеров\. Однако в проекте [Gitea](https://github.com/go-gitea/gitea) была обнаружена следующая интересная ошибка:

```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)
      }
    }
    ....
  }
  ....
}
```

Предупреждение PVS\-Studio: 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

Давайте присмотримся к строке формата:

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

Казалось бы, всё хорошо и количество аргументов совпадает с количеством плейсхолдеров в строке\. Предполагается, что `%[2]s` будет соответствовать переменной `constraint`, поскольку имеется индекс `[2]`, `%[1]s` — соответствовать `tableName`, и последнее оставшееся `%v` — переменной `err`\. Но на самом деле вместо `err` для `%v` будет подставлено значение `constraint`\.

Почему так происходит? 

В Golang можно явно указать номер аргумента, который должен подставляться в конкретный плейсхолдер\. Важно отметить, что следующий номер аргумента для подстановки вычисляется исходя из номера предыдущего аргумента подстановки, за исключением первого аргумента\. Следовательно, если явно указать, что должен подставляться аргумент `n`, то следом за ним будет подставлен аргумент `n + 1`\.

В этом случае `%[1]s` указывает на первый аргумент `tableName` и неявно переносит номер аргумента для следующего плейсхолдера на `1` от текущего, то есть второй аргумент `constraint` вместо задуманного `err`\.

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

Паттерн `if A { } else if A { }` зачастую является copy\-paste ошибкой, поскольку в таком случае второе условие `A` всегда будет ложным при его проверке и, следовательно, код внутри `then`\-ветки никогда не будет выполнен\.

#### Glance

Такую ситуацию можно обнаружить в проекте [Glance](https://github.com/glanceapp/glance):

```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
  }
}
```

Предупреждение PVS\-Studio: 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)

Здесь снова copy\-paste: `then`ветки обоих `if` очень похожи, но, вероятнее всего, тут сделали копипаст и забыли поменять второе условие\.

#### Havoc

Аналогичную ошибку можно найти в кодовой базе фреймворка [Havoc](https://github.com/HavocFramework/Havoc):

```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"
}
```

Предупреждение PVS\-Studio: 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)

Во фрагменте кода условие и `then`\-ветки `if`\-ов совпадают, поэтому, возможно, что это лишь избыточный код\. Но также есть вероятность, что вместо второго `INJECT_ERROR_FAILED` должно быть что\-то другое, и сейчас часть логики программы попросту не работает\.

### Case\-повторюшки

Стандартный линтер не даёт написать несколько `case` с повторяющимися выражениями, но иногда не справляется\. 

Например, здесь стандартный инструмент будет ругаться:

```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` используется дважды в качестве выражения для `case.`

Но уже здесь `go vet` ошибки не найдёт:

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

В свою очередь, диагностическое правило V8010 анализатора PVS\-Studio учитывает различные перестановки внутри бинарных выражений\.

Рассмотрим срабатывания этого правила на реальных проектах\.

#### AdGuardHome

В коде популярного блокировщика рекламы [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
  }
}
```

Предупреждение PVS\-Studio: V8010 Two or more case branches have equivalent expressions\. [migrator\.go 95](https://github.com/AdguardTeam/AdGuardHome/blob/4c360e4ae8be1891028b7033bc4c047dae7edb76/internal/configmigrate/migrator.go#L95)

Очень подозрительный код\. На первый взгляд кажется, что произошла путаница из\-за оператора присваивания, и в последнем кейсе должно быть `target > current`\. Но, судя по строке "target schema version %d lower than current %d" внутри `fmt.Errorf` в теле этого кейса, условие написано правильно\.

Поэтому, скорее всего, неправильно написано условие для первого `case`, что очень странно, поскольку обычно наоборот: сначала пишут правильно, а потом ошибаются \(так называемый [эффект последней строки](https://habr.com/ru/companies/pvs-studio/articles/224783/)\)\.

### Лишний compound assignment

Диагностическое правило V8011 сообщает, что слева и справа от составного оператора присваивания используется одна и та же переменная\. Это может означать, что разработчик использовал составной оператор присваивания \(например, `+=`\), а затем в правой части написал ту же переменную, к которой происходит присваивание, но так, будто используется обычный оператор присваивания `=`\. Например:

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

Очевидно, что нет смысла вычитать из `x` самого себя и можно сразу написать так:

```cpp
x = 2
```

Но, скорее всего, здесь ошибка, и разработчик хотел вычесть из `x` значение `2`\.

#### Gitea

Подобный простейший пример можно найти в кодовой базе проекта [Gitea](https://github.com/go-gitea/gitea):

```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               // <=
          }
        }
      }
    }
  ....
}
```

Предупреждение PVS\-Studio: 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)

Очевидно, что нет смысла вычитать из `m[1]` своё же значение\. Скорее всего, из `m[1]` хотели вычесть `i`, но произошла опечатка и получилось что\-то среднее между `m[1] = m[1] – i` и `m[1] -= i`\.

В этом же проекте нашлась похожая ошибка:

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

Предупреждение PVS\-Studio: V8011 Identical expression 'previousNum' 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"     // <=
    }
  }
  ....
}
```

Предупреждения PVS\-Studio:

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)

Здесь оператор `+=` используется для типа `string`\. Очень странным является добавление к переменной `uri` самой себя\.

#### OpenDiablo2

Бывают случаи, когда скобки могут запутать\. Например, как в проекте [OpenDiablo2](https://github.com/OpenDiablo2/OpenDiablo2):

```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
  }
}
```

Предупреждения PVS\-Studio:

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)

Здесь ошибки не так бросаются в глаза, но если раскрыть скобки, то получим:

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

И:

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

Для второго срабатывания\.

## Бета\-версия PVS\-Studio для Golang

В апреле 2026 года мы планируем выпустить открытую бета\-версию PVS\-Studio для языка Go\. Для того, чтобы это не пропустить, настоятельно рекомендуем подписаться на нашу [рассылку](https://pvs-studio.ru/ru/subscribe/)\.

Дальше — больше\. Мы планируем активно развивать наш Go анализатор, поэтому будем рады, если вы попробуете [PVS\-Studio](https://pvs-studio.ru/ru/pvs-studio/try-free/) и дадите нам обратную связь\.

## Заключение

На этом всё\. Мы рассмотрели небольшое количество срабатываний нашего анализатора на Golang\-проектах и наглядно убедились в том, что ошибки, к сожалению, не обходят стороной даже крупные проекты\. Поэтому важно использовать инструменты для контроля качества кодовой базы\.

Отметим, что анализатор PVS\-Studio можно использовать бесплатно\. Если вы участвуете в разработке открытого проекта \(например, тех, что упомянуты в статье\), то можете получить бесплатную версию\. Подробности описаны в [статье](https://pvs-studio.ru/ru/blog/posts/1286/) про бесплатное лицензирование\.

Берегите себя и свой код\!