Variáveis, tipos primitivos e declaração curta

Variáveis, tipos primitivos e declaração curta

Variáveis em Go: var, := e const, o valor zero de cada tipo, iota, inteiros com e sem sinal, float, byte e rune, a conversão explícita que estoura em silêncio, o sombreamento que o := esconde dentro de blocos e por que len("ação") vale 6, e não os 4 esperados.
Golang

• • 13 min de leitura

 

O sistema de tipos do Go é sua primeira linha de defesa! Go é uma linguagem estaticamente tipada. Isso significa que o tipo de cada variável é conhecido em tempo de compilação, e o compilador rejeita qualquer operação que viole as regras de tipos antes mesmo do programa ser executado. Esse comportamento elimina uma classe inteira de erros que em linguagens dinamicamente tipadas só aparecem em produção.

Ao mesmo tempo, Go oferece inferência de tipos — em muitos casos você não precisa escrever o tipo explicitamente, pois o compilador consegue deduzi-lo a partir do valor atribuído. O resultado é um código seguro sem ser verboso.

Declarando variáveis com var

A forma mais explícita de declarar uma variável em Go usa a palavra-chave var:

package main

import "fmt"

func main() {
    var nome string
    var idade int
    var altura float64
    var ativo bool

    nome = "Ricardo"
    idade = 35
    altura = 1.78
    ativo = true

    fmt.Println(nome, idade, altura, ativo)
}

Quando uma variável é declarada sem valor inicial, Go automaticamente atribui o valor zero do tipo correspondente:

  • int → 0
  • float64 → 0.0
  • string → "" (string vazia)
  • bool → false
  • ponteiros, slices, maps, funções → nil

Esse comportamento é intencional e previsível. Em Go não existe o conceito de variável "não inicializada" com valor indefinido — toda variável tem um valor zero bem definido.

Declaração com valor inicial

É possível declarar e inicializar na mesma linha:

var nome string = "Ricardo"
var idade int = 35

Quando o valor inicial é fornecido, o tipo pode ser omitido — o compilador infere:

var nome = "Ricardo"   // inferido como string
var idade = 35         // inferido como int
var altura = 1.78      // inferido como float64
var ativo = true       // inferido como bool

A declaração curta com :=

Dentro de funções, a forma mais comum e idiomática de declarar variáveis em Go é o operador :=. Ele declara e inicializa ao mesmo tempo, com inferência de tipo automática:

package main

import "fmt"

func main() {
    nome := "Ricardo"
    idade := 35
    altura := 1.78
    ativo := true

    fmt.Println(nome, idade, altura, ativo)
}

O := só pode ser usado dentro de funções. No escopo de pacote (fora de funções), apenas var é permitido. Essa é uma regra que confunde iniciantes — vale fixá-la desde já.

Outro detalhe importante: o := exige que pelo menos uma das variáveis do lado esquerdo seja nova. O código abaixo compila porque b é nova, mesmo que a já exista:

a := 10
a, b := 20, 30  // válido: b é nova
fmt.Println(a, b)

Declaração múltipla

Go permite declarar várias variáveis de uma vez, o que é útil para agrupar variáveis relacionadas:

var (
    servidor string = "localhost"
    porta    int    = 5432
    ssl      bool   = true
)

Ou com declaração curta:

x, y, z := 10, 20, 30
fmt.Println(x, y, z)

Constantes com const

Quando um valor não deve mudar durante a execução do programa, use const em vez de var:

const Pi = 3.14159
const MaxTentativas = 5
const NomeDaAplicacao = "MinhaAPI"

Constantes em Go são avaliadas em tempo de compilação. Elas podem ser de tipos numéricos, string ou bool. O valor precisa ser conhecido antes da execução, por isso uma chamada de função comum não serve: const c = strings.ToUpper("a") é recusado com is not constant. A exceção são algumas funções embutidas aplicadas a constantes, que o compilador resolve sozinho — const n = len("abc") compila e vale 3.

O identificador iota

Go oferece um recurso elegante para criar sequências de constantes inteiras relacionadas, chamado iota. Dentro de um bloco const, iota começa em zero e incrementa a cada linha:

const (
    Domingo = iota  // 0
    Segunda         // 1
    Terca           // 2
    Quarta          // 3
    Quinta          // 4
    Sexta           // 5
    Sabado          // 6
)

fmt.Println(Domingo, Segunda, Sabado) // 0 1 6

É possível usar expressões com iota:

const (
    KB = 1 << (10 * (iota + 1))  // 1024
    MB                            // 1048576
    GB                            // 1073741824
)

Tipos primitivos em detalhe

Go possui um conjunto rico de tipos numéricos, o que permite escolher o mais adequado para cada situação.

Inteiros com sinal:

var a int8   = 127          // -128 a 127
var b int16  = 32767        // -32768 a 32767
var c int32  = 2147483647
var d int64  = 9223372036854775807
var e int    = 100          // tamanho depende da arquitetura (32 ou 64 bits)

Inteiros sem sinal:

