A palavra-chave const declara um nome para um valor que o compilador conhece em tempo de compilação, como um timeout, o nome de um cabeçalho ou um limite de tamanho. Uma constante em Go só pode guardar um booleano, um número, uma string ou uma rune, e nunca muda enquanto o programa corre. A maioria das constantes não tem tipo, ou seja, não têm um tipo fixo até as usares, por isso const maxBodyBytes = 1 << 20 funciona como int, int64 ou float64 sem conversão. Dentro de um bloco const, o identificador pré-declarado iota conta a partir de 0, e é assim que o Go escreve enums (especificação do Go).
Resumo rápido
const name = valuedeclara uma constante. Um blococonst ( ... )declara várias, e a maior parte do código Go agrupa assim as constantes relacionadas.- As constantes podem ser booleanos, números, strings ou runes. Slices, maps, structs e o resultado de uma chamada de função como
time.Now()não podem ser constantes. - Uma constante sem tipo como
5ou"GET"assume o tipo de que o contexto precisa. É por essa razão que5 * time.Secondcompila, enquantotimeout * time.Secondcom uma variávelintnão compila. - As constantes sem tipo são exatas. O compilador faz a aritmética delas com precisão arbitrária e só se queixa quando um valor não cabe no tipo em que acaba por ficar.
- O
iotacomeça em 0 em cada blococonste sobe um por linha._salta um valor,iota + 1começa em 1 e1 << iotaconstrói bit flags. - O Go não tem uma palavra-chave
enum. Um enum é um tipo com nome, um blococonstcomiotae, normalmente, um métodoString(), que a ferramentastringerpode gerar. - Não podes obter o endereço de uma constante nem atribuir-lhe um valor. Valores calculados em tempo de execução, e tabelas de consulta como maps, pertencem a um
var.
Como se declara uma constante em Go?
Escreve const, um nome e um valor. Para várias constantes relacionadas, usa um bloco agrupado entre parênteses:
example.gogopackage main import ( "fmt" "net/http" "time" ) const maxBodyBytes = 1 << 20 // 1 MiB const ( readTimeout = 5 * time.Second writeTimeout = 10 * time.Second idleTimeout = 2 * time.Minute ) const ( headerRequestID = "X-Request-ID" headerRetryAfter = "Retry-After" ) func main() { srv := &http.Server{ Addr: ":8080", ReadTimeout: readTimeout, WriteTimeout: writeTimeout, IdleTimeout: idleTimeout, } fmt.Println(srv.ReadTimeout, srv.WriteTimeout, srv.IdleTimeout) }
Isto imprime 5s 10s 2m0s. Com valores simples como estes, o agrupamento serve só para quem lê. Cada nome no bloco é uma constante independente, e o bloco mantém os timeouts do servidor num só sítio, onde quem faz a revisão os pode comparar.
As constantes seguem as mesmas regras de âmbito e visibilidade que as variáveis. Uma constante declarada ao nível do pacote é visível em todos os ficheiros do pacote, e é exportada se o nome começar com maiúscula, como em http.StatusNotFound ou time.RFC3339. A palavra-chave package em Go explica como funcionam os nomes exportados entre pacotes. Uma constante declarada dentro de uma função só existe nessa função. O estilo do Go usa mixed caps nas constantes como em qualquer outro nome, por isso a grafia idiomática é maxBodyBytes ou MaxBodyBytes, e não MAX_BODY_BYTES.
Um bloco const tem uma regra que um bloco var não tem. Quando uma linha tem um nome mas não tem tipo nem valor, repete o tipo e a expressão da linha anterior. Sozinho, isto só dá o mesmo valor a vários nomes, o que raramente é o que queres. Em conjunto com o iota, permite numerar uma lista inteira de constantes com uma só expressão.
Que valores podem ser constantes em Go?
Só booleanos, números, strings e runes. Os tipos numéricos são inteiros, números de vírgula flutuante e números complexos, e uma rune é um inteiro que representa um code point Unicode. Tudo o resto falha na compilação, incluindo um valor que na prática é fixo mas tem de ser calculado em tempo de execução:
example.gogopackage main import "time" const startedAt = time.Now()
example.texttext./main.go:5:19: time.Now() (value of struct type time.Time) is not constant
Os valores compostos são recusados da mesma forma. const allowedMethods = []string{"GET", "POST"} falha com []string{…} (value of type []string) is not constant. Slices, maps, structs, ponteiros, channels e funções não podem ser constantes, mesmo quando todos os elementos são literais.
Os tipos com nome construídos sobre um número, uma string ou um booleano são permitidos. O time.Duration é definido como um int64, por isso const pollInterval = 500 * time.Millisecond é uma constante do tipo time.Duration com o valor 500ms. O mesmo vale para os teus próprios tipos, como type LogLevel int, que é a base dos enums.
Algumas funções built-in também produzem constantes quando o argumento o permite. O len de um array é uma constante, porque o comprimento de um array faz parte do seu tipo. len("Retry-After") sobre uma constante string também é uma constante. unsafe.Sizeof, unsafe.Alignof e unsafe.Offsetof são constantes sempre que o tipo do argumento tem um tamanho fixo, por isso const headerSize = unsafe.Sizeof(int64(0)) compila e tem o valor 8. O len de um slice não é uma constante, já que o comprimento de um slice só se conhece em tempo de execução.
Qual é a diferença entre constantes tipadas e sem tipo em Go?
Uma constante tipada tem um tipo na sua declaração. Uma constante sem tipo não tem, e assume o tipo de que o código à sua volta precisar:
example.gogoconst maxRetries int32 = 3 // typed: always an int32 const maxBodyBytes = 1 << 20 // untyped: becomes whatever type it is used as
A tipada segue as regras normais de atribuição, por isso não pode ir para um int sem conversão:
example.gogovar attempts int = maxRetries
example.texttext./main.go:6:21: cannot use maxRetries (constant 3 of type int32) as int value in variable declaration
A sem tipo cabe tanto em var limit int64 = maxBodyBytes como em var limit int = maxBodyBytes e em float64(maxBodyBytes). A biblioteca padrão tira partido desta flexibilidade e deixa sem tipo a maioria das suas constantes numéricas, como math.MaxInt64 e math.Pi.
Uma constante sem tipo tem, ainda assim, um tipo por omissão. É usado quando nada mais decide o tipo, como em x := 1.5 ou quando a constante é passada ao fmt.Println:
| Constante sem tipo | Tipo por omissão |
|---|---|
true, false | bool |
42, 1 << 20 | int |
1.5, 1e9 | float64 |
'a' | rune (um alias de int32) |
2i | complex128 |
"GET" | string |
Porque é que 5 * time.Second compila mas timeout * time.Second falha?
Porque 5 é uma constante sem tipo e timeout é uma variável. O Go nunca converte sozinho entre tipos numéricos, mas uma constante sem tipo pode assumir qualquer tipo numérico onde o seu valor caiba. Em 5 * time.Second, o 5 passa a ser um time.Duration, e o resultado é um time.Duration de cinco segundos.
Uma variável já tem um tipo, por isso a mesma multiplicação falha quando o número vem da configuração:
example.gogopackage main import ( "net/http" "os" "strconv" "time" ) func main() { timeout, _ := strconv.Atoi(os.Getenv("TIMEOUT_SECONDS")) client := &http.Client{Timeout: timeout * time.Second} _ = client }
example.texttext./main.go:12:34: invalid operation: timeout * time.Second (mismatched types int and "time".Duration)
O timeout é um int, e o time.Second é um time.Duration. A solução é uma conversão explícita, time.Duration(timeout) * time.Second. Uma constante sem tipo com o mesmo valor funciona sem ela, por isso const retryDelay = 3 seguido de retryDelay * time.Second dá 3s.
Qual é a precisão das constantes sem tipo?
As constantes numéricas sem tipo são exatas. A especificação obriga os compiladores a representar as constantes inteiras com pelo menos 256 bits e a manter as constantes de vírgula flutuante exatas com uma mantissa de pelo menos 256 bits, e o compilador padrão do Go permite até 512 bits. O math.Pi está escrito com mais dígitos do que um float64 consegue guardar, e só é arredondado quando vai parar a uma variável float64 ou float32.
Essa precisão permite escrever valores intermédios demasiado grandes para qualquer tipo do Go, desde que o resultado final caiba. const maxOffset = 1 << 100 compila, e maxOffset >> 98 é a constante 4. O erro só aparece quando o valor tem de passar a ser um tipo concreto:
example.gogopackage main import "fmt" const maxOffset = 1 << 100 func main() { fmt.Println(maxOffset) }
example.texttext./main.go:8:14: cannot use maxOffset (untyped int constant 1267650600228229401496703205376) as int value in argument to fmt.Println (overflows)
Como o compilador conhece o valor de todas as constantes, estas verificações de intervalo acontecem em tempo de compilação, em vez de o valor dar a volta em silêncio em tempo de execução. Mais alguns exemplos:
| Código | Erro do compilador |
|---|---|
var uploadLimit int32 = 1 << 40 | cannot use 1 << 40 (untyped int constant 1099511627776) as int32 value in variable declaration (overflows) |
const maxConns uint8 = 300 | cannot use 300 (untyped int constant) as uint8 value in constant declaration (overflows) |
var workers int = 2.5 | cannot use 2.5 (untyped float constant) as int value in variable declaration (truncated) |
A verificação olha só para o valor. var workers int = 2.0 compila, porque 2.0 é uma constante float sem tipo cujo valor é um número inteiro.
O que é o iota em Go?
O iota é um identificador pré-declarado que só tem significado dentro de uma declaração const. Vale 0 na primeira especificação de constante (spec) de um bloco const e sobe um em cada spec seguinte. Uma spec é uma linha que declara nomes, por isso as linhas em branco e os comentários não contam. Com a repetição implícita da expressão anterior, um tipo e um único iota chegam para numerar uma lista inteira:
example.gogotype LogLevel int const ( LevelDebug LogLevel = iota // 0 LevelInfo // 1 LevelWarn // 2 LevelError // 3 )
LevelInfo, LevelWarn e LevelError não têm tipo nem valor próprios, por isso cada uma repete LogLevel = iota com o valor seguinte de iota. Todas as constantes da lista têm o tipo LogLevel.
Como o iota conta:
- O
iotavolta a 0 no início de cada blococonst. Dois blocos separados começam ambos em 0. - Conta specs, não nomes. Duas constantes declaradas na mesma linha veem o mesmo
iota. - Uma linha que usa
_conta na mesma, por isso o identificador em branco salta um valor. - O
iotaé uma constante inteira sem tipo, por isso funciona em qualquer expressão constante, comoiota + 1ou1 << iota.
Como se começa o iota em 1 ou se salta um valor?
Soma-lhe um, ou deita fora o primeiro valor com _. Começar em 1 deixa o valor zero livre para indicar «não definido»:
example.gogotype Priority int const ( PriorityLow Priority = iota + 1 // 1 PriorityNormal // 2 PriorityHigh // 3 )
Saltar com _ serve para os casos em que o primeiro valor não tem significado. As unidades de tamanho abaixo saltam iota == 0, já que 1 << 0 seria 1 byte:
example.gogotype ByteSize int64 const ( _ = iota // skip 0 KB ByteSize = 1 << (10 * iota) MB GB )
O KB é 1 << 10, ou seja 1024, o MB é 1 << 20 e o GB é 1 << 30. Cada linha repete ByteSize = 1 << (10 * iota) com um iota maior.
Como se definem bit flags com iota?
Desloca o 1 para a esquerda iota posições. Cada constante fica assim com o seu próprio bit, e combinas as constantes com |:
example.gogopackage main import "fmt" type Permission uint8 const ( PermRead Permission = 1 << iota // 1 PermWrite // 2 PermDelete // 4 ) func main() { perm := PermRead | PermWrite fmt.Println(perm&PermWrite != 0) // true fmt.Println(perm&PermDelete != 0) // false }
A biblioteca padrão usa o mesmo padrão. log.Ldate, log.Ltime e log.Lshortfile são flags 1 << iota que combinas em log.SetFlags(log.LstdFlags | log.Lshortfile).
O Go tem enums?
Não como uma funcionalidade à parte da linguagem. Não existe a palavra-chave enum. O Go constrói os enums a partir de três peças que já tem: um tipo com nome, um bloco const com iota e um método String() para que os valores apareçam como nomes e não como números:
example.gogopackage main import "fmt" type LogLevel int const ( LevelDebug LogLevel = iota LevelInfo LevelWarn LevelError ) func (l LogLevel) String() string { switch l { case LevelDebug: return "debug" case LevelInfo: return "info" case LevelWarn: return "warn" case LevelError: return "error" default: return fmt.Sprintf("LogLevel(%d)", int(l)) } } func main() { fmt.Println(LevelWarn, LogLevel(9)) fmt.Printf("level=%v\n", LevelError) }
Isto imprime warn LogLevel(9) e depois level=error. O fmt chama String() quando imprime um valor com Println, %v ou %s. O caso default transforma um valor fora da lista, como LogLevel(9), em algo legível.
Como se geram métodos String com o stringer?
Escrever aquele switch à mão torna-se aborrecido, e fica desatualizado quando alguém acrescenta uma constante e se esquece do método. A ferramenta stringer, da equipa do Go, gera-o por ti. Acrescenta uma diretiva go:generate junto ao tipo e executa go generate:
example.gogopackage main import "fmt" //go:generate go run golang.org/x/tools/cmd/stringer@latest -type=OrderStatus -trimprefix=OrderStatus type OrderStatus int const ( OrderStatusUnknown OrderStatus = iota OrderStatusPending OrderStatusPaid OrderStatusShipped OrderStatusRefunded ) func main() { fmt.Println(OrderStatusPaid, OrderStatus(42)) }
O go generate escreve orderstatus_string.go com um método String(), e o programa imprime Paid OrderStatus(42). O -trimprefix retira o prefixo comum dos nomes. O ficheiro gerado inclui também uma pequena verificação em tempo de compilação. Quando o valor de uma constante existente muda e o ficheiro não foi gerado de novo, a build falha com um erro como invalid argument: index 4 out of bounds [0:1]. Uma constante acrescentada no fim da lista não é detetada, e aparece como OrderStatus(5) até voltares a executar go generate, por isso muitas equipas executam-no na CI e fazem falhar a build quando os ficheiros gerados mudam.
Porque é que o valor zero de um enum deve ser Unknown?
Porque todas as variáveis em Go começam com o seu valor zero, e num enum baseado em int esse valor é 0. Uma struct Order descodificada de JSON sem o campo status, ou declarada com var o Order, tem o estado 0. Se 0 for OrderStatusPending, um valor em falta parece uma encomenda pendente real. Pôr um OrderStatusUnknown explícito em primeiro lugar, ou começar em iota + 1, faz com que o valor zero signifique «não definido», e o código pode recusá-lo.
O Go verifica se um switch cobre todos os valores de um enum?
Não. Um tipo enum continua a ser um tipo inteiro normal, por isso o compilador não sabe que valores são válidos. var level LogLevel = 9 compila, já que 9 é uma constante sem tipo que cabe num int. Um switch que só trata alguns dos valores também compila sem qualquer aviso.
As equipas que querem essa verificação acrescentam o linter exhaustive, que vem incluído no golangci-lint. Assinala um switch sobre um tipo enum que deixa de fora uma das constantes do tipo. Um caso default, como o do método String() acima, trata em tempo de execução os valores que estão fora da lista.
Quando usar const ou var em Go?
Usa const para valores que se conhecem quando escreves o código e que nunca mudam: timeouts, limites, nomes de cabeçalhos, valores de enums. Usa var para tudo o que é calculado em tempo de execução, lido da configuração ou construído a partir de um tipo composto.
Não podes obter o endereço de uma constante, já que uma constante não fica guardada numa variável:
example.gogop := &maxBodyBytes
example.texttext./main.go:6:8: invalid operation: cannot take address of maxBodyBytes (untyped int constant 1048576)
O problema surge com APIs que recebem ponteiros para campos opcionais. Desde o Go 1.26, o new aceita uma expressão, por isso new(maxBodyBytes) devolve um *int que aponta para uma cópia do valor. Em versões anteriores, copia primeiro a constante para uma variável e obtém o endereço dela.
Também não podes atribuir um valor a uma constante. maxBodyBytes = 2 << 20 falha com cannot assign to maxBodyBytes (neither addressable nor a map index expression).
As constantes não usadas compilam. Uma variável local não usada é um erro, declared and not used: retries, mas uma constante não usada é aceite, mesmo dentro de uma função. Linters como o staticcheck assinalam na mesma as constantes não exportadas e não usadas ao nível do pacote.
As tabelas de consulta são variáveis. Um map de estado para etiqueta não pode ser uma constante, por isso declara-o com var ao nível do pacote, ou devolve-o a partir de uma função quando não queres que outro código o altere:
example.gogovar orderStatusLabels = map[OrderStatus]string{ OrderStatusPending: "Awaiting payment", OrderStatusPaid: "Paid", OrderStatusShipped: "On the way", OrderStatusRefunded: "Refunded", }
Um var ao nível do pacote pode ser alterado por qualquer código do pacote, e esse é o preço de poder guardar qualquer tipo. Quando os valores são fixos e simples, uma constante dá-te verificações em tempo de compilação e nenhum risco de alterações em tempo de execução. A palavra-chave var em Go aprofunda as variáveis, os valores zero e o :=.
Onde entra o LevelUpGo
O LevelUpGo ensina Go através de exercícios que executam código Go real no navegador. O Go Basics apresenta o const a par das variáveis e dos tipos. O Go Language Deep Dives tem uma lição sobre iota e padrões com const onde constróis um enum, bit flags e unidades de tamanho em bytes. O Training Ground tem exercícios curtos e independentes para praticares fora de um curso. Para as outras 24 palavras reservadas, vê Palavras-chave do Go: as 25 explicadas.
Perguntas frequentes
O const é uma palavra-chave em Go?
Sim. O const é uma das 25 palavras-chave reservadas do Go, por isso não o podes usar como nome de variável, de função ou de tipo. Inicia uma declaração de constante, seja uma única const name = value ou um bloco agrupado const ( ... ).
Um slice ou um map pode ser uma constante em Go?
Não. As constantes só podem ser booleanos, números, strings e runes, ou tipos com nome construídos sobre eles. const allowedMethods = []string{"GET", "POST"} falha com is not constant. Usa um var ao nível do pacote para slices e maps, ou uma função que devolve uma cópia nova quando quem chama não pode alterar a partilhada.
O que é o iota em Go?
O iota é um identificador pré-declarado que numera as linhas de um bloco const. Vale 0 na primeira linha, sobe um em cada linha seguinte e volta a zero em cada bloco novo. Com um tipo com nome e a repetição implícita, dá a cada constante de uma lista o seu próprio valor, e é assim que o Go escreve enums e bit flags.
Qual é a diferença entre constantes tipadas e sem tipo em Go?
Uma constante tipada, como const maxRetries int32 = 3, tem um tipo fixo e segue as regras normais de atribuição. Uma constante sem tipo, como const maxRetries = 3, não tem tipo até ser usada, por isso pode passar a ser um int, um int64, um float64 ou um time.Duration. As constantes sem tipo também mantêm valores exatos, com precisão arbitrária, até serem convertidas.
O Go tem enums?
O Go não tem a palavra-chave enum, mas na prática tem enums. Declaras um tipo com nome como type OrderStatus int, listas os seus valores num bloco const com iota e acrescentas um método String(), muitas vezes gerado pelo stringer. O compilador não limita o tipo aos valores listados, por isso valida os dados de entrada e usa o linter exhaustive se precisares que cada switch cubra todos os valores.
Fontes
- The Go Programming Language Specification, Constants: https://go.dev/ref/spec#Constants
- The Go Programming Language Specification, Constant declarations: https://go.dev/ref/spec#Constant_declarations
- The Go Programming Language Specification, Iota: https://go.dev/ref/spec#Iota
- The Go Programming Language Specification, Representability: https://go.dev/ref/spec#Representability
- The Go Blog, Constants (Rob Pike): https://go.dev/blog/constants
- Effective Go, Constants: https://go.dev/doc/effective_go#constants
- stringer: https://pkg.go.dev/golang.org/x/tools/cmd/stringer
- Go Code Review Comments, Mixed caps: https://go.dev/wiki/CodeReviewComments#mixed-caps
- Linter exhaustive: https://github.com/nishanths/exhaustive
