Операторы << и >> в Go: битовые сдвиги влево и вправо
Что такое битовые сдвиги в 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 сам оптимизирует это в сдвиг.
Ответов пока нет.
Войдите чтобы оставить ответ