Toda variável em Go começa com um zero value, números nunca se convertem sozinhos e constantes são mais flexíveis que variáveis. Aprenda var, :=, inteiros com tamanho, overflow, iota e os verbos do Printf para inspecionar valores.
Go é rígido com tipos em alguns lugares onde outras linguagens são relaxadas. Um int nunca vira um float64 sem você pedir. Uma variável nunca fica guardando lixo. Uma constante que não cabe é pega antes de o programa rodar.
Este post cobre declaração de variáveis, os tipos básicos, zero values, conversões, constantes e iota, além dos verbos do fmt.Printf que você vai usar para olhar tudo isso. Todo programa abaixo rodou no Go 1.26, e a saída foi colada direto da execução.
Declarando variáveis: var e :=
Go tem duas formas de declarar uma variável. var dá nome à variável e, se você quiser, o tipo e o valor. := declara e atribui num passo só, e tira o tipo do valor.
package main
import "fmt"
var greeting = "hello"
func main() {
var count int
var name string = "Ada"
age := 36
fmt.Println(greeting, count, name, age)
}
Ele imprime:
hello 0 Ada 36
count foi declarada com tipo e sem valor, então guarda 0. name tem tipo e valor. age := 36 pega o tipo int do literal 36.
As duas formas não são permitidas nos mesmos lugares:
varfunciona em qualquer lugar, inclusive no nível do pacote, fora de qualquer função.greetingfoi declarada assim.:=só funciona dentro de uma função. Escrevacount := 3no nível do pacote e o build falha comsyntax error: non-declaration statement outside function body.
Dentro de uma função, a maior parte do código Go usa :=. Use var quando você quer o zero value de propósito, ou quando quer um tipo diferente do que o literal daria, como em var ratio float64 = 2.
:= precisa de pelo menos um nome novo
A forma := declara. Ela não serve para atribuir a uma variável que já existe no mesmo escopo:
package main
import "fmt"
func main() {
x := 1
x := 2
fmt.Println(x)
}
O build falha com:
./main.go:7:4: no new variables on left side of :=
Use x = 2 para mudar uma variável que você já tem.
Existe uma exceção, e você vai ver ela o tempo todo. Quando o := tem vários nomes à esquerda, ele é permitido desde que pelo menos um deles seja novo. Os outros só recebem a atribuição:
package main
import (
"fmt"
"strconv"
)
func main() {
n, err := strconv.Atoi("42")
fmt.Println(n, err)
m, err := strconv.Atoi("x7")
fmt.Println(m, err)
}
Ele imprime:
42 <nil>
0 strconv.Atoi: parsing "x7": invalid syntax
A segunda linha declara m e reaproveita err. É por isso que código Go consegue chamar uma função depois da outra e continuar verificando a mesma variável err. A parte sobre erros mostra o que fazer com ela.
Os tipos básicos
A maior parte do código Go se vira com quatro tipos básicos:
| Tipo | Guarda | Exemplo |
|---|---|---|
int |
números inteiros | 42 |
float64 |
números com parte fracionária | 2.99 |
string |
texto | "Ada" |
bool |
true ou false |
true |
int tem 64 bits nas máquinas de 64 bits em que você quase sempre vai rodar. Quando você precisa de um tamanho exato, para um formato de arquivo, um protocolo de rede ou para economizar memória num slice enorme, Go tem inteiros com tamanho definido:
- com sinal:
int8,int16,int32,int64 - sem sinal:
uint8,uint16,uint32,uint64, e tambémuint
Um int8 guarda de -128 a 127. Um uint8 guarda de 0 a 255. Também existe float32, mas float64 é o padrão para decimais, e é nele que um literal como 2.99 se transforma.
Overflow: dá a volta em tempo de execução, falha em tempo de compilação
Um inteiro com tamanho tem espaço para uma faixa fixa de valores, e o que acontece quando você passa do fim depende de o Go conseguir ver isso chegando. Em tempo de execução, o valor dá a volta:
package main
import "fmt"
func main() {
var small int8 = 127
small++
fmt.Println(small)
var u uint8 = 0
u--
fmt.Println(u)
}
Ele imprime:
-128
255
Somar 1 ao maior int8 dá o menor. Subtrair 1 de um uint8 que guarda 0 dá 255. Não tem panic nem aviso. Os bits viram como o hodômetro de um carro.
Quando o valor que estoura é uma constante escrita no seu código, o compilador consegue verificar, e verifica:
package main
import "fmt"
func main() {
var small int8 = 128
fmt.Println(small)
}
O build falha com:
./main.go:6:19: cannot use 128 (untyped int constant) as int8 value in variable declaration (overflows)
Então a regra é: constantes são verificadas quando você compila, e aritmética com variáveis dá a volta em silêncio quando você roda. Se um contador pode mesmo chegar tão alto, dê a ele um tipo maior.
Zero values: nada fica sem inicializar
Todo tipo em Go tem um zero value, e uma variável declarada sem valor guarda esse zero value. Não existe um estado “não inicializado” para você esquecer:
package main
import "fmt"
func main() {
var i int
var f float64
var s string
var b bool
var p *int
var nums []int
var ages map[string]int
var point struct{ X, Y int }
fmt.Println(i, f, b, p, nums, ages, point)
fmt.Printf("%q\n", s)
fmt.Println(nums == nil, ages == nil, p == nil)
}
Ele imprime:
0 0 false <nil> [] map[] {0 0}
""
true true true
Números começam em 0. Um bool começa como false. Uma string começa como "", a string vazia, e é por isso que ela foi impressa com %q aqui: o Println não mostraria nada. Ponteiros, slices e maps começam como nil. Uma struct começa com cada campo no seu próprio zero value, então point é {0 0}.
Explicado como se você tivesse dez anos
Imagine que toda caixa nova que você ganha já vem com alguma coisa dentro. Uma caixa de números vem com um 0. Uma caixa de respostas sim-ou-não vem com um “não”. Uma caixa de palavras vem com um cartão em branco.
Você nunca abre uma caixa nova e encontra tralha velha que alguém deixou. Então, mesmo que você esqueça de colocar algo, você sabe exatamente o que tem lá.
A versão precisa
Quando o Go aloca memória para uma variável, seja com var, new, make ou um literal composto que deixa campos de fora, ele preenche essa memória com bits zero. Para cada tipo, todos os bits zero querem dizer algo sensato: 0 para números, false para bool, "" para strings, e nil para ponteiros, slices, maps, channels, funções e interfaces.
É por isso que um programa Go não consegue ler os bytes que sobraram de um valor anterior, e por isso o go vet não precisa avisar sobre usar uma variável antes de ela receber valor. Isso também molda como os tipos Go são desenhados. Um bom tipo é útil no seu zero value: um slice nil é um slice vazio que funciona, e var total int já está pronto para receber somas.
Onde a analogia falha: algumas caixas chegam vazias e lacradas. Um map nil pode ser lido sem problema, e ler qualquer chave dá o zero value, mas escrever nele causa panic. A parte sobre maps mostra por quê e o que fazer.
Nada de conversões implícitas
Go nunca converte entre tipos numéricos por conta própria, nem de int para float64, onde nada se perderia:
package main
import "fmt"
func main() {
count := 3
price := 2.5
fmt.Println(count * price)
}
O build falha com:
./main.go:8:14: invalid operation: count * price (mismatched types int and float64)
count é um int e price é um float64, e um operador precisa que os dois lados tenham o mesmo tipo. Você diz qual conversão quer escrevendo o nome do tipo como se fosse uma chamada de função:
package main
import (
"fmt"
"math"
)
func main() {
count := 3
price := 2.99
total := float64(count) * price
fmt.Println(total)
fmt.Println(int(price), int(-price))
fmt.Println(int(math.Round(price)))
big := 300
fmt.Println(uint8(big))
}
Ele imprime:
8.97
2 -2
3
44
float64(count) transforma 3 em 3.0, e a multiplicação funciona.
No sentido contrário, int(price) trunca. Ele descarta a parte fracionária, então 2.99 vira 2 e -2.99 vira -2. Arredonda em direção ao zero, não para baixo e não para o mais próximo. Se você quer arredondar, chame math.Round antes.
A última linha converte um int que guarda 300 para uint8. Só os 8 bits de baixo sobrevivem, e 300 menos 256 é 44. Como no overflow, o compilador pega isso quando o valor é uma constante: uint8(300) falha com constant 300 overflows uint8. Com uma variável, ele não tem como saber, então o valor dá a volta.
A rigidez custa algumas palavras a mais. Em troca, toda conversão num programa Go está escrita, então você consegue achar o ponto onde um valor perdeu precisão.
Constantes: com tipo e sem tipo
Uma constante é um valor fixado na compilação, declarado com const. A parte surpreendente é que uma constante sem tipo pode ser usada com vários tipos:
package main
import "fmt"
const ratio = 2
func main() {
var count int = 5
var price float64 = 1.25
var tiny int8 = 3
fmt.Println(count*ratio, price*ratio, tiny*ratio)
fmt.Printf("%T %T %T\n", count*ratio, price*ratio, tiny*ratio)
}
Ele imprime:
10 2.5 6
int float64 int8
O mesmo ratio multiplicou um int, um float64 e um int8, sem conversões. Compare com o erro de count * price lá em cima. Uma variável guardando 2 não conseguiria fazer isso.
Dê um tipo à constante, e a flexibilidade some:
package main
import "fmt"
const ratio int = 2
func main() {
var price float64 = 1.25
fmt.Println(price * ratio)
}
O build falha com:
./main.go:9:14: invalid operation: price * ratio (mismatched types float64 and int)
Agora ratio é um int, e segue as mesmas regras de qualquer variável int.
Explicado como se você tivesse dez anos
Uma constante sem tipo é como o número 2 escrito num post-it. Você pode colar num pote de bolinhas de gude, numa jarra de água ou num saco de farinha, e ele quer dizer “dois” do que estiver ali.
Uma constante com tipo é o número 2 impresso numa bolinha de gude. Agora são duas bolinhas, e você não consegue despejar isso na jarra.
A versão precisa
Uma constante sem tipo tem um tipo de valor (kind), como inteiro, ponto flutuante ou string, mas ainda não tem um tipo fixo. Quando você usa a constante, o Go dá a ela o tipo que o contexto pede, desde que o valor caiba nesse tipo. Quando nada mais decide, ela assume um tipo padrão: int para 2, float64 para 2.5, string para "hi". Foi daí que age := 36 tirou o int.
Constantes sem tipo também são exatas. O compilador faz a aritmética de constantes com muito mais precisão do que qualquer tipo Go tem, então um valor intermediário pode ser enorme, desde que o final caiba:
package main
import "fmt"
const huge = 1 << 100
func main() {
fmt.Println(huge >> 98)
fmt.Println(huge / (1 << 90))
}
Ele imprime:
4
1024
1 << 100 não cabe em nenhum tipo inteiro que o Go tem. Como constante, tudo bem, porque só os resultados, 4 e 1024, chegam a virar valores int.
Onde a analogia falha: o post-it ainda precisa caber. A constante sem tipo 128 não entra num int8, e esse é o erro de overflow de antes.
iota para enumerações
Go não tem palavra-chave enum. Em vez disso, dentro de um bloco const, o nome iota conta as linhas: 0 na primeira, 1 na segunda, e assim por diante. Junto com um tipo nomeado, isso dá uma enumeração:
package main
import "fmt"
type Weekday int
const (
Sunday Weekday = iota
Monday
Tuesday
Wednesday
)
func main() {
fmt.Println(Sunday, Monday, Tuesday, Wednesday)
fmt.Printf("%T %v\n", Tuesday, Tuesday)
}
Ele imprime:
0 1 2 3
main.Weekday 2
Só Sunday tem = iota escrito. Uma constante sem nada depois do nome repete a expressão de cima, então Monday também é Weekday = iota, mas na linha em que iota vale 1. As quatro constantes têm o tipo Weekday, não int puro.
Pulando valores
O nome vazio _ ocupa uma linha, e um valor de iota, sem criar constante:
package main
import "fmt"
type Level int
const (
_ Level = iota
Debug
Info
_
Error
)
func main() {
fmt.Println(Debug, Info, Error)
var unset Level
fmt.Println(unset == Debug)
}
Ele imprime:
1 2 4
false
O primeiro _ gasta o 0, e o segundo gasta o 3. Pular o 0 é uma escolha comum com um motivo real. O zero value de Level é 0, então um Level que ninguém definiu não quer dizer Debug por acidente. unset == Debug é false.
Tamanhos e flags com 1 << iota
O iota pode aparecer dentro de qualquer expressão constante, e o deslocamento de bits é o uso clássico. Aqui cada linha desloca 1 para a esquerda dez bits a mais, então cada tamanho é 1024 vezes o anterior:
package main
import "fmt"
type ByteSize int64
const (
_ = iota // 0: thrown away
KB ByteSize = 1 << (10 * iota)
MB
GB
TB
)
func main() {
fmt.Println(KB, MB, GB, TB)
file := 1536 * MB
fmt.Printf("%T\n", file)
fmt.Printf("%.2f GB\n", float64(file)/float64(GB))
}
Ele imprime:
1024 1048576 1073741824 1099511627776
main.ByteSize
1.50 GB
KB está na linha em que iota vale 1, então é 1 << 10. MB repete a expressão com iota em 2, que dá 1 << 20. 1536 * MB multiplica uma constante sem tipo por um ByteSize, então o resultado também é um ByteSize.
Um 1 << iota simples dá um bit por constante, e é assim que código Go escreve flags que você pode combinar:
package main
import "fmt"
type Permission uint8
const (
Read Permission = 1 << iota
Write
Execute
)
func main() {
fmt.Println(Read, Write, Execute)
perm := Read | Execute
fmt.Printf("%03b %d\n", perm, perm)
fmt.Println(perm&Write != 0, perm&Execute != 0)
}
Ele imprime:
1 2 4
101 5
false true
Read | Execute liga dois bits, e %03b imprime esses bits em binário. perm&Write != 0 pergunta se um bit está ligado.
Olhando valores com fmt.Printf
O fmt.Printf recebe uma string de formato com verbos que começam com %, e alguns deles cobrem quase tudo que você vai querer inspecionar:
package main
import "fmt"
func main() {
name := "Ada"
age := 36
height := 1.6549
admin := true
tags := []string{"go", "math"}
fmt.Printf("%v %v %v %v %v\n", name, age, height, admin, tags)
fmt.Printf("%T %T %T %T %T\n", name, age, height, admin, tags)
fmt.Printf("%q %q\n", name, tags)
fmt.Printf("%d|%5d|%-5d|\n", age, age, age)
fmt.Printf("%.2f %8.2f\n", height, height)
}
Ele imprime:
Ada 36 1.6549 true [go math]
string int float64 bool []string
"Ada" ["go" "math"]
36| 36|36 |
1.65 1.65
%vimprime qualquer valor no formato padrão. É o que oPrintlnusa.%Timprime o tipo. É o jeito mais rápido de responder “o que o:=me deu?”%qimprime strings entre aspas, então strings vazias e espaços perdidos ficam visíveis. Funciona com um slice de strings também.%dimprime um inteiro. Um número no meio define uma largura mínima, e-preenche à direita em vez de à esquerda.%.2fimprime um float com duas casas decimais, arredondado.%8.2ftambém preenche até 8 caracteres.
Para structs, o %v tem dois irmãos maiores:
package main
import "fmt"
type Point struct {
X, Y int
}
func main() {
p := Point{3, 4}
fmt.Printf("%v\n", p)
fmt.Printf("%+v\n", p)
fmt.Printf("%#v\n", p)
}
Ele imprime:
{3 4}
{X:3 Y:4}
main.Point{X:3, Y:4}
O %+v acrescenta os nomes dos campos, o que deixa a saída de debug legível. O %#v imprime o valor como sintaxe Go, com nome do tipo e tudo.
Usar o verbo errado não para o programa. fmt.Printf("%d\n", "Ada") imprime %!d(string=Ada) e segue em frente. Mas o go vet pega isso antes de você rodar qualquer coisa: fmt.Printf format %d has arg "Ada" of wrong type string. Vale criar o hábito de rodar go vet a cada mudança.
byte e rune são outros nomes para inteiros
Go tem dois nomes de tipo que você vai encontrar assim que trabalhar com texto, e nenhum deles é um tipo novo. byte é outro nome para uint8, e rune é outro nome para int32:
package main
import "fmt"
func main() {
var b byte = 'A'
var r rune = 'é'
fmt.Println(b, r)
fmt.Printf("%T %T\n", b, r)
fmt.Printf("%c %c %q\n", b, r, r)
var u uint8 = b
var i int32 = r
fmt.Println(u, i)
fmt.Println(len("café"))
}
Ele imprime:
65 233
uint8 int32
A é 'é'
65 233
5
O Println mostra números, porque é só isso que eles são. O %T nem diz byte ou rune: ele informa uint8 e int32, porque um alias é o mesmo tipo com um segundo nome. É também por isso que var u uint8 = b não precisa de conversão. Para ver o caractere, use %c, ou %q para ele entre aspas.
A última linha é uma prévia do que vem por aí. "café" tem quatro caracteres, mas tamanho 5, porque len conta bytes e o é ocupa dois. A parte sobre strings, bytes e runes explica por quê.
O que lembrar
varfunciona em qualquer lugar e dá o zero value quando você omite o valor.:=só funciona dentro de funções e precisa de pelo menos um nome novo à esquerda.- Todo tipo tem um zero value: 0,
false,""ounil. Uma variável Go nunca fica sem inicializar. - Inteiros com tamanho dão a volta em silêncio em tempo de execução. Uma constante que não cabe é erro de compilação.
- Go nunca converte números por você. Escreva
float64(n)ouint(f), e lembre queint(f)trunca em direção ao zero. - Constantes sem tipo assumem o tipo que o contexto pede, desde que o valor caiba. Constantes com tipo se comportam como variáveis daquele tipo.
iotaconta as linhas de um blococonst. Use_para pular valores, muitas vezes o 0, e1 << iotapara flags.%v,%Te%qsão o jeito mais rápido de ver o que um valor realmente é.
Em Go, todo valor tem um tipo que você pode imprimir e um valor inicial que você pode prever.