Blog

Valores e tipos em Go: zero values, constantes e iota

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:

  • var funciona em qualquer lugar, inclusive no nível do pacote, fora de qualquer função. greeting foi declarada assim.
  • := só funciona dentro de uma função. Escreva count := 3 no nível do pacote e o build falha com syntax 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ém uint

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

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
  • %v imprime qualquer valor no formato padrão. É o que o Println usa.
  • %T imprime o tipo. É o jeito mais rápido de responder “o que o := me deu?”
  • %q imprime strings entre aspas, então strings vazias e espaços perdidos ficam visíveis. Funciona com um slice de strings também.
  • %d imprime um inteiro. Um número no meio define uma largura mínima, e - preenche à direita em vez de à esquerda.
  • %.2f imprime um float com duas casas decimais, arredondado. %8.2f també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

  • var funciona 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, "" ou nil. 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) ou int(f), e lembre que int(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.
  • iota conta as linhas de um bloco const. Use _ para pular valores, muitas vezes o 0, e 1 << iota para flags.
  • %v, %T e %q sã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.

Quanto este post te ajudou?

Clique em um coração para avaliar!

Média das avaliações 0 / 5. Total de votos: 0

Nenhum voto até agora. Seja o primeiro a avaliar este post.