Build tags e compilação condicional

1. Introdução às Build Tags

Build tags são diretivas de compilação que permitem incluir ou excluir arquivos do processo de build do Go com base em condições específicas. Elas são essenciais para lidar com código dependente de plataforma, funcionalidades experimentais, ou configurações de ambiente.

A sintaxe moderna utiliza o formato //go:build (introduzido no Go 1.17), substituindo o legado // +build. O compilador avalia essas tags antes de compilar cada arquivo, decidindo se ele deve ser processado ou ignorado.

//go:build linux
package main

func getOS() string {
    return "Linux"
}

2. Sintaxe e Operadores Lógicos

As build tags suportam expressões booleanas completas com operadores lógicos:

Tags simples:

//go:build linux
//go:build amd64
//go:build !windows

Operadores combinados:

// Apenas Linux 64 bits ou macOS
//go:build (linux && amd64) || darwin

// Qualquer sistema exceto Windows
//go:build !windows

// Linux ou macOS com arquitetura ARM
//go:build (linux || darwin) && arm64

Parênteses são obrigatórios para agrupar expressões e garantir a precedência correta.

3. Arquivos com Build Tags no Nome

Go oferece uma convenção de nomenclatura que simplifica a separação por SO e arquitetura:

file_linux.go       // Compila apenas no Linux
file_windows.go     // Compila apenas no Windows
file_darwin.go      // Compila apenas no macOS
file_amd64.go       // Compila apenas em AMD64
file_linux_amd64.go // Compila apenas no Linux AMD64

Limitações: Essa convenção só funciona para sistema operacional (GOOS) e arquitetura (GOARCH). Para tags customizadas, é obrigatório usar a diretiva //go:build.

Quando usar cada abordagem:
- Use nome de arquivo para diferenças simples de SO/arquitetura
- Use tags explícitas para combinações complexas ou tags customizadas

4. Aplicações Práticas: Portabilidade entre SOs

Vamos criar um utilitário que obtém informações do sistema de forma portável:

// sysinfo.go - Interface comum
package sysinfo

type Info struct {
    Hostname string
    OS       string
    Uptime   uint64
}

func Get() (*Info, error) {
    return getSysInfo()
}
// sysinfo_linux.go
//go:build linux

package sysinfo

import "syscall"

func getSysInfo() (*Info, error) {
    var info syscall.Sysinfo_t
    if err := syscall.Sysinfo(&info); err != nil {
        return nil, err
    }
    return &Info{
        Hostname: getHostname(),
        OS:       "linux",
        Uptime:   info.Uptime,
    }, nil
}

func getHostname() string {
    name, _ := syscall.Gethostname()
    return name
}
// sysinfo_windows.go
//go:build windows

package sysinfo

import (
    "os"
    "syscall"
)

func getSysInfo() (*Info, error) {
    hostname, _ := os.Hostname()
    return &Info{
        Hostname: hostname,
        OS:       "windows",
        Uptime:   getUptimeWindows(),
    }, nil
}

func getUptimeWindows() uint64 {
    dll := syscall.NewLazyDLL("kernel32.dll")
    proc := dll.NewProc("GetTickCount64")
    ret, _, _ := proc.Call()
    return uint64(ret) / 1000
}

5. Aplicações Práticas: Flags de Compilação Customizadas

Tags customizadas permitem ativar/desativar funcionalidades durante o build:

// logger.go
package logger

type Logger struct {
    verbose bool
}

func New() *Logger {
    return &Logger{}
}

func (l *Logger) Log(msg string) {
    // Implementação padrão silenciosa
}
// logger_debug.go
//go:build debug

package logger

import "fmt"

func (l *Logger) Log(msg string) {
    if l.verbose {
        fmt.Println("[DEBUG]", msg)
    }
}

func (l *Logger) SetVerbose(v bool) {
    l.verbose = v
}
// main.go
package main

import "logger"

func main() {
    log := logger.New()
    log.Log("Aplicação iniciada")
    // Com 'go build -tags debug', exibe a mensagem
}

Compilando com tags:

go build -tags "debug" -o app
go build -tags "debug,integration" -o app  # Múltiplas tags

6. Testes Condicionais com Build Tags

Testes também podem ser condicionais, útil para testes de integração ou específicos de plataforma:

// database_test.go
package database

import "testing"

func TestLocalConnection(t *testing.T) {
    // Teste básico que roda sempre
}
// database_integration_test.go
//go:build integration

package database

import "testing"

func TestDatabaseConnection(t *testing.T) {
    // Teste que requer banco de dados real
    // Só executa com: go test -tags integration
}

Executando testes específicos:

go test -tags integration ./...
go test -tags "integration,linux" ./...

7. Build Tags e o Arquivo go.mod

Build tags interagem com módulos de forma transparente — arquivos excluídos por tags não são compilados, mesmo que estejam no módulo. Isso é útil para:

  • Excluir arquivos de dependências que não são compatíveis com sua plataforma
  • Usar go:generate com tags para gerar código condicional:
//go:generate go run gen.go -tags "production"
//go:build production

package main

Problemas comuns: Conflitos de tags podem ocorrer quando múltiplos arquivos no mesmo pacote têm condições mutuamente exclusivas mas nenhuma cobre todos os casos. Sempre garanta que pelo menos um arquivo seja compilado para cada combinação possível.

8. Boas Práticas e Armadilhas Comuns

Evite duplicação excessiva: Use interfaces para abstrair funcionalidades e mantenha o máximo de código compartilhado:

// storage.go
type Storage interface {
    Save(data []byte) error
    Load() ([]byte, error)
}

Mantenha a legibilidade: Comentários claros sobre o propósito de cada tag:

//go:build (linux && amd64) || darwin
// Este arquivo implementa operações de E/S específicas
// para Linux AMD64 e macOS (Apple Silicon e Intel)

Teste todas as combinações em CI:

# Exemplo de matriz de build no GitHub Actions
matrix:
  goos: [linux, windows, darwin]
  goarch: [amd64, arm64]

Depuração com go list:

# Verifique quais arquivos serão compilados
go list -f '{{.GoFiles}}' -tags "linux,amd64"

Migração de // +build para //go:build:

# Use a ferramenta oficial para migração
gofmt -w *.go

Referências