Creating 8 bit binary data from 4,3, and 1 bit data in Golang

Viewed 700

I need to form a header (8 bits) using a version (4 bits), count (3 bits), identifier (1 bit). How can I achieve this in Go? For example:

version: 1 (0001)
count: 3 (011)
identifier: 1(1)

Header: 00010111 (23)

I'm doing the following which works but there's a lot of cumbersome code. How can I do this efficiently?

const (
    VersionSize       binary.Bits  = 4
    countSize         binary.Bits  = 3
    IdentifierSize    binary.Bits  = 1
)

type header struct {
    version      uint8 
    count        uint8
    identifier   uint8
}

func main() {
    headerObj := &header{version:1, count:3, identifier:1}
    headerBytes := encode(headerObj)
    // prints [23]
    fmt.PrintLn(headerBytes)
}

func (h *header) encode() []byte {
    var header []byte

    vercountIdBinary := toBinary(h.version, versionSize) + toBinary(h.count,countSize) + toBinary(h.identifier, IdentifierSize)

    vercountIdByte, _ := strconv.ParseInt(vercountIdBinary, 2, 8)
    header = append(header, byte(vercountIdByte))

    return header
}

func toBinary(value interface{}, bitSize binary.Bits) string {
    format := "%0" + strconv.Itoa(int(bitSize)) + "b"
    return fmt.Sprintf(format, value)
}
1 Answers

Packing and unpacking bits into a number can be achieved simply with bit masking and shifting.

For example to pack bits into a number, mask and assign the first, then shift left the result with the bits number or the next data (to make enough room for it). Then mask the 2nd number, and "add" it using bitwise OR. Then shift again with the size of the 3rd number, and repeat.

To unpack: mask the result with the size of the last field, and you got the last number. Shift right the data with the size of the decoded number, and mask with the next (in reverse order) number's size, and you got the number. Repeat this process until you have decoded all numbers.

For example, this packs identifier to most significant bits, count in middle and version to least significant bits, but you may do the opposite order by packing fields in reverse order:

const (
    BitsVersion = 4
    BitsCount   = 3
    BitsId      = 1
)

const (
    MaskVersion = 1<<BitsVersion - 1
    MaskCount   = 1<<BitsCount - 1
    MaskId      = 1<<BitsId - 1
)

type header struct {
    version    uint8
    count      uint8
    identifier uint8
}

func (h *header) ToByte() uint8 {
    var b uint8

    b = h.identifier & MaskId
    b <<= BitsCount
    b |= h.count & MaskCount
    b <<= BitsVersion
    b |= h.version & MaskVersion

    return b
}

func (h *header) ParseByte(b uint8) {
    h.version = b & MaskVersion
    b >>= BitsVersion
    h.count = b & MaskCount
    b >>= BitsCount
    h.identifier = b & MaskId
}

Testing it:

h := &header{
    version:    3,
    count:      2,
    identifier: 1,
}
fmt.Printf("%+v\n", h)

b := h.ToByte()

h2 := &header{}
h2.ParseByte(b)
fmt.Printf("%+v\n", h2)

Which will output (try it on the Go Playground):

&{version:3 count:2 identifier:1}
&{version:3 count:2 identifier:1}

Note: the above example encodes the fields in id-count-version order. The order of fields doesn't matter as long as both the packing and unpacking works with the same order. If you need reverse order (version-count-id), simply reverse the order in which fields are packed / unpacked. Here's how to do that:

func (h *header) ToByte() uint8 {
    var b uint8

    b = h.version & MaskVersion
    b <<= BitsCount
    b |= h.count & MaskCount
    b <<= BitsId
    b |= h.identifier & MaskId

    return b
}

func (h *header) ParseByte(b uint8) {
    h.identifier = b & MaskId
    b >>= BitsId
    h.count = b & MaskCount
    b >>= BitsCount
    h.version = b & MaskVersion
}

This outputs the same. Try this one on the Go Playground.

Note that if you have to do this with multiple data, targeting an io.Writer stream, you may use the github.com/icza/bitio library (disclosure: I'm the author).

Related