var a uint8  = 255
var b uint16 = 65535
var c uint32 = 4294967295
var d uint64 = 18446744073709551615
var e uint   = 100

Ponto flutuante:

var f32 float32 = 3.14
var f64 float64 = 3.141592653589793

Na prática, float64 é o tipo padrão para números decimais em Go. Use float32 apenas quando o consumo de memória for uma preocupação concreta.

Números complexos:

var c1 complex64  = 1 + 2i
var c2 complex128 = 3.5 + 4.2i

Booleano:

var verdadeiro bool = true
var falso bool = false

String:

var saudacao string = "Olá, mundo"

Strings em Go são imutáveis e, por baixo, são uma sequência de bytes; como o código-fonte é UTF-8, os literais também são. Caracteres como ã, ç e é são armazenados e impressos corretamente sem configuração nenhuma — mas len conta bytes, não letras: len("ação") vale 6, e utf8.RuneCountInString("ação") vale 4. Indexar também devolve byte: s[1] nessa string é 195, o primeiro dos dois bytes do ç.

byte e rune:

var b byte = 'A'    // alias para uint8, representa um byte
var r rune = 'ã'    // alias para int32, representa um ponto Unicode

A distinção entre byte e rune é importante ao trabalhar com strings que contêm caracteres multibyte. Um caractere ã, por exemplo, ocupa 2 bytes em UTF-8, mas corresponde a um único rune.

Conversão de tipos

Go não faz conversões implícitas entre tipos numéricos. Toda conversão precisa ser explícita:

var inteiro int = 42
var flutuante float64 = float64(inteiro)
var pequeno int8 = int8(inteiro)

fmt.Println(inteiro, flutuante, pequeno)

Tentar somar um int com um float64 diretamente causa erro de compilação. Isso é intencional: o compilador força o programador a ser explícito sobre o que está fazendo.

var a int = 10
var b float64 = 3.5

// Isso causa erro:
// resultado := a + b

// Correto:
resultado := float64(a) + b
fmt.Println(resultado) // 13.5

A variável em branco _

Go exige que toda variável local declarada seja usada. Caso contrário, o compilador emite um erro, declared and not used. A regra não vale para variáveis de pacote: uma var naoUsada = 42 fora das funções compila sem reclamação. Para os casos em que você precisa descartar um valor — por exemplo, quando uma função retorna múltiplos valores e você só precisa de alguns — use o identificador especial _:

package main

import "fmt"

func coordenadas() (float64, float64) {
    return -23.55, -46.63
}

func main() {
    latitude, _ := coordenadas()
    fmt.Println("Latitude:", latitude)
}

O _ não é uma variável de verdade — é um destino de descarte. Valores atribuídos a ele são ignorados pelo compilador.

Escopo de variáveis

O escopo em Go segue as chaves {}. Uma variável declarada dentro de um bloco não existe fora dele:

func main() {
    x := 10

    if x > 5 {
        y := 20
        fmt.Println(x, y) // funciona
    }

    // fmt.Println(y) — erro: y não existe aqui
}

Variáveis declaradas no escopo de pacote (fora de funções) existem durante toda a execução do programa e são acessíveis em qualquer função do mesmo pacote.

Das formas de declarar, := é a que mais aparece no dia a dia, e o valor zero é o que deixa o código enxuto — um contador nasce valendo 0, uma string nasce vazia, sem inicialização obrigatória. As surpresas deste começo ficam quase todas na fronteira entre o que o compilador vê e o que só existe na execução: a constante que estoura é recusada, a conversão que estoura passa em silêncio, e len conta bytes, não letras. Vale guardar essa fronteira, porque ela reaparece em quase todo tema daqui em diante.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Numa função, x := 1 é declarado no início. Mais abaixo, dentro de um if, alguém escreve x, y := 2, 3 para aproveitar o y novo, e depois do if imprime x esperando 2. O programa compila e imprime 1. Por quê, se o artigo diz que := reaproveita as variáveis que já existem?

Ver resposta

✓ Resposta: Porque o := só reaproveita variáveis declaradas no mesmo escopo. O bloco do if é um escopo novo, e ali dentro não existe nenhum x; então o := declara dois nomes novos, x e y, e esse x interno esconde o de fora até o fim do bloco. A atribuição de 2 vai para a variável interna, que morre na chave de fechamento, e o x original continua valendo 1. É o que se chama de sombreamento, e o Go 1.27 compila esse código sem erro e sem aviso do go vet. A regra do artigo, a de que o := precisa de pelo menos uma variável nova, está certa, mas vale apenas quando as duas declarações dividem o bloco. A correção é declarar só o que é novo e atribuir o resto: var y int seguido de x, y = 2, 3, com =. O caso mais comum dessa armadilha na vida real envolve o err, redeclarado dentro de um if ou de um for e nunca devolvido para quem esperava por ele.

Exercício 2

Um sistema guarda a quantidade de itens de um pedido num int8 para "economizar memória". Um cliente compra 200 unidades. O que o código qtd := int8(v), com v valendo 200, produz? E por que int8(200), escrito com o número direto, nem compila?

