Bitmasking em Go: Uma Técnica Poderosa para Gerenciamento de Opções
Leonan Carvalho
Posted on July 13, 2024
Introdução
Bitmasking é uma técnica eficiente e poderosa utilizada na programação para representar e manipular conjuntos de opções usando operações bitwise. Esta técnica permite armazenar múltiplos estados booleanos em um único valor numérico, onde cada bit representa uma opção distinta. Embora eu tenha começado minha jornada de programação com PHP, onde bitmasking é bastante utilizado, descobri que essa técnica é igualmente poderosa em outras linguagens como C, Java e até mesmo nas linguagens mais modernas como Go.
Neste artigo, vou compartilhar como implementar bitmasking em Go e discutir alguns exemplos práticos baseados na minha experiência.
Conceitos Básicos
O que é Bitmasking?
Bitmasking envolve o uso de operações bitwise para gerenciar conjuntos de flags ou opções. Cada opção é representada por um bit em um valor inteiro, permitindo que múltiplas opções sejam combinadas e verificadas de maneira eficiente através da compactação de dados, permitindo economizar espaço de memória e melhorar o desmpenho de programas críticos.
Operadores Bitwise
Os operadores bitwise mais comuns utilizados em bitmasking são:
-
AND (
&
): Usado para verificar se um bit específico está definido. -
OR (
|
): Usado para definir bits específicos. -
XOR (
^
): Usado para alternar bits específicos. -
NOT (
~
): Usado para inverter todos os bits.
Implementação em Go
Vamos criar uma implementação de bitmasking em Go, utilizando um exemplo de sistema de configuração para uma estrutura chamada Service
.
Usaremos o tipo iota
para definir constantes de opções, onde cada constante representa uma opção específica como um bit único.
package main
import (
"fmt"
)
type ServiceOption int
const (
WITH_OPTION_A ServiceOption = 1 << iota
WITH_OPTION_B
WITH_OPTION_C
)
Mas atenção, com o tipo int
podemos definir somente no máximo 32 opções de flag. Por isso, Ao definir uma flag esteja atento à possibilidade de crescimento desse conjunto.
Se você precisa superar a limitação de 32 flags que um tipo int
permite, você pode considerar algumas alternativas que suportam mais bits. Aqui estão algumas opções:
Inteiros de 64 Bits
Em Go, você pode usar o tipo int64 para representar até 64 flags.
type ServiceOption int64
Usar um Array de Inteiros
Se você precisar de um número ainda maior de flags, pode usar um array ou slice de inteiros. Cada elemento do array pode armazenar 32 ou 64 flags, dependendo do tipo de inteiro usado (int32 ou int64).
type ServiceOption int64
type ServiceOptions [2]int64 // 2 * 64 = 128 flags
const (
WITH_OPTION_A ServiceOption = 1 << iota
WITH_OPTION_B
WITH_OPTION_C
// Continue até 127 (2 * 64 - 1)
)
func (p *ServiceOptions) Set(flag ServiceOption) {
index := flag / 64
bit := flag % 64
p[index] |= 1 << bit
}
func (p *ServiceOptions) Clear(flag ServiceOption) {
index := flag / 64
bit := flag % 64
p[index] &^= 1 << bit
}
func (p *ServiceOptions) Has(flag ServiceOption) bool {
index := flag / 64
bit := flag % 64
return p[index]&(1<<bit) != 0
}
Você também pode criar um tipo personalizado que usa slices ou arrays internamente para armazenar bits, mas torna tudo um pouco mais complexo, então adicionei um exemplo implementação no Go Playground
Atribuindo as flags na estrutura de dados
Ao Definirmos nossa bitmask agora vamos anexá-lo à uma estrutura chamada Service
que incluirá um campo flags
para armazenar as opções combinadas, vamos usar o operador Bitwise| OR
para para definir bits específicos na criação do objeto.
type Service struct {
flags ServiceOption
}
func NewService(flags ...ServiceOption) *Service {
var opts ServiceOption
for _, flag := range flags {
opts |= flag
}
return &Service{
flags: opts,
}
}
Verificando se uma flag existe no bitmask
Com o construtor completo agora só precisamos criar uma forma de verificar se uma determinada opção está definida , vamos implementar o método HasOption com o operador bitwise &
AND para retornar a existência da flag dentro da nossa bitmask de flags.
func (s *Service) HasOption(flag ServiceOption) bool {
return s.flags&flag != 0
}
func main() {
defaultService := NewService()
fmt.Println("Default Service")
fmt.Println("Has Option A:", defaultService.HasOption(WITH_OPTION_A))
fmt.Println("Has Option B:", defaultService.HasOption(WITH_OPTION_B))
modifiedService := NewService(WITH_OPTION_A | WITH_OPTION_B)
fmt.Println("\nModified Service")
fmt.Println("Has Option A:", modifiedService.HasOption(WITH_OPTION_A))
fmt.Println("Has Option B:", modifiedService.HasOption(WITH_OPTION_B))
}
Agora nosso exemplo está completo, https://go.dev/play/p/rcHwLs-rUaA
Exemplo de uso do Iota para definir constantes Enums que representam dias da semana fonte
Exemplos de Uso no mundo real
No exemplo acima nós criamos duas instâncias de um serviço sem muita função, apenas para demostrar como podemos aplicar diferentes flags e com as opções sendo modificadas de acordo com os valores definidos no seu construtor, eliminando a necessidade de diversas flags boleanas e tornando o conjunto de modificadores expansíveis.
Um exemplo clássico de uso de bitmasking é em sistemas de permissões, onde diferentes níveis de acesso (leitura, escrita, execução) são representados por diferentes bits.
type Permission int
const (
Read Permission = 1 << iota
Write
Execute
)
type User struct {
permissions Permission
}
func (u *User) HasPermission(p Permission) bool {
return u.permissions&p != 0
}
func main() {
user := &User{permissions: Read | Write}
fmt.Println("Can Read:", user.HasPermission(Read))
fmt.Println("Can Write:", user.HasPermission(Write))
fmt.Println("Can Execute:", user.HasPermission(Execute))
}
Neste exemplo, podemos ver como é simples e eficiente verificar múltiplas permissões combinando-as em um único valor inteiro.
Vamos supor que eu queira incluir novas permissõs como Delete e Share,
Basta que eu defina novas permissões à minhas constantes:
const (
Read Permission = 1 << iota
Write
Execute
Delete
Share
)
Essas permissões ainda podem ser armazenadas em um banco de dados por exemplo
Vamos assumir que temos uma tabela chamada users com um campo permissions que armazena o valor das permissões usando bitmask.
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT,
permissions INTEGER
);
Como o bitmask é um inteiro, ele será armazenado no banco de dados de forma bem direta, sem muitas complicações, reduzindo tamanhos de tabelas e dados armazenados.
Um Porém cuidado, caso uma permissão seja renomeada ou movida de posição na constante irá mudar o valor inteiro, tornando initulizável o valor armazenado.
No exemplo acima a permissão Read | Write
irá imprimir o valor inteiro 3
. Porém vamos supor que você queira melhorar a legibilidade do seu código adicionando a primeira declaração do iota como um valor vazio, sumindo um usuário sem permissão alguma.
const (
_ Permission = 1 << iota
Read
Write
Execute
)
A permissão Read | Write
agorá irá imprimir o valor 10
ao invés de 3
.
Exemplo permissões de sistema
Configurações de inicialização ou opções de sistema podem ser combinadas e verificadas usando bitmasking para determinar o comportamento do sistema.
type SystemOption int
const (
EnableLogging SystemOption = 1 << iota
EnableDebugging
EnableMetrics
)
type SystemConfig struct {
options SystemOption
}
func (s *SystemConfig) IsEnabled(option SystemOption) bool {
return s.options&option != 0
}
func main() {
config := &SystemConfig{options: EnableLogging | EnableMetrics}
fmt.Println("Logging Enabled:", config.IsEnabled(EnableLogging))
fmt.Println("Debugging Enabled:", config.IsEnabled(EnableDebugging))
fmt.Println("Metrics Enabled:", config.IsEnabled(EnableMetrics))
}
Um exemplo um pouco mais avançado...
O uso de bitwise e bitmasking pode ser encontrado em operações de gráficos computacionais, onde frequentemente manipulamos pixels e cores.
Em gráficos computacionais, as cores são frequentemente representadas por valores RGBA (Red, Green, Blue, Alpha), onde cada componente da cor é armazenado em um byte (8 bits). Podemos usar operações bitwise para manipular essas cores.
O exemplo abaixo mostra como um programa que inverte as cores de uma imagem usando operações bitwise.
package main
import (
"image"
"image/color"
"image/draw"
"image/jpeg"
"image/png"
"log"
"os"
)
// Inverte a cor de um pixel usando operações bitwise
func invertColor(c color.Color) color.Color {
r, g, b, a := c.RGBA()
return color.RGBA{
R: uint8(^r >> 8),
G: uint8(^g >> 8),
B: uint8(^b >> 8),
A: uint8(a >> 8), // Alpha não é invertido
}
}
// Função para inverter as cores de uma imagem
func invertImageColors(img image.Image) image.Image {
bounds := img.Bounds()
invertedImg := image.NewRGBA(bounds)
draw.Draw(invertedImg, bounds, img, bounds.Min, draw.Src)
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
originalColor := img.At(x, y)
invertedColor := invertColor(originalColor)
invertedImg.Set(x, y, invertedColor)
}
}
return invertedImg
}
func main() {
// Abre o arquivo de imagem
file, err := os.Open("input.png")
if err != nil {
log.Fatalf("failed to open: %s", err)
}
defer file.Close()
// Decodifica a imagem
img, err := png.Decode(file)
if err != nil {
log.Fatalf("failed to decode: %s", err)
}
// Inverte as cores da imagem
invertedImg := invertImageColors(img)
// Salva a imagem invertida
outputFile, err := os.Create("output.png")
if err != nil {
log.Fatalf("failed to create: %s", err)
}
defer outputFile.Close()
err = png.Encode(outputFile, invertedImg)
if err != nil {
log.Fatalf("failed to encode: %s", err)
}
log.Println("Image inversion completed successfully")
}
Nesse código a invertColor
recebe uma cor (color.Color) e inverte seus componentes RGB usando a operação bitwise NOT (^). O componente Alpha (A) não é invertido.
c.RGBA() retorna os componentes de cor como valores de 16 bits (0-65535), por isso os componentes são deslocados 8 bits para a direita (>> 8) para serem convertidos para a faixa de 8 bits (0-255).
Desvantagens dessa abodagem
Embora o bitmasking seja extremamente eficiente em termos de desempenho e uso de memória, suas desvantagens em termos de complexidade, legibilidade e manutenção devem ser cuidadosamente consideradas.
- Complexidade: Bitmasking pode ser confuso para programadores iniciantes ou para aqueles que não estão familiarizados com operações bitwise. A manipulação de bits diretamente exige uma compreensão sólida de operações binárias.
- Legibilidade do Código: O código que utiliza bitmasking pode ser menos legível e intuitivo em comparação com outras abordagens. Por exemplo, verificar se um bit específico está definido pode não ser tão claro quanto verificar um campo booleano em uma estrutura de banco de dados.
- Manutenção: Remover as opções ou modificar opções existentes pode ser propenso a erros, especialmente se não houver documentação adequada ou se os valores dos bits não forem gerenciados cuidadosamente.
- Limitações de Tamanho: Dependendo do tipo de dado utilizado (por exemplo, int), há um limite no número de flags que podem ser representadas. Por exemplo, um int de 32 bits pode representar até 32 flags diferentes. Isso pode ser uma limitação em sistemas que necessitam de um grande número de opções.
- Erros Silenciosos: Erros na manipulação de bits podem ser difíceis de diagnosticar e podem não resultar em falhas imediatas ou óbvias. Por exemplo, definir ou limpar o bit errado pode alterar inadvertidamente múltiplas flags, levando a comportamentos inesperados que podem ser difíceis de rastrear.
Conclusão
Bitmasking é uma técnica valiosa para representar e manipular conjuntos de opções de maneira eficiente. Em Go, essa técnica pode ser implementada de forma simples e eficaz, como demonstrado nos exemplos acima. Seja para sistemas de permissões, configurações de sistema ou estados de jogo, bitmasking oferece uma maneira poderosa de gerenciar múltiplas opções com operações bitwise rápidas e eficientes.
Para projetos onde a legibilidade e a facilidade de manutenção são prioridades, ou onde o número de opções é grande, outras técnicas, como estruturas de dados customizadas ou mapas, podem ser mais apropriadas. No entanto, para sistemas onde o desempenho é crítico e o número de opções é manejável, bitmasking continua sendo uma ferramenta poderosa e eficiente.
Se você está vindo de um background em PHP, C, Java ou qualquer outra linguagem, experimentar bitmasking em Go pode oferecer uma nova perspectiva, somando a eficiência e a simplicidade desta técnia ao arsenal de qualquer programador.
Posted on July 13, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.