1. Біти, прапорці та маски
Якщо ви до цього спокійно жили з +, -, * і if, то бітові оператори спершу виглядають як «таємна мова стародавніх ґоферів». Насправді все простіше: це лише інший погляд на цілі числа. Число можна сприймати не тільки як «сорок два», а й як набір маленьких перемикачів — бітів, які або увімкнено, або вимкнено.
Такий підхід особливо корисний, коли потрібно зберігати кілька незалежних ознак в одному числі й швидко перевіряти, які саме біти увімкнено.
Найчастіший практичний сценарій — прапорці (flags): набір опцій, дозволів або режимів. Наприклад, у користувача можуть бути права read, write, execute. Зберігати три окремі bool можна, але інколи зручніше тримати все в одному числі й керувати бітами.
Двійковий запис прямо в коді: 0b1100 і читабельні маски
Уявіть, що у вас є панель із лампочок. Кожна лампочка — це один біт: 0 означає «вимкнено», а 1 — «увімкнено». Ціле число в пам’яті зберігається як послідовність таких бітів. Наприклад, число 13 у двійковому вигляді — це 1101.
У Go двійкове подання — це не лише спосіб побачити, як виглядає число, а й можливість записувати маски прямо в коді так, щоб їх можна було читати очима.
Для цього в числових літералах є префікси:
- 0b... — двійковий запис (найкраще для прапорців і бітових полів)
- 0x... — шістнадцятковий запис (часто в протоколах і байтах)
- 0o... — вісімковий запис (рідше, але інколи трапляється)
- без префікса — десятковий
Усі ці записи еквівалентні:
const (
a = 12 // десяткова
b = 0b1100 // двійкова
c = 0x0C // шістнадцяткова
d = 0o14 // вісімкова
)
Чому 0b... такий зручний для прапорців? Ви одразу бачите, які біти увімкнено, а не просто число «12». Наприклад, 0b0101 означає, що увімкнені біти 0 і 2.
Щоб довгі маски не перетворювалися на кашу, у числах можна ставити _ для групування бітів — значення не змінюється:
const mask = 0b1111_0000_0011_0101
Так око легко бачить групи, як у байтах і ніблах, а код читається швидше.
Ключове слово дня — маска. Маска — це число, у якому нас цікавлять певні біти. Часто маска містить рівно один увімкнений біт, наприклад 00010000. У такому разі вона представляє один прапорець.
Щоб не напружувати очі, ми будемо активно використовувати форматування %b у fmt.Printf, яке друкує число у двійковому вигляді — тобто «лампочками».
2. Базові операції з прапорцями
Зсув вліво <<: як отримати маску одного прапорця
Коли в Go-коді ви бачите запис 1 << n, це майже завжди про прапорці. Якщо подивитися на це у двійковому вигляді, 1 — це 0b0001. Зсув << n просто переносить цю єдину одиницю вліво: 1 << 3 → 0b1000, 1 << 5 → 0b100000.
Саме тому 1 << n так добре підходить для створення маски одного прапорця. Ідея проста: беремо число 1 (у двійковому вигляді це ...0001) і рухаємо єдину одиничку на n позицій вліво.
Зробімо маленький експеримент і подивімося, як виглядає один біт на різних позиціях. Це допоможе перестати сприймати << як магію й почати бачити в ньому звичайний спосіб обрати лампочку з номером n.
package main
import "fmt"
func main() {
var mask uint = 1 << 3
fmt.Printf("mask=%b (%d)\n", mask, mask) // маска=1000 (8)
}
Тут 1 << 3 дало нам 1000 (у десятковому записі це 8). Це і є маска «увімкнено тільки біт №3».
Практична звичка: для прапорців часто використовують uint, бо це просто біти, без несподіванок зі знаком.
Побітове OR |: як увімкнути прапорець у наборі
Коли ви зберігаєте набір прапорців в одному числі, операцію «увімкнути прапорець» виконують через | (побітове АБО). По суті це означає: «увімкни ось цю лампочку, але решту не чіпай».
Уявімо, що в нас є два прапорці: один у біті 0, інший у біті 2. Спочатку увімкнемо перший, а потім додамо другий — і подивимося, що вийде.
package main
import "fmt"
func main() {
var a uint = 1 << 0 // 0001
var b uint = 1 << 2 // 0100
var flags uint = a | b
fmt.Printf("%b\n", flags) // 101
}
У двійковому вигляді вийшло 101: увімкнені біти 0 і 2. Важливо, що | не складає числа у звичайному сенсі, а об’єднує увімкнені біти.
Побітове AND &: як перевірити, чи увімкнено прапорець
Перевірка прапорця зазвичай виглядає так: (flags & SomeFlag) != 0. Це читається як «перетин набору прапорців із маскою непорожній».
Інакше кажучи, якщо потрібний біт був увімкнений, то після & він залишиться, і результат не буде нулем.
Зараз ми зробимо саме те, що ви найчастіше бачитимете в реальному Go-коді: перевіримо, чи є в користувача право запису.
package main
import "fmt"
func main() {
const (
read uint = 1 << 0 // 001
write uint = 1 << 1 // 010
)
var perms uint = read // тільки читання
fmt.Println(perms&write != 0) // false
fmt.Println(perms&read != 0) // true
}
Зверніть увагу: прапорці майже ніколи не перевіряють через ==. Перевірка perms == read означає «у мене увімкнено рівно read і більше нічого», а не «у мене є read».
Побітове AND NOT &^: як вимкнути прапорець
Оператор &^ у Go читається як «AND NOT», тобто «залиш біти, крім тих, що вказані в масці». Це стандартна операція для вимкнення прапорця без впливу на інші.
Якщо | — це «увімкнути лампочку», то &^ — це «вимкнути лампочку» за маскою.
package main
import "fmt"
func main() {
const (
read uint = 1 << 0 // 001
write uint = 1 << 1 // 010
exec uint = 1 << 2 // 100
)
var perms uint = read | write | exec // 111
perms = perms &^ write
fmt.Printf("%b\n", perms) // 101
}
Після &^ write біт write зник, а решта залишилися.
XOR ^ і унарний ^x: перемикання та інверсія
Символ ^ у Go має два значення — і саме тут новачки зазвичай насуплюються:
- a ^ b — це XOR (виключне АБО), побітове.
- ^a — це побітове НЕ (інверсія всіх бітів).
XOR корисний, коли ви хочете перемкнути біт: якщо він був 0 — стане 1, якщо був 1 — стане 0.
Спочатку XOR:
package main
import "fmt"
func main() {
var x uint = 0b1010
var y uint = 0b1100
fmt.Printf("%b\n", x^y) // 110
}
А тепер перемикання прапорця:
package main
import "fmt"
func main() {
const debug uint = 1 << 0
var flags uint = 0
flags = flags ^ debug // увімкнули debug
flags = flags ^ debug // вимкнули debug
fmt.Println(flags) // 0
}
Унарний ^x — це інверсія всіх бітів. У контексті прапорців це використовують рідше й обережніше, бо ви інвертуєте усі біти числа, а не тільки відомі вам прапорці. Тому для вимкнення конкретних прапорців зазвичай беруть &^.
Зсув вправо >>: як дістати біт і прочитати поле
Зсув вправо >> частіше трапляється, коли потрібно прочитати окремий біт або невелике бітове поле. Наприклад, ви хочете дізнатися значення біта №3: можна зсунути число на 3 вправо й узяти & 1.
>> зсуває потрібний біт у позицію 0, а & 1 забирає тільки його.
package main
import "fmt"
func main() {
var flags uint = 0b10100 // біт 2 увімкнено, біт 4 увімкнено
bit2 := (flags >> 2) & 1
bit3 := (flags >> 3) & 1
fmt.Println(bit2, bit3) // 1 0
}
Це не єдиний спосіб перевірки прапорця (зазвичай простіше flags & mask != 0), але корисно знати, бо подібне трапляється в задачах, де в числі закодовано кілька значень.
3. Пріоритет операторів і дужки
Бітові оператори підпорядковуються пріоритету, і інколи вираз без дужок перетворюється на ребус навіть для автора через два дні. Гарна новина: якщо ви поставите дужки трохи щедріше, ніж потрібно, компілятор не образиться, а колеги скажуть вам дякую.
Особливо небезпечні вирази, у яких змішані &, |, ^ і порівняння з 0. Навіть якщо ви знаєте правила пріоритету, мозок читає такий код повільно.
Тому практичне правило просте: якщо вираз складно прочитати з першого погляду, додавайте дужки або виносьте частини в проміжні змінні.
Приклад охайної перевірки:
package main
import "fmt"
func main() {
const (
read uint = 1 << 0
write uint = 1 << 1
)
perms := read | write
canWrite := (perms & write) != 0
fmt.Println(canWrite) // true
}
4. Налагодження масок: %b, ширина та провідні нулі
Коли ви налагоджуєте прапорці, десятковий вивід (13) майже марний: ви не бачите лампочок. Тому важливо тримати маски читабельними ще в коді — через 0b... і _ — а двійковий вивід використовувати лише як швидку перевірку, коли сумніваєтесь.
А якщо ви все-таки виводите число, двійковий формат (1101) значно корисніший за десятковий. Але в нього є недолік: провідні нулі зазвичай не друкуються, і вам важко зрозуміти, скільки бітів ви взагалі бачите.
Для зручного налагодження краще друкувати фіксовану ширину, наприклад 8 біт, 16 біт або 32 біти. Тоді ви бачите панель перемикачів повністю.
У fmt.Printf це робиться форматами на кшталт %08b (8 символів, заповнення нулями).
package main
import "fmt"
func main() {
var x uint = 5
fmt.Printf("%b\n", x) // 101
fmt.Printf("%08b\n", x) // 00000101
}
5. Мініприклад: права користувача через прапорці
Продовжімо наш навчальний «консольний мінісервіс»: читаємо ввід, обчислюємо результат, друкуємо вивід. Сьогодні додамо до нього просту модель прав доступу: читання, запис і виконання — та навчимося перевіряти, чи дозволено виконати дію.
Тут важливо не лише написати робочий код, а й зробити його читабельним. Ми використаємо type Perm uint, щоб числа стали смисловим типом, і задаватимемо прапорці через 1 << iota, щоб кожен прапорець був степенем двійки, а не «1, 2, 3».
Оголошуємо тип і прапорці
package main
import "fmt"
func main() {
type Perm uint
const (
PermRead Perm = 1 << iota
PermWrite
PermExec
)
var p Perm = PermRead | PermExec
fmt.Printf("perms=%b\n", uint(p)) // perms=101
}
Зверніть увагу на важливу деталь: ми друкуємо uint(p), тому що %b зручніше застосовувати до звичайного числового типу. Сам Perm теж є числом, але так початківцям простіше сприймати.
Читаємо код операції та вирішуємо, чи дозволено
Зробімо дуже простий протокол вводу: користувач вводить поточне число прав, наприклад 5, і номер операції (1=read, 2=write, 3=exec). Ми перевіряємо результат і друкуємо "allowed" або "denied".
Наша мета — зробити так, щоб рядок p&PermWrite != 0 перестав бути страшним.
package main
import "fmt"
func main() {
type Perm uint
const (
PermRead Perm = 1 << iota
PermWrite
PermExec
)
var raw uint
var op int
fmt.Scan(&raw, &op)
p := Perm(raw)
allowed := false
if op == 1 {
allowed = (p & PermRead) != 0
} else if op == 2 {
allowed = (p & PermWrite) != 0
} else if op == 3 {
allowed = (p & PermExec) != 0
}
if allowed {
fmt.Println("allowed")
} else {
fmt.Println("denied")
}
}
Цей код використовує лише те, що ми вже вміємо: ввід через fmt.Scan, умови if/else, константи, типи та побітові оператори.
Режим адміністратора: прибрати право запису
Тепер додамо ще один режим: якщо op == 99, ми приберемо write і виведемо нове значення маски. Це вправа саме на &^.
Багато хто вперше по-справжньому розуміє &^ саме в такому сценарії: він робить рівно це — прибирає потрібний прапорець.
package main
import "fmt"
func main() {
type Perm uint
const (
PermRead Perm = 1 << iota
PermWrite
PermExec
)
var raw uint
var op int
fmt.Scan(&raw, &op)
p := Perm(raw)
if op == 99 {
p = p &^ PermWrite
fmt.Printf("%d %b\n", uint(p), uint(p)) // наприклад: 5 101
return
}
fmt.Println("ok")
}
Тут ми використали return, щоб не ускладнювати решту логіки. Це нормально: ранній вихід часто робить код простішим для читання.
6. Шпаргалка з операторів
Тримати в голові одразу всі оператори важко, особливо якщо ви поки рідко пишете такий код. Таблиця нижче — не для зубріння, а для швидкого «ага, ось що робить цей символ» під час читання коду.
Головне — розуміти типові патерни: увімкнути (|), перевірити (&), вимкнути (&^), створити маску (1 << n).
| Оператор | Назва (за змістом) | Типове застосування для прапорців | Підказка |
|---|---|---|---|
|
зсув вліво | отримати маску одного біта | «поставити 1 на позицію n» |
|
OR | увімкнути прапорці з b у a | «об’єднати лампочки» |
|
AND | перевірити наявність прапорця | «перетин лампочок» |
|
AND NOT | вимкнути один або кілька прапорців за маскою | «вимкнути лампочки з маски» |
|
XOR | перемкнути біти, де вони відрізняються | «перемикання» |
|
побітове НЕ | інвертувати всі біти | «перевернути все» |
|
зсув вправо | «витягнути» біт або поле вниз | «зсунути до молодших бітів» |
7. Типові помилки під час роботи з бітовими операторами
Помилка №1: задавати прапорці як 1, 2, 3, а не як степені двійки.
Це найчастіша логічна помилка. Для набору прапорців кожен прапорець має займати окремий біт: 1, 2, 4, 8... Якщо зробити 1, 2, 3, то прапорець 3 у двійковому вигляді дорівнює 11 і фактично вмикає два біти одразу. У підсумку перевірки та комбінації починають поводитися дивно.
Помилка №2: перевіряти прапорець через ==, а не через &.
Вираз perms == PermRead відповідає на запитання «у мене увімкнено лише читання й нічого більше». А запитання «у мене є читання?» має звучати як (perms & PermRead) != 0. Якщо переплутати ці підходи, то користувач із правами read+write раптом «не має read» за вашою перевіркою.
Помилка №3: плутати ^ як XOR із піднесенням до степеня.
У Go немає оператора піднесення до степеня ^. Символ ^ — це побітовий XOR або побітова інверсія в унарній формі. Якщо ви спробуєте написати 2 ^ 3 і очікувати 8, ви отримаєте XOR-результат.
Помилка №4: використовувати унарний ^mask для вимкнення прапорця й дивуватися результатам.
Технічно можна вимкнути прапорець так: flags = flags & (^mask), але це складніше читати й легко помилитися. У Go для цього є прямий оператор &^, і він набагато чесніше виражає намір: очистити біти за маскою.
Помилка №5: не друкувати двійкове подання під час налагодження й намагатися зрозуміти прапорці за десятковими числами.
Число 13 саме по собі не каже вам, які прапорці увімкнено. А 1101 — каже. Якщо ви налагоджуєте прапорці й не використовуєте %b (а інколи ще й %08b), то ви буквально самі собі ускладнюєте життя.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