В системном программировании постоянно приходится производить манипуляции с битами и байтами. Именно о таких манипуляциях пойдет речь в этой статье.

Типы

В Go есть типы, которые позволяют точно задать размер числовой переменной:

  • uint8 – 1 байт
  • uint16 – 2 байта
  • uint32 – 4 байта
  • uint64 – 8 байт

Префикс "u" означает, что значение переменной беззнаковое, то есть 0, 1, 2 и так далее.

Без префикса "u" число будет знаковым, для int8 от -128 до 127. Отрицательные числа кодируются дополнительным кодом.

Типы int и uint лучше не использовать, так как на разных платформах они имеют разный размер, обычно 32 бита или 64 бита. Тип переменной нужно прописывать явно:

var x uint16 = 123

Если не указать тип uint16, то переменная будет типа int.

Поскольку тип uint8 хранит байт, то для этого типа есть синоним – byte. То есть byte и uint8 – это одно и тоже. Там, где нужно передать byte, можно передавать uint8, и наоборот, ошибки не будет.

Набор байт можно представить ввиде массива:

b := []byte{0, 1, 2, 3}

Многобайтовые последовательности можно хранить как в перменных типа uintN, так и в массивах. Но при использовании uintN нужно задуматься о порядке байт: little-endian или big-endian. Порядок байт зависит от платформы. Например, переменная типа uint16 содержит число, состоящее их 2-x байт, в памяти эти два байта могут располагаться по-разному, в зависимости от платформы (процессора, операционной системы). При переборе ячеек памяти, младший байт может идти перед старшим, а может наоборот. Чаще всего младший байт идет вперед, такой порядок называется little-endian.

В пакете encoding/binary есть методы для преобразования uintN в массив байт и обратно.

Функции для преобразования числа (uint16) в массив байт:

binary.LittleEndian.Uint16([]byte) uint16
binary.BigEndian.Uint16([]byte) uint16
binary.NativeEndian.Uint16([]byte) uint16

Функции для преобразования массива байт в число (uint16):

binary.LittleEndian.PutUint16([]byte, uint16)
binary.BigEndian.PutUint16([]byte, uint16)
binary.NativeEndian.PutUint16([]byte, uint16)

Числа

Go позволяет записывать числа в двоичном и шестнадцетиричном формате. А так же в восьмеричном (но кому он нужен?).

Для удобства восприятия, разряды можно разделять подчеркиванием.

Примеры:

  • двоичное число: 0b1010_1111
  • шестнадцетиричное число: 0xF010_1234

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

Операция Пример
И result = a & b
ИЛИ result = a | b
Ислючающее ИЛИ result = a ^ b
Инверсия result = ^a
И-НЕ result = a &^ b
Сдвиг влево result = a << b
Сдвиг вправо result = a >> b

Эти операции в сочетании с присваиванием:

Операция Эквивалент
b &= a b = b & a
b |= a b = b | a
b ^= a b = b ^ a
b &^= a b = b &^ a
b >>= a b = b >> a
b <<= a b = b << a

В отличии от языка C в Go появилась новая операция – И-НЕ. Это операция И, но с инверсий второго аргумента, то есть "a И (НЕ b)". Такая операция часто используется для обнуления бита:

a &^= 1<<n

Пример. В результате следующей операции в переменной "a" биту под номером "n" будет присвоен 0.

a := 0b1111
a &^= (1 << 2) // Результат: 1011

Биты считаются с нуля справа налево, как и в Си.

Строковое представление

Различные байтовые и битовые представления можно выводить в консоль или получать ввиде строки, с помощью форматирования строк:

// печатает в консоль
fmt.Printf(format, a, b, c) 

// возвращает строку
fmt.Sprintf(format, a, b, c)

format – форматная строка, определяет формат результирующей строки.

// Напечатает "1 2 3"
fmt.Printf("%d %d %d", 1, 2, 3)

В форматной строке можно использовать следующий подстановки:

  • %x – данные в шестнадцетиричном виде, буквы в нижнем регистре (a-f)
  • %X – данные в шестнадцетиричном виде, буквы в верхнем регистре (A-F)
  • %#b, %#x или %#X – аналогично предыдущим, но благодаря решетке, в начале добавляется префикс 0b, 0x или 0X соответственно.

С помощью этих подстановок можно выводить как обычные числа так и массивы:

// Рузультат: "AB"
fmt.Printf("%X", 0xAB)
// Рузультат: "ABCD"
fmt.Printf("%X", []byte{0xAB, 0xCD})

Зафиксировать длину числа в строке и дополнить число ведущими нулями можно с помощью подстанвоки "%0w", где w – длина строкового представления числа.

// Рузультат: "00AB"
fmt.Printf("%04X", 0xAB)

Без нуля "0" строка будет дополняться пробелами:

fmt.Printf("%4X", 0xAB) // Рузультат: " AB"