Operadores, expressões e conversão de tipo

Operadores, expressões e conversão de tipo

Operadores em Go: aritméticos, de comparação, lógicos com curto-circuito e bit a bit, incluindo o &^, a precedência, a divisão inteira que trunca em direção a zero, o overflow silencioso nas conversões, o centavo que o float64 perde e o strconv para ir de texto a número e de volta.
Golang

• • 14 min de leitura

Se variáveis são os substantivos de um programa, operadores são os verbos. Eles definem o que acontece com os dados — como são combinados, comparados, transformados e manipulados. Go tem um conjunto de operadores enxuto e bem definido, sem surpresas ou comportamentos ambíguos.

Operadores aritméticos

Os operadores aritméticos básicos funcionam como esperado para tipos numéricos:

package main

import "fmt"

func main() {
    a := 15
    b := 4

    fmt.Println(a + b)   // 19  — adição
    fmt.Println(a - b)   // 11  — subtração
    fmt.Println(a * b)   // 60  — multiplicação
    fmt.Println(a / b)   // 3   — divisão inteira
    fmt.Println(a % b)   // 3   — resto da divisão (módulo)
}

Um ponto que merece atenção é a divisão inteira. Quando ambos os operandos são inteiros, o resultado também é inteiro — a parte fracionária é descartada sem arredondamento. Para obter o resultado com casas decimais, ao menos um dos operandos precisa ser float64:

a := 15
b := 4

inteiro := a / b                        // 3
decimal := float64(a) / float64(b)      // 3.75

fmt.Println(inteiro, decimal)

Go não possui operador de exponenciação como **. Para potências, usa-se a função math.Pow:

import "math"

resultado := math.Pow(2, 10)  // 1024 — um float64, que o Println mostra sem casas decimais
fmt.Println(resultado)

Operadores de incremento e decremento

Go possui ++ e --, mas com uma diferença importante em relação a C e Java: eles são declarações, não expressões. Isso significa que não retornam valor e só podem ser usados de forma isolada, nunca dentro de uma expressão maior:

x := 10
x++         // válido: x agora é 11
x--         // válido: x agora é 10

// y := x++ — erro de compilação
// if x++ > 5 — erro de compilação

Além disso, Go não possui as formas prefixadas ++x e --x. Apenas o sufixo é permitido.

Operadores de atribuição composta

Go oferece versões combinadas dos operadores aritméticos e bitwise com atribuição:

x := 10

x += 5   // x = x + 5  → 15
x -= 3   // x = x - 3  → 12
x *= 2   // x = x * 2  → 24
x /= 4   // x = x / 4  → 6
x %= 4   // x = x % 4  → 2

fmt.Println(x) // 2

Operadores de comparação

Operadores de comparação sempre retornam um valor bool. São usados principalmente em estruturas de controle:

a := 10
b := 20

fmt.Println(a == b)  // false — igualdade
fmt.Println(a != b)  // true  — diferença
fmt.Println(a < b)   // true  — menor que
fmt.Println(a > b)   // false — maior que
fmt.Println(a <= b)  // true  — menor ou igual
fmt.Println(a >= b)  // false — maior ou igual

Em Go, comparações entre tipos diferentes são proibidas pelo compilador. Não é possível comparar um int com um float64 diretamente — a conversão precisa ser explícita antes.

Operadores lógicos

Usados para combinar expressões booleanas:

t := true
f := false

fmt.Println(t && f)  // false — E lógico (ambos precisam ser true)
fmt.Println(t || f)  // true  — OU lógico (pelo menos um precisa ser true)
fmt.Println(!t)      // false — negação

Go utiliza avaliação em curto-circuito. No operador &&, se o primeiro operando for false, o segundo não é avaliado. No operador ||, se o primeiro for true, o segundo é ignorado. Esse comportamento é importante quando o segundo operando é uma chamada de função com efeitos colaterais ou que pode causar pânico.

func perigosa() bool {
    fmt.Println("função executada")
    return true
}

resultado := false && perigosa()
// "função executada" NÃO é impresso — curto-circuito
fmt.Println(resultado) // false

Operadores bitwise

