Avatar ·

Операторы << и >> в Go: битовые сдвиги влево и вправо

Всем привет! Я только начал изучать Go и наткнулся на операторы `<<` и `>>`. В документации написано, что это операторы сдвига, но я не совсем понимаю, как они работают на практике. Вот пример кода, который я написал: ```go package main import "fmt" func main() { x := 4 y := x << 2 z := x >> 1 fmt.Println(y, z) // Вывод: 16 2 } ``` Я ожидал, что `x << 2` даст 8 (4 * 2), а `x >> 1` даст 2 (4 / 2). Результат 16 и 2 меня смутил. Почему сдвиг влево на 2 дал умножение на 4, а не на 2? И что вообще означают эти операторы? Также я видел использование `<<` в контексте `1 << iota` при определении констант. Можете объяснить, как это работает? Я пытался разобраться в побитовых операциях, но путаюсь в двоичной системе. Буду благодарен за подробное объяснение с примерами, желательно на русском, потому что английские статьи я понимаю не до конца. <<>>

Что такое битовые сдвиги в Go?

Операторы << (сдвиг влево) и >> (сдвиг вправо) в Go работают с двоичным представлением целых чисел. Они перемещают все биты числа влево или вправо на указанное количество позиций. Это эффективный способ быстрого умножения или деления на степени двойки.

Как работает сдвиг влево (<<)

Каждый сдвиг влево на одну позицию умножает число на 2. Если сдвинуть на n позиций — умножаем на 2n. Ваш пример: x := 4 (двоичное 100). Сдвиг на 2 позиции влево:

  4 = 100 (двоичная)
  4 << 2 = 10000 (двоичная) = 16 (десятичная)

Почему не 8? Потому что вы сдвинули на 2 позиции, а не на 1. 4 << 1 даст 8. Формула: a << n = a * 2n.

Как работает сдвиг вправо (>>)

Сдвиг вправо на одну позицию делит число на 2 (целочисленно). Сдвиг на n позиций — деление на 2n. Ваш пример: x := 4 (двоичное 100). Сдвиг на 1 позицию вправо:

  4 = 100 (двоичная)
  4 >> 1 = 10 (двоичная) = 2 (десятичная)

Формула: a >> n = a / 2n (целочисленное деление).

Практический пример с разными числами

package main

import "fmt"

func main() {
    // Умножение и деление на степени двойки
    a := 10          // 1010 в двоичной
    fmt.Println(a << 1)  // 20 (10 * 2)
    fmt.Println(a << 3)  // 80 (10 * 8)
    fmt.Println(a >> 1)  // 5  (10 / 2)
    fmt.Println(a >> 2)  // 2  (10 / 4 = 2.5, отброшено 0.5)
    
    // Проверка чётности через сдвиг
    b := 7
    fmt.Println(b >> 1)  // 3 (7 / 2 = 3.5, целая часть 3)
}

Использование с iota

Конструкция 1 << iota создаёт последовательность степеней двойки. iota начинается с 0 и увеличивается на 1 для каждого следующего элемента:

package main

import "fmt"

const (
    FlagRead  = 1 << iota  // 1 << 0 = 1
    FlagWrite               // 1 << 1 = 2
    FlagExec                // 1 << 2 = 4
)

func main() {
    fmt.Println(FlagRead, FlagWrite, FlagExec) // Вывод: 1 2 4
    
    // Комбинирование флагов
    permissions := FlagRead | FlagWrite  // 1 | 2 = 3
    fmt.Println(permissions)             // 3
}

Важные замечания

  • Типы: Сдвиг работает только с целыми числами (int, uint, byte и т.д.). Для float64 или string операция не определена.
  • Знаковые числа: Для знаковых типов (int) сдвиг вправо сохраняет знак (арифметический сдвиг). Для беззнаковых — просто заполняет нулями (логический сдвиг).
  • Переполнение: При сдвиге влево биты, выходящие за пределы разрядности типа, теряются. Например, uint8(255) << 1 даст 254, а не 510.
  • Производительность: Сдвиги выполняются намного быстрее умножения/деления на процессорном уровне, поэтому их часто используют в низкоуровневом программировании.

Если вы только начинаете — запомните главное: a << n это a * 2n, а a >> n это a / 2n. Со временем вы привыкнете к двоичной логике и сможете использовать эти операторы для оптимизации кода.

<<>>

Дополнение: практическое применение битовых сдвигов

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

Работа с цветами (RGB)

Часто цвета хранят в одном 32-битном числе: 8 бит на красный, 8 на зелёный, 8 на синий и 8 на альфа-канал. Сдвиги помогают извлекать компоненты:

color := 0xAABBCCDD // ARGB формат
alpha := (color >> 24) & 0xFF // 0xAA
red   := (color >> 16) & 0xFF // 0xBB
green := (color >> 8)  & 0xFF // 0xCC
blue  := color & 0xFF         // 0xDD

Проверка установленных битов

Допустим, у вас есть 8 флагов в одном байте. Проверить, установлен ли третий флаг (22 = 4):

flags := byte(0b10110101) // 0b — двоичный литерал (Go 1.13+)
thirdFlag := (flags >> 2) & 1 // 1 — флаг установлен

Оптимизация в циклах

Если нужно умножать на 2 в цикле, сдвиг работает быстрее:

for i := 1; i < 1024; i <<= 1 { // 1, 2, 4, 8, ... 512
    fmt.Println(i)
}

Совет: не злоупотребляйте сдвигами для умножения/деления, если код и так понятен. Используйте их там, где это действительно нужно для производительности или при работе с битовыми масками. Для обычного кода пишите a * 2, компилятор Go сам оптимизирует это в сдвиг.