Пустая строка в Go: лучший способ проверки и что под капотом

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: вопрос, который мучает каждого Go-разработчика

Если вы когда-нибудь писали на Go, то наверняка сталкивались с простой, на первый взгляд, задачей: проверить, не пустая ли строка. Казалось бы, что может быть проще? Но, как показывает популярнейший вопрос на Stack Overflow (десятки тысяч просмотров!), даже в такой элементарной операции есть свои нюансы.

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

Готовы? Поехали! 🚀

Что такое строка в Go? Заглядываем под капот

Прежде чем говорить о проверке пустоты, давайте разберемся, что вообще из себя представляет строка в Go. В отличие от многих других языков, строка в Go — это неизменяемая (immutable) последовательность байтов. Но самое интересное — это то, как она хранится в памяти.

String Header: структура, которую вы не видите

Внутри Go представляет строку как небольшую структуру данных, которая называется string header. В коде это выглядит так:

type StringHeader struct {
    Data uintptr  // Указатель на массив байтов
    Len  int      // Длина строки в байтах
}

Это означает, что любая строка, даже пустая, занимает 16 байт (на 64-битной системе) — 8 байт под указатель и 8 байт под длину. Важно: пустая строка — это не nil! Это полноценный объект с указателем (обычно на специальный пустой блок памяти) и длиной, равной нулю.

Как это влияет на проверку?

Зная это, мы можем понять, как работают разные методы проверки. Когда вы пишете s == "", Go компилятор не сравнивает каждый байт. Он делает то, что называется "де-факто сравнением заголовков". Он сравнивает два поля: длину и указатель. Если оба поля совпадают, строки считаются равными. Для пустой строки это означает сравнение Len = 0 и Data = &emptyStringData.

А теперь посмотрим на len(s) == 0. Функция len() для строк — это не вызов функции, а встроенная операция. Компилятор Go просто читает поле Len из заголовка строки. Это одна инструкция процессора: MOV (переместить значение).

Три способа проверки: мифы и реальность

На Stack Overflow вы найдете три основных подхода. Давайте разберем каждый из них с точки зрения производительности и стиля.

1. len(s) == 0 — "Классический" метод

Как работает: Берет поле Len из заголовка строки и сравнивает его с нулем.

Плюсы:

  • Самая высокая производительность. Это буквально одна-две инструкции процессора. Никаких вызовов функций, никаких сравнений байтов.
  • Идиоматично для Go. Go-сообщество любит этот метод за его простоту и эффективность. Вы часто увидите его в стандартной библиотеке.
  • Понятно. "Если длина строки равна нулю, значит строка пуста" — логично и интуитивно.

Минусы: Субъективно может показаться менее читаемым, чем s == "" для новичков.

2. s == "" — "Естественный" метод

Как работает: Сравнивает два заголовка строк. Для пустой строки это сравнение Len = 0 и Data = &emptyStringData.

Плюсы:

  • Интуитивно понятно для начинающих. Это то, что вы пишете на Python, JavaScript, Java. "Если строка равна пустой строке".
  • Читаемость. Некоторые разработчики считают, что s == "" лучше читается, чем len(s) == 0, хотя это дело вкуса.

Минусы:

  • Микро-оптимизация в минус. Хотя разница в производительности минимальна, s == "" выполняет чуть больше работы. Ему нужно сравнить не только длину, но и указатель. На практике это не играет роли, но в горячих циклах (hot path) может быть заметно.
  • Потенциальная путаница с nil. Начинающие могут подумать, что s == "" не сработает, если s — это nil. Но в Go строка не может быть nil! Её "нулевое значение" (zero value) — это пустая строка. var s string — это s = "".

3. len(s) == 0 — "Перестраховщик" (и это ошибка)

Некоторые разработчики, пришедшие из других языков, пытаются написать len(s) == 0, думая, что это "безопаснее". Это заблуждение. Как мы уже выяснили, len() прекрасно работает с пустой строкой. Нет никакой разницы в безопасности между этими двумя методами.

Бенчмарк: кто быстрее?

Давайте не будем гадать, а напишем простой бенчмарк на Go. Это лучший способ узнать правду.

package main

import (
    "testing"
)

var result bool

func BenchmarkLenCheck(b *testing.B) {
    s := ""
    for i := 0; i < b.N; i++ {
        result = len(s) == 0
    }
}

func BenchmarkEqualCheck(b *testing.B) {
    s := ""
    for i := 0; i < b.N; i++ {
        result = s == ""
    }
}

Запустим его:

go test -bench=. -benchmem

Результаты (на Go 1.21) будут примерно такими:

BenchmarkLenCheck-8       1000000000   0.2372 ns/op   0 B/op   0 allocs/op
BenchmarkEqualCheck-8     1000000000   0.2391 ns/op   0 B/op   0 allocs/op

Вывод: Разница в производительности статистически незначима! Оба метода выполняются за долю наносекунды и не выделяют память. Компилятор Go настолько умён, что оптимизирует оба варианта практически до одного и того же машинного кода.

Однако, если мы посмотрим на код, сгенерированный для s ==

Блоги

Рекомендаций по книгам