Ver resposta

✓ Resposta: Produz -56, em silêncio. A conversão entre inteiros em tempo de execução nunca falha: ela mantém os 8 bits de baixo e os reinterpreta, e 200 em 8 bits com sinal é −56. Do mesmo jeito, uint8(300) dá 44, e um int32 que vale 2147483647 somado a 1 vira −2147483648, sem pânico e sem aviso do go vet. Com o número escrito direto, o caso muda de natureza: 200 é uma constante, e as constantes são verificadas pelo compilador, que recusa o programa com constant 200 overflows int8. Ou seja, o compilador protege você do valor que ele conhece e não pode proteger do valor que chega do usuário. A lição prática é que economizar bytes com tipos pequenos quase nunca vale o risco: int é o padrão para contagens, e quando um tipo menor é exigido, por formato de arquivo ou protocolo, a faixa deve ser conferida antes da conversão, com a entrada fora dela tratada como erro.

Exercício 3

Uma tela precisa mostrar só as duas primeiras letras do nome de um produto, e o código usa s[:2]. Para "ação", aparece um "a" seguido de um losango com interrogação. O que houve, e como cortar corretamente?

Ver resposta

✓ Resposta: Fatiar uma string corta bytes, não caracteres. Em UTF-8 o ç ocupa dois bytes, então "ação"[:2] pega o a e só o primeiro byte do ç: o resultado é "a\xc3", e utf8.ValidString devolve false para ele. O losango com interrogação é o caractere de substituição que o terminal ou o navegador exibe no lugar do byte órfão. O mesmo raciocínio explica por que len("ação") vale 6. Para cortar por caractere, converta para []rune, que é uma fatia de pontos Unicode: string([]rune(s)[:2]) devolve aç. A conversão aloca uma cópia, o que não importa num nome de produto e pode importar num laço sobre um texto grande; nesse caso, o for i, r := range s percorre a string caractere a caractere sem copiar nada. Para textos com emojis compostos ou letras com acento combinado, nem a runa basta, e é preciso uma biblioteca de segmentação.

Exercício 4

O artigo diz que slices e maps têm valor zero nil. Numa função, var itens []int seguido de itens = append(itens, 1) funciona, mas var contagem map[string]int seguido de contagem["x"] = 1 derruba o programa. Se os dois começam como nil, por que só um deles quebra?

Ver resposta

✓ Resposta: Porque o append sabe lidar com uma fatia nil: vendo que não há espaço, ele aloca um array novo e devolve uma fatia que aponta para ele, e é por isso que o resultado precisa ser reatribuído a itens. Depois da linha, itens é [1], com tamanho 1. Um map, ao contrário, é alterado no lugar, e um map nil não tem onde guardar nada: a escrita gera o pânico assignment to entry in nil map. Ler de um map nil, porém, é seguro: contagem["x"] devolve 0 e len(contagem) também. Essa assimetria é uma das primeiras surpresas de quem chega ao Go. A regra prática é inicializar maps antes de escrever, com make(map[string]int) ou com o literal map[string]int{}, e tomar cuidado dobrado com maps que são campos de struct, porque o valor zero da struct traz o map nil junto.

Exercício 5

Um colega declara no topo do pacote var cache = carregarCache() e fica surpreso ao ver que o programa compila mesmo sem nenhuma função usar cache, enquanto uma variável local esquecida quebra o build. Explique a diferença e diga por que essa declaração no topo merece atenção mesmo sem ser usada.

Ver resposta

✓ Resposta: A regra do "declarada e não usada" vale só para variáveis locais. Uma variável local esquecida é quase sempre engano, como um resultado calculado que ninguém conferiu, e o compilador consegue ver o escopo inteiro dela de uma vez. Uma variável de pacote pode ser usada por qualquer arquivo do pacote, inclusive por testes, e sua simples existência às vezes é o objetivo; por isso var naoUsada = 42 no topo compila sem reclamação. O ponto de atenção é outro: o inicializador de uma variável de pacote roda antes do main, esteja ela em uso ou não. Então carregarCache() vai ler disco, abrir conexão ou fazer o que mais estiver lá dentro a cada execução, inclusive em cada go test do pacote, e um erro ali acontece antes de qualquer tratamento. O staticcheck, na verificação U1000, aponta variáveis de pacote não exportadas que ninguém usa; e inicialização cara, mesmo quando usada, costuma ficar melhor dentro de uma função chamada no momento certo.

Comentários

Mais em Golang

Select, timeouts e cancelamento de operações
Select, timeouts e cancelamento de operações

Select, timeouts e cancelamento em Go: select como event loop, time.After e os…

Injeção de dependência sem frameworks
Injeção de dependência sem frameworks

Injeção de dependência em Go sem framework: construtores que validam o que é…

Criando um servidor HTTP com net/http
Criando um servidor HTTP com net/http

Servidor HTTP em Go com net/http: Handler e ServeMux, timeouts, o roteador do…