Para manipulação em nível de bits, Go oferece o conjunto completo de operadores bitwise:

a := 0b1100  // 12 em binário
b := 0b1010  // 10 em binário

fmt.Println(a & b)   // 0b1000 = 8  — AND bitwise
fmt.Println(a | b)   // 0b1110 = 14 — OR bitwise
fmt.Println(a ^ b)   // 0b0110 = 6  — XOR bitwise
fmt.Println(a &^ b)  // 0b0100 = 4  — AND NOT (bit clear)
fmt.Println(a << 1)  // 0b11000 = 24 — deslocamento à esquerda
fmt.Println(a >> 1)  // 0b0110 = 6  — deslocamento à direita

O operador &^ é exclusivo do Go e não tem equivalente direto em C ou Java. Ele limpa os bits do operando esquerdo que estão definidos no operando direito.

Precedência de operadores

Quando múltiplos operadores aparecem em uma expressão, Go segue uma ordem de precedência definida. Do maior para o menor nível:

Nível Operadores
5 (maior) * / % << >> & &^
4 + - | ^
3 == != < <= > >=
2 &&
1 (menor) ||

A recomendação prática é usar parênteses sempre que a precedência não for óbvia. Código claro é preferível a código que exige que o leitor memorize tabelas:

// Ambíguo para o leitor:
resultado := 2 + 3*4 - 1

// Explícito e sem dúvida:
resultado := 2 + (3 * 4) - 1

Conversão de tipos em profundidade

Como visto no artigo Variáveis, tipos primitivos e declaração curta, Go não realiza conversões implícitas. Toda conversão entre tipos diferentes deve ser escrita explicitamente pelo programador. A sintaxe é sempre Tipo(valor).

Conversões numéricas:

var i int = 42
var f float64 = float64(i)
var u uint = uint(f)

fmt.Println(i, f, u) // 42 42 42

Atenção com truncamento. Converter float64 para int descarta a parte decimal sem arredondamento:

x := 3.99
y := int(x)
fmt.Println(y) // 3 — não 4

Conversão entre inteiros de tamanhos diferentes. Converter um valor grande para um tipo menor pode causar overflow silencioso:

var grande int32 = 300
var pequeno int8 = int8(grande)
fmt.Println(pequeno) // 44 — overflow! 300 não cabe em int8

Go não emite erro em tempo de execução para overflow em conversões — o valor simplesmente é truncado. Essa é uma armadilha que exige atenção.

Conversão entre string e slice de bytes:

s := "Olá, Go"
b := []byte(s)         // string → []byte
s2 := string(b)        // []byte → string

fmt.Println(b)         // [79 108 195 161 44 32 71 111]
fmt.Println(s2)        // Olá, Go

Essa conversão é muito comum ao trabalhar com I/O, redes e manipulação de texto.

Conversão entre string e slice de runes:

s := "Olá"
r := []rune(s)
fmt.Println(len(s))    // 4 bytes — o 'á' ocupa 2 bytes em UTF-8
fmt.Println(len(r))    // 3 runes — 3 caracteres visuais

Quando a contagem de caracteres visuais importa, sempre converta para []rune antes de usar len.

O pacote strconv: conversões entre string e tipos numéricos

Converter números em strings e vice-versa é uma operação frequente. O pacote strconv da biblioteca padrão é a ferramenta correta para isso — nunca use conversão direta string(numero) com inteiros, pois o resultado é inesperado:

fmt.Println(string(65)) // "A" — não "65"!

O que acontece acima é que Go interpreta 65 como um ponto de código Unicode e retorna o caractere correspondente. O compilador aceita a conversão, mas o go vet a denuncia quando o número vem de uma variável: conversion from int to string yields a string of one rune, not a string of digits. Quando o caractere é mesmo o que se quer, a forma que deixa a intenção clara é string(rune(65)). Para converter o número 65 na string "65", use strconv:

import (
    "fmt"
    "strconv"
)

func main() {
    // int → string
    n := 42
    s := strconv.Itoa(n)
    fmt.Println(s)          // "42"
    fmt.Printf("%T\n", s)  // string

    // string → int
    valor, err := strconv.Atoi("123")
    if err != nil {
        fmt.Println("Erro na conversão:", err)
        return
    }
    fmt.Println(valor + 1)  // 124

    // float64 → string
    f := 3.14159
    fs := strconv.FormatFloat(f, 'f', 2, 64)
    fmt.Println(fs)  // "3.14"

    // string → float64
    fv, err := strconv.ParseFloat("2.718", 64)
    if err == nil {
        fmt.Println(fv)  // 2.718
    }
}

A função strconv.Atoi retorna dois valores: o inteiro convertido e um erro. Esse padrão de retorno duplo é o idioma central do Go para tratamento de falhas e será explorado em profundidade nos próximos módulos.

Expressões e o pacote fmt

O pacote fmt oferece funções para formatar e imprimir expressões com precisão:

nome := "Ricardo"
idade := 35
altura := 1.78

fmt.Printf("Nome: %s\n", nome)
fmt.Printf("Idade: %d\n", idade)
fmt.Printf("Altura: %.2f\n", altura)
fmt.Printf("Tipo de idade: %T\n", idade)

// Formatação para string sem imprimir
s := fmt.Sprintf("Usuário: %s, %d anos", nome, idade)
fmt.Println(s)

Os verbos de formatação mais usados são %d para inteiros, %f para floats, %s para strings, %t para booleanos, %T para o tipo da variável e %v para o valor padrão de qualquer tipo.

Os operadores do Go cabem numa página e quase não têm casos especiais: ++ é instrução e não expressão, não há exponenciação, e toda mistura de tipos exige conversão escrita à mão. O cuidado maior está nas bordas — a divisão inteira que trunca em direção a zero, o int8 que estoura sem aviso, o float64 que não guarda 19,99 com exatidão — e em saber qual ferramenta converte o quê: T(valor) entre números, strconv entre número e texto.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Uma função decide se um número é ímpar com n%2 == 1. Nos testes com 3, 7 e 11 ela acerta; em produção, com saldos negativos, -7 é classificado como par. Explique o resultado e escreva o teste certo.

Ver resposta

✓ Resposta: Em Go, a divisão inteira trunca em direção a zero, e o resto tem o mesmo sinal do dividendo. Por isso -7/2 vale -3, e não −4, e -7%2 vale -1, e não 1; já 7%-2 vale 1, porque o sinal do divisor não importa. Como -1 == 1 é falso, todo negativo ímpar cai no ramo do par. O teste que funciona para qualquer sinal compara com zero, n%2 != 0, já que o resto de um ímpar é 1 ou −1, mas nunca 0. Quem vem do Python estranha, porque lá -7 % 2 dá 1 (o resto segue o sinal do divisor). A regra do Go é a mesma do C e do Java, e a lição se estende a qualquer uso de % com números que podem ser negativos, como índices circulares: (i%n + n) % n é a forma de garantir um resultado entre 0 e n−1.

Exercício 2

Um relatório calcula a média dividindo o total pela quantidade de registros. Quando o filtro não devolve nenhum registro, o serviço cai com um pânico se os valores são int, mas segue em frente imprimindo NaN se são float64. Explique as duas reações e qual é pior.

Ver resposta

✓ Resposta: Divisão inteira por zero não tem resultado possível, e o Go para o programa com runtime error: integer divide by zero. Se o zero for uma constante escrita no código, como em 10 / 0, nem chega a compilar: invalid operation: division by zero. Com ponto flutuante, a divisão segue o padrão IEEE 754 e não falha: 1/z dá +Inf, -1/z dá -Inf e 0/z, que é o caso da média de um total zero por zero registros, dá NaN. O float é o pior dos dois, porque o NaN se espalha em silêncio por toda conta seguinte e aparece no relatório, na API ou no banco como se fosse um dado. Detectá-lo também é contraintuitivo: nan == nan é false, e o teste certo é math.IsNaN(x). A correção de verdade é não dividir: conferir se a quantidade é zero antes e decidir, explicitamente, o que a média de um conjunto vazio significa para o negócio.

Exercício 3

Um carrinho guarda o preço como float64 e converte para centavos com int(preco * 100) antes de enviar ao gateway de pagamento. Um produto de R$ 19,99 é cobrado como 1998 centavos. De onde sai esse centavo perdido, e qual é a correção?

Ver resposta

✓ Resposta: O número 19,99 não tem representação exata em binário. O float64 guarda o valor mais próximo, e a multiplicação por 100 dá 1998.9999999999998. A conversão int(...) descarta a parte fracionária sem arredondar, como o artigo mostra com int(3.99), e o resultado vira 1998. O remendo imediato é arredondar antes de converter: int(math.Round(preco * 100)) dá 1999. A correção de fundo é não usar ponto flutuante para dinheiro. O valor deve entrar no sistema já em centavos, como inteiro, ou ser tratado com um tipo decimal de biblioteca, e o float64 fica para o que é medida, não contabilidade. Uma curiosidade ajuda a entender o fenômeno: 0.1+0.2 == 0.3 escrito com constantes dá true, porque as constantes do Go têm precisão exata em tempo de compilação, mas com variáveis a, b := 0.1, 0.2, o a+b == 0.3 dá false, e a+b imprime 0.30000000000000004. O mesmo cálculo muda de resultado conforme seja feito pelo compilador ou pelo processador.

Exercício 4

As permissões de um arquivo são guardadas num inteiro, com Ler = 1, Escrever = 2 e Executar = 4, definidas com 1 << iota. Um usuário tem as três (valor 7). Escreva a expressão que retira só a escrita, a que testa se a escrita está presente e diga por que perm - Escrever é uma solução perigosa.

Ver resposta

✓ Resposta: Para retirar a escrita, use perm &^ Escrever, o AND NOT do artigo: ele zera no operando da esquerda os bits que estão ligados no da direita, e 7 vira 5, com leitura e execução mantidas. Para testar, use perm&Escrever != 0, que isola o bit e compara com zero, e aqui dá true. Os parênteses não fazem falta, porque & tem precedência maior que !=, como mostra a tabela do artigo. Já perm - Escrever só funciona se o bit estiver ligado: aplicado a um usuário que não tinha escrita, como 5, dá 3, que é leitura mais escrita, justamente a permissão que se queria tirar. O &^ é idempotente: aplicado duas vezes, ou num valor que não tinha o bit, não muda nada. É essa propriedade que faz dos operadores de bit a ferramenta certa para flags, e não a aritmética.

Exercício 5

Numa revisão aparece if p.x > 0 && p != nil, onde p é um ponteiro para struct que pode vir nulo. O autor argumenta que "o && confere as duas coisas, então tanto faz a ordem". O que acontece quando p é nil, e como o curto-circuito resolve?

Ver resposta

✓ Resposta: A ordem importa, e muito. O && avalia da esquerda para a direita e só olha o segundo operando se o primeiro for verdadeiro. Na ordem do autor, p.x é avaliado primeiro e, com p nulo, o programa para com invalid memory address or nil pointer dereference antes mesmo de chegar à verificação. Invertendo, p != nil && p.x > 0, o primeiro teste dá false, o segundo nunca é avaliado e a expressão inteira vale false, sem pânico. É o mesmo mecanismo que o artigo demonstra com a função perigosa(): o curto-circuito não é só otimização, é o que permite escrever a guarda e o uso na mesma expressão. A regra de leitura é pôr sempre a condição que protege antes da que depende dela, e o mesmo vale para o ||, como em p == nil || p.x == 0.

Comentários

Mais em Golang

Linting, formatação e análise estática: gofmt, golangci-lint
Linting, formatação e análise estática: gofmt, golangci-lint

Qualidade de código em Go: gofmt, goimports, go vet e staticcheck com exemplos…

Funções: declaração, múltiplos retornos e variádicas
Funções: declaração, múltiplos retornos e variádicas

Funções em Go: parâmetros agrupados, múltiplos retornos e o padrão (resultado…

Padrões de concorrência: fan-out, fan-in e pipelines
Padrões de concorrência: fan-out, fan-in e pipelines

Padrões de concorrência em Go: pipelines com cancelamento, fan-out e fan-in…