Побитовые операции

Краткое описание

В этом уроке знакомимся с побитовыми операциями в Go — нишевым инструментом для работы с двоичным представлением чисел. Разбираем операции AND, OR, XOR и битовые сдвиги. Автор честно предупреждает: в реальной разработке это используется редко, но знать о существовании нужно.

Ключевые концепции

Двоичное представление чисел

Любое десятичное число можно представить в двоичном формате:

5 (десятичное) = 0101 (двоичное)
3 (десятичное) = 0011 (двоичное)

Все побитовые операции работают с двоичным представлением.

Побитовое И (AND) — оператор &

Правило: результат 1 только если оба бита равны 1, иначе 0.

1 & 1 = 1
1 & 0 = 0
0 & 1 = 0
0 & 0 = 0

Пример:

result := 5 & 3

// 0101 (5)
// 0011 (3)
// ----
// 0001 (1)

fmt.Println(result)  // 1

Применение: маскирование битов, проверка флагов.

Побитовое ИЛИ (OR) — оператор |

Правило: результат 0 только если оба бита равны 0, иначе 1.

0 | 0 = 0
0 | 1 = 1
1 | 0 = 1
1 | 1 = 1

Пример:

result := 5 | 3

// 0101 (5)
// 0011 (3)
// ----
// 0111 (7)

fmt.Println(result)  // 7

Применение: установка флагов, объединение битовых масок.

Побитовое исключающее ИЛИ (XOR) — оператор ^

Правило: результат 1 если биты разные, 0 если одинаковые.

0 ^ 0 = 0
1 ^ 1 = 0
0 ^ 1 = 1
1 ^ 0 = 1

Пример:

result := 5 ^ 3

// 0101 (5)
// 0011 (3)
// ----
// 0110 (6)

fmt.Println(result)  // 6

Применение: переключение битов, простое шифрование, поиск уникальных элементов.

Побитовый сдвиг влево («) — умножение на 2

Суть: все биты сдвигаются влево, справа добавляются нули.

result := 5 << 1

// 0101 (5) << 1
// 1010 (10)

fmt.Println(result)  // 10

Математика: x << n = x * 2^n

5 << 1   // 5 * 2¹ = 10
5 << 2   // 5 * 2² = 20
5 << 3   // 5 * 2³ = 40

Применение: быстрое умножение на степени двойки.

Побитовый сдвиг вправо (») — деление на 2

Суть: все биты сдвигаются вправо, младший бит отбрасывается.

result := 5 >> 1

// 0101 (5) >> 1
// 0010 (2)

fmt.Println(result)  // 2

Математика: x >> n = x / 2^n (целочисленное деление)

128 >> 1   // 128 / 2¹ = 64
128 >> 2   // 128 / 2² = 32
128 >> 3   // 128 / 2³ = 16

Применение: быстрое деление на степени двойки.

Практические примеры

Базовые побитовые операции

package main

import "fmt"

func main() {
    var a int = 5  // 0101
    var b int = 3  // 0011

    // Побитовое И
    fmt.Printf("%d & %d = %d\n", a, b, a&b)  // 5 & 3 = 1

    // Побитовое ИЛИ
    fmt.Printf("%d | %d = %d\n", a, b, a|b)  // 5 | 3 = 7

    // Побитовое XOR
    fmt.Printf("%d ^ %d = %d\n", a, b, a^b)  // 5 ^ 3 = 6
}

Побитовые сдвиги

value := 5  // 0101

// Сдвиг влево (умножение на 2)
fmt.Printf("%d << 1 = %d\n", value, value<<1)  // 5 << 1 = 10
fmt.Printf("%d << 2 = %d\n", value, value<<2)  // 5 << 2 = 20
fmt.Printf("%d << 3 = %d\n", value, value<<3)  // 5 << 3 = 40

// Сдвиг вправо (деление на 2)
value = 128
fmt.Printf("%d >> 1 = %d\n", value, value>>1)  // 128 >> 1 = 64
fmt.Printf("%d >> 2 = %d\n", value, value>>2)  // 128 >> 2 = 32

Составные побитовые операторы

x := 5

x &= 3   // x = x & 3  →  1
x |= 7   // x = x | 7  →  7
x ^= 2   // x = x ^ 2  →  5
x <<= 1  // x = x << 1 →  10
x >>= 2  // x = x >> 2 →  2

Важные моменты

1. Редко используется на практике

Автор честно предупреждает: в большинстве backend-задач (работа с JSON, API, базы данных) побитовые операции почти не нужны.

2. Работает одинаково во всех языках

Побитовые операции — это фундамент двоичной математики. Работают идентично в Go, C#, Java, JavaScript и других языках.

3. Не тратьте много времени

Достаточно знать, что они существуют. При необходимости всегда можно вернуться и изучить подробнее.

4. Применение в нишевых задачах

  • Работа с правами доступа (флаги)
  • Оптимизация памяти (битовые маски)
  • Криптография
  • Сетевые протоколы
  • Низкоуровневое программирование

5. Сдвиги быстрее умножения/деления

На уровне процессора сдвиг выполняется за одну инструкцию, что быстрее умножения/деления. Но современные компиляторы сами оптимизируют x * 2 в x << 1.

6. Одинаковые на всех архитектурах

Побитовые операции работают одинаково на современных процессорах (x86, ARM). Исключение — квантовые компьютеры, но это другая история.

Best Practices

1. Используйте для флагов и прав

const (
    FlagA = 1 << 0  // 1
    FlagB = 1 << 1  // 2
    FlagC = 1 << 2  // 4
)

flags := FlagA | FlagC  // установить A и C
if flags&FlagA != 0 {   // проверить A
    // флаг установлен
}

2. Комментируйте побитовый код

// Маскируем младшие 8 бит
value &= 0xFF

// Устанавливаем 5-й бит
value |= (1 << 5)

3. Не оптимизируйте преждевременно

// Не нужно писать так ради "оптимизации"
result := x << 3  // x * 8

// Пишите понятно, компилятор сам оптимизирует
result := x * 8

4. Используйте константы для читаемости

// Плохо
permissions := 7  // что это значит?

// Хорошо
const (
    Read    = 1 << 0
    Write   = 1 << 1
    Execute = 1 << 2
)
permissions := Read | Write | Execute  // понятно!

Что запомнить

  • Побитовые операции работают с двоичным представлением чисел
  • & (AND): 1 только если оба бита 1
  • | (OR): 0 только если оба бита 0
  • ^ (XOR): 1 если биты разные
  • << (сдвиг влево): умножение на 2^n
  • >> (сдвиг вправо): деление на 2^n
  • Составные операторы: &=, |=, ^=, <<=, >>=
  • Редко используется в обычной разработке
  • Основное применение: флаги, права доступа, битовые маски
  • Работает одинаково во всех языках программирования
  • Не нужно глубоко изучать — достаточно знать о существовании
  • При необходимости можно вернуться и изучить детально
  • Не стоит переживать, если тема показалась сложной

Полезные ссылки