Ошибка, которую никто никогда не совершал. Ведь так, да?
- суббота, 18 июля 2026 г. в 00:00:10
Уже с первого курса университета — или даже раньше — вы могли догадаться, что писать arr[len(arr)] — плохая затея. И вряд ли так кто-то может ошибиться, правда? Мы тоже так думали, пока не проверили 1000 проектов.

Любой разработчик на Go знает, что встроенная функция len() принимает значение и возвращает длину в соответствии с типом значения.
Обратимся к документации и подробнее рассмотрим сигнатуру функции:
func len(v Type) int
Встроенная функция len возвращает длину v в соответствии с типом переданного значения:
Массив: количество элементов в v.
Указатель на массив: количество элементов в *v (даже если v равно nil).
Срез или карта: количество элементов в v; если v равно nil, len(v) равно нулю.
Строка: количество байт в v.
Канал: количество элементов в очереди (непрочитанных) в буфере канала; если v равно nil, len(v) равно нулю.
Рассмотрим простой пример:
func SimpleExample(arr []int) { _ = arr[len(arr)] }
Индексы коллекций всегда начинаются с 0. Соответственно, для слайса длины n допустимые индексы находятся в диапазоне от 0 до n – 1. И len(arr) вернёт n. Поэтому обращение к arr[len(arr)] всегда приведёт к панике программы.
Небольшое предисловие: если кто-то не знает, мы разрабатываем статический анализатор кода PVS-Studio для C, C++, C# и Java. Совсем недавно у нас закончилась программа раннего доступа к новым анализаторам для Go и JS/TS. Сейчас мы активно готовимся к их релизу, который будет в августе. Примерно в то же время стартует программа раннего доступа для PVS-Studio Atlas — командного решения для управления результатами анализа кода. Следите за новостями.
Возвращаемся к V8031. Это диагностика для Go анализатора. Когда мы её делали, то не думали, что она сможет дать такие хорошие результаты. Даже предполагали, что она может ничего не найти. Возникали мысли: “А вдруг это надуманная ошибка, которую отыщут на любом ревью или любым тестом?” Но по опыту знали, что есть десятки таких типов ошибок, которые вроде бы должны легко находиться, но их пропускают из-за лени, невнимательности или усталости.
Реализовать диагностику, естественно, было легко. С точки зрения реализации она сделана на чистом AST и по минимуму использует наши наработки сверх того, что предоставляют встроенные средства для анализа в Go. Кстати, если захотите, то можете сделать свои собственные диагностические правила. Начать можно с нашей ознакомительной статьи “Как сделать свой статический анализатор для Go?”.
После реализации наступает этап тестирования. В нашей команде он состоит из трёх частей:
Unit-тестирование;
Запуск диагностического правила на нашей выборке проектов с GitHub;
Запуск диагностического правила на 1000 самых популярных Go проектах (отбираем по количеству звёзд).
Сейчас нас интересует именно третий этап. Как только мы получили результаты, то сильно удивились. Давайте посмотрим на них.
Обычно в своих статьях мы расписываем ошибку и объясняем возможные пути решения. Здесь этого не будет, так как для всех фрагментов было бы одно и то же описание, которое в точности повторит описание из начала статьи.
bfe — современный балансировщик нагрузки, использующийся в Baidu.
type BasicLitList []*BasicLit .... func (b BasicLitList) End() token.Pos { return b[len(b)].End() }
Предупреждение PVS-Studio: V8031 Using the call of the ‘len’ function as index will cause off-by-one error. Consider using ‘len(b) - 1’ instead. ast.go 138
Ferret — это программируемый механизм извлечения данных и автоматизации для разработчиков.
func (vec *Vector) Push(value core.Value) *Vector { slice := vec.getCurrentSlice() slice[len(slice)] = value return vec }
Предупреждение PVS-Studio: V8031 Using the call of the ‘len’ function as index will cause off-by-one error. Consider using ‘len(slice) - 1’ instead. vector.go 28
fq — инструмент, язык и декодеры для работы с двоичными и текстовыми форматами.
var blockFns = map[uint64]func(d *decode.D, dc *decodeContext){ .... blockTypeInterfaceDescription: func(d *decode.D, dc *decodeContext) { typ := d.FieldU16("link_type", format.LinkTypeMap) d.FieldU16("reserved") d.FieldU32("snap_len") d.FieldArray("options", ....) dc.interfaceTypes[len(dc.interfaceTypes)] = int(typ) }, .... }
Предупреждение PVS-Studio: V8031 Using the call of the ‘len’ function as index will cause off-by-one error. Consider using ‘len(dc.interfaceTypes) - 1’ instead. pcapng.go 234
Incus — это современная система управления контейнерами и менеджер виртуальных машин.
func qemuRawCfgOverride(conf []cfg.Section, confOverride string) (....) { .... content := §ionContent{ comment: sec.Comment, entries: entries, } indexedSection[len(indexedSection)] = content orderedSections = append(orderedSections, section{ name: sec.Name, content: content, }) .... }
Предупреждение PVS-Studio: V8031 Using the call of the ‘len’ function as index will cause off-by-one error. Consider using ‘len(indexedSection) - 1’ instead. driver_qemu_config_override.go 50
А в этом проекте были обнаружены две такие ошибки:
func (f *heartbeatFixture) node() (*state.State, *cluster.Gateway, string) { .... f.gateways[len(f.gateways)] = gateway f.states[gateway] = state f.servers[gateway] = server return state, gateway, address }
V8031 Using the call of the ‘len’ function as index will cause off-by-one error. Consider using ‘len(f.gateways) - 1’ instead. heartbeat_test.go 253
Отмечу, что некоторые ошибки я встречал по несколько раз в других проектах. Это происходило из-за того, что условный проект 1 включал условный проект 2 в качестве зависимости. Так что можно сказать, что ошибки множатся.
Вот таким интересным результатом мы хотели поделиться с вами. Вероятность мала, но, возможно, эта ошибка есть в вашем проекте. Не так давно мы рассказывали о ещё одной интересной ошибке в статье “^ != <<”. А чтобы код проекта был чистым, вы можете использовать статический анализатор PVS-Studio.
Удачи!
Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Artem Rovenskii. One mistake no one has ever made. Right?.