Volver al blog

La palabra clave const en Go: constantes sin tipo, iota y enums

Cómo funciona la palabra clave const en Go: declaraciones agrupadas, qué puede ser una constante, constantes con tipo y sin tipo, iota, enums con métodos String y stringer, y const frente a var.

La palabra clave const en Go: constantes sin tipo, iota y enums

La palabra clave const declara un nombre para un valor que el compilador conoce en tiempo de compilación, como un timeout, el nombre de una cabecera o un límite de tamaño. Una constante de Go solo puede contener un booleano, un número, una cadena o una rune, y nunca cambia mientras el programa se ejecuta. La mayoría de las constantes no tienen tipo, es decir, no tienen un tipo fijo hasta que las usas, así que const maxBodyBytes = 1 << 20 funciona como int, int64 o float64 sin conversión. Dentro de un bloque const, el identificador predeclarado iota cuenta desde 0, y así es como Go escribe los enums (especificación de Go).

Resumen rápido

  • const name = value declara una constante. Un bloque const ( ... ) declara varias, y la mayor parte del código Go agrupa así las constantes relacionadas.
  • Las constantes pueden ser booleanos, números, cadenas o runes. Los slices, los maps, los structs y el resultado de una llamada a función como time.Now() no pueden ser constantes.
  • Una constante sin tipo como 5 o "GET" toma el tipo que necesita su contexto. Por esa razón 5 * time.Second compila, mientras que timeout * time.Second con una variable int no.
  • Las constantes sin tipo son exactas. El compilador hace su aritmética con precisión arbitraria y solo se queja cuando un valor no cabe en el tipo en el que acaba.
  • iota empieza en 0 en cada bloque const y sube de uno en uno por línea. _ se salta un valor, iota + 1 empieza en 1 y 1 << iota construye flags de bits.
  • Go no tiene palabra clave enum. Un enum es un tipo con nombre, un bloque const con iota y normalmente un método String(), que la herramienta stringer puede generar.
  • No puedes tomar la dirección de una constante ni asignarle un valor. Los valores calculados en tiempo de ejecución y las tablas de búsqueda como los maps van en una var.

¿Cómo se declara una constante en Go?

Escribe const, un nombre y un valor. Para varias constantes relacionadas, usa un bloque agrupado entre paréntesis:

example.gogo
package 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)
}

Esto imprime 5s 10s 2m0s. Con valores simples como estos, la agrupación es solo para quien lee. Cada nombre del bloque es una constante independiente, y el bloque reúne los timeouts del servidor en un solo sitio donde quien revisa el código puede compararlos.

Las constantes siguen las mismas reglas de ámbito y visibilidad que las variables. Una constante declarada a nivel de paquete es visible en todos los archivos del paquete, y se exporta si su nombre empieza por mayúscula, como http.StatusNotFound o time.RFC3339. La palabra clave package en Go explica cómo funcionan los nombres exportados entre paquetes. Una constante declarada dentro de una función solo existe en esa función. En Go las constantes se escriben en MixedCaps, igual que cualquier otro nombre, así que la forma idiomática es maxBodyBytes o MaxBodyBytes, no MAX_BODY_BYTES.

Un bloque const tiene una regla que un bloque var no tiene. Cuando una línea tiene un nombre pero no tiene tipo ni valor, repite el tipo y la expresión de la línea anterior. Por sí sola, esa regla solo da el mismo valor a varios nombres, algo que rara vez quieres. Combinada con iota, permite que una sola expresión numere una lista entera de constantes.

¿Qué valores pueden ser constantes en Go?

Solo booleanos, números, cadenas y runes. Los tipos numéricos son enteros, números de coma flotante y números complejos, y una rune es un entero que representa un punto de código Unicode. Cualquier otra cosa no compila, incluido un valor que en la práctica es fijo pero que hay que calcular en tiempo de ejecución:

example.gogo
package main

import "time"

const startedAt = time.Now()
example.texttext
./main.go:5:19: time.Now() (value of struct type time.Time) is not constant

Los valores compuestos se rechazan de la misma forma. const allowedMethods = []string{"GET", "POST"} falla con []string{…} (value of type []string) is not constant. Los slices, los maps, los structs, los punteros, los channels y las funciones no pueden ser constantes, aunque todos sus elementos sean literales.

Los tipos con nombre basados en un número, una cadena o un booleano sí se permiten. time.Duration se define como un int64, así que const pollInterval = 500 * time.Millisecond es una constante de tipo time.Duration con el valor 500ms. Lo mismo vale para tus propios tipos, como type LogLevel int, que es la base de los enums.

Algunas funciones integradas también producen constantes cuando su argumento lo permite. len de un array es una constante porque la longitud de un array forma parte de su tipo. len("Retry-After") sobre una cadena constante también es una constante. unsafe.Sizeof, unsafe.Alignof y unsafe.Offsetof son constantes siempre que el tipo del argumento tenga un tamaño fijo, así que const headerSize = unsafe.Sizeof(int64(0)) compila y vale 8. len de un slice no es una constante, porque la longitud de un slice solo se conoce en tiempo de ejecución.

¿Qué diferencia hay entre constantes con tipo y sin tipo en Go?

Una constante con tipo lleva un tipo en su declaración. Una constante sin tipo no lo lleva, y toma el tipo que necesite el código que la rodea:

example.gogo
const maxRetries int32 = 3   // typed: always an int32
const maxBodyBytes = 1 << 20 // untyped: becomes whatever type it is used as

La que tiene tipo sigue las reglas normales de asignación, así que no puede ir a un int sin conversión:

example.gogo
var attempts int = maxRetries
example.texttext
./main.go:6:21: cannot use maxRetries (constant 3 of type int32) as int value in variable declaration

La que no tiene tipo encaja igual en var limit int64 = maxBodyBytes, en var limit int = maxBodyBytes y en float64(maxBodyBytes). La biblioteca estándar aprovecha esa flexibilidad y deja sin tipo la mayoría de sus constantes numéricas, como math.MaxInt64 y math.Pi.

Una constante sin tipo sigue teniendo un tipo por defecto. Se usa cuando nada en el contexto fija el tipo, como en x := 1.5 o cuando la constante se pasa a fmt.Println:

Constante sin tipoTipo por defecto
true, falsebool
42, 1 << 20int
1.5, 1e9float64
'a'rune (un alias de int32)
2icomplex128
"GET"string

¿Por qué 5 * time.Second compila y timeout * time.Second falla?

Porque 5 es una constante sin tipo y timeout es una variable. Go nunca convierte entre tipos numéricos por su cuenta, pero una constante sin tipo puede tomar cualquier tipo numérico en el que quepa su valor. En 5 * time.Second, el 5 se convierte en un time.Duration, y el resultado es un time.Duration de cinco segundos.

Una variable ya tiene un tipo, así que la misma multiplicación falla cuando el número viene de la configuración:

example.gogo
package 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)

timeout es un int, y time.Second es un time.Duration. La solución es una conversión explícita, time.Duration(timeout) * time.Second. Una constante sin tipo con el mismo valor funciona sin conversión, así que const retryDelay = 3 seguido de retryDelay * time.Second da 3s.

¿Qué precisión tienen las constantes sin tipo?

Las constantes numéricas sin tipo son exactas. La especificación exige que los compiladores representen las constantes enteras con al menos 256 bits y que mantengan las constantes de coma flotante con una mantisa de al menos 256 bits, y el compilador estándar de Go admite hasta 512 bits. math.Pi está escrita con más dígitos de los que caben en un float64, y solo se redondea cuando acaba en una variable float64 o float32.

Esa precisión te permite escribir valores intermedios demasiado grandes para cualquier tipo de Go, siempre que el resultado final quepa. const maxOffset = 1 << 100 compila, y maxOffset >> 98 es la constante 4. El error solo llega cuando el valor tiene que convertirse en un tipo concreto:

example.gogo
package 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 el compilador conoce el valor de cada constante, estas comprobaciones de rango ocurren en tiempo de compilación en lugar de desbordarse en silencio en tiempo de ejecución. Algunos ejemplos más:

CódigoError del compilador
var uploadLimit int32 = 1 << 40cannot use 1 << 40 (untyped int constant 1099511627776) as int32 value in variable declaration (overflows)
const maxConns uint8 = 300cannot use 300 (untyped int constant) as uint8 value in constant declaration (overflows)
var workers int = 2.5cannot use 2.5 (untyped float constant) as int value in variable declaration (truncated)

La comprobación solo mira el valor. var workers int = 2.0 compila, porque 2.0 es una constante float sin tipo cuyo valor es un número entero.

¿Qué es iota en Go?

iota es un identificador predeclarado que solo tiene sentido dentro de una declaración const. Vale 0 en la primera especificación de constante de un bloque const y sube de uno en uno con cada especificación posterior. Una especificación es una línea que declara nombres, así que las líneas en blanco y los comentarios no cuentan. Junto con la repetición implícita de la expresión anterior, un tipo y un solo iota bastan para numerar una lista entera:

example.gogo
type LogLevel int

const (
	LevelDebug LogLevel = iota // 0
	LevelInfo                  // 1
	LevelWarn                  // 2
	LevelError                 // 3
)

LevelInfo, LevelWarn y LevelError no tienen tipo ni valor propios, así que cada una repite LogLevel = iota con el siguiente valor de iota. Todas las constantes de la lista tienen el tipo LogLevel.

Cómo cuenta iota:

  1. iota vuelve a 0 al principio de cada bloque const. Dos bloques separados empiezan los dos en 0.
  2. Cuenta especificaciones, no nombres. Dos constantes declaradas en la misma línea ven el mismo iota.
  3. Una línea que usa _ también cuenta, así que el identificador en blanco se salta un valor.
  4. iota es una constante entera sin tipo, así que funciona en cualquier expresión constante, como iota + 1 o 1 << iota.

¿Cómo se empieza iota en 1 o se salta un valor?

Súmale uno, o descarta el primer valor con _. Empezar en 1 deja libre el valor cero para que signifique «sin definir»:

example.gogo
type Priority int

const (
	PriorityLow    Priority = iota + 1 // 1
	PriorityNormal                     // 2
	PriorityHigh                       // 3
)

Saltar con _ encaja cuando el primer valor no tiene sentido. Las unidades de tamaño de abajo se saltan iota == 0, porque 1 << 0 sería 1 byte:

example.gogo
type ByteSize int64

const (
	_           = iota // skip 0
	KB ByteSize = 1 << (10 * iota)
	MB
	GB
)

KB es 1 << 10 (1024), MB es 1 << 20 y GB es 1 << 30. Cada línea repite ByteSize = 1 << (10 * iota) con un iota mayor.

¿Cómo se definen flags de bits con iota?

Desplaza 1 a la izquierda iota posiciones. Así cada constante recibe su propio bit, y las combinas con |:

example.gogo
package 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
}

La biblioteca estándar usa el mismo patrón. log.Ldate, log.Ltime y log.Lshortfile son flags 1 << iota que combinas en log.SetFlags(log.LstdFlags | log.Lshortfile).

¿Tiene Go enums?

No como una característica aparte del lenguaje. No existe la palabra clave enum. Go construye los enums con tres piezas que ya tiene: un tipo con nombre, un bloque const con iota y un método String() para que los valores se impriman como nombres y no como números:

example.gogo
package 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)
}

Esto imprime warn LogLevel(9) y después level=error. fmt llama a String() cuando imprime un valor con Println, %v o %s. El caso default convierte un valor que no está en la lista, como LogLevel(9), en algo legible.

¿Cómo se generan métodos String con stringer?

Escribir ese switch a mano se vuelve tedioso, y se queda desactualizado cuando alguien añade una constante y se olvida del método. La herramienta stringer del equipo de Go lo genera. Añade una directiva go:generate junto al tipo y ejecuta go generate:

example.gogo
package 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))
}

go generate escribe orderstatus_string.go con un método String(), y el programa imprime Paid OrderStatus(42). -trimprefix quita de los nombres el prefijo común. El archivo generado también contiene una pequeña comprobación en tiempo de compilación. Cuando cambia el valor de una constante existente y el archivo no se ha regenerado, la compilación falla con un error como invalid argument: index 4 out of bounds [0:1]. Una constante añadida al final de la lista no se detecta, y se imprime como OrderStatus(5) hasta que vuelves a ejecutar go generate. Por eso muchos equipos lo ejecutan en CI y hacen fallar la compilación cuando cambian los archivos generados.

¿Por qué el valor cero de un enum debería ser Unknown?

Porque toda variable de Go empieza con su valor cero, y en un enum basado en int ese valor es 0. Un struct Order decodificado de un JSON sin campo status, o declarado con var o Order, tiene el estado 0. Si 0 es OrderStatusPending, un valor ausente parece un pedido pendiente real. Poner primero un OrderStatusUnknown explícito, o empezar en iota + 1, hace que el valor cero signifique «sin definir», y así el código puede rechazarlo.

¿Comprueba Go que un switch cubre todos los valores de un enum?

No. Un tipo enum sigue siendo un tipo entero normal, así que el compilador no sabe qué valores son válidos. var level LogLevel = 9 compila, porque 9 es una constante sin tipo que cabe en un int. Un switch que solo trata algunos de los valores también compila sin ninguna advertencia.

Los equipos que quieren esa comprobación añaden el linter exhaustive, que viene incluido en golangci-lint. Informa de cualquier switch sobre un tipo enum al que le falte alguna de las constantes del tipo. Un caso default, como el del método String() de arriba, trata en tiempo de ejecución los valores que no están en la lista.

¿Cuándo usar const y cuándo var en Go?

Usa const para valores que conoces al escribir el código y que nunca cambian: timeouts, límites, nombres de cabeceras, valores de enums. Usa var para todo lo que se calcula en tiempo de ejecución, se lee de la configuración o se construye con un tipo compuesto.

No puedes tomar la dirección de una constante, porque una constante no se guarda en una variable:

example.gogo
p := &maxBodyBytes
example.texttext
./main.go:6:8: invalid operation: cannot take address of maxBodyBytes (untyped int constant 1048576)

Esto se nota en las APIs que usan punteros para los campos opcionales. Desde Go 1.26, new acepta una expresión, así que new(maxBodyBytes) devuelve un *int que apunta a una copia del valor. En versiones anteriores, copia primero la constante en una variable y toma su dirección.

Tampoco puedes asignar un valor a una constante. maxBodyBytes = 2 << 20 falla con cannot assign to maxBodyBytes (neither addressable nor a map index expression).

Las constantes sin usar compilan. Una variable local sin usar es un error, declared and not used: retries, pero una constante sin usar se acepta, incluso dentro de una función. Linters como staticcheck siguen informando de las constantes no exportadas sin usar a nivel de paquete.

Las tablas de búsqueda son variables. Un map de estado a etiqueta no puede ser una constante, así que decláralo con var a nivel de paquete, o devuélvelo desde una función cuando no quieras que otro código lo modifique:

example.gogo
var orderStatusLabels = map[OrderStatus]string{
	OrderStatusPending:  "Awaiting payment",
	OrderStatusPaid:     "Paid",
	OrderStatusShipped:  "On the way",
	OrderStatusRefunded: "Refunded",
}

Cualquier código del paquete puede cambiar una var a nivel de paquete, y ese es el precio de poder guardar cualquier tipo. Cuando los valores son fijos y simples, una constante te da comprobaciones en tiempo de compilación y ningún riesgo de cambios en tiempo de ejecución. La palabra clave var en Go explica en detalle las variables, los valores cero y :=.

Dónde encaja LevelUpGo

LevelUpGo enseña Go con ejercicios que ejecutan código Go real en el navegador. Go Basics presenta const junto con las variables y los tipos. Go Language Deep Dives tiene una lección sobre iota y patrones con const en la que construyes un enum, flags de bits y unidades de tamaño en bytes. El Training Ground tiene ejercicios cortos e independientes para practicar fuera de un curso. Para las otras 24 palabras reservadas, consulta Palabras clave de Go: las 25 explicadas.

Preguntas frecuentes

¿Es const una palabra clave en Go?

Sí. const es una de las 25 palabras clave reservadas de Go, así que no puedes usarla como nombre de variable, de función ni de tipo. Inicia una declaración de constantes, ya sea una sola const name = value o un bloque agrupado const ( ... ).

¿Puede un slice o un map ser una constante en Go?

No. Las constantes solo pueden ser booleanos, números, cadenas y runes, o tipos con nombre basados en ellos. const allowedMethods = []string{"GET", "POST"} falla con is not constant. Usa una var a nivel de paquete para slices y maps, o una función que devuelva una copia nueva cuando quien la llama no deba modificar la compartida.

¿Qué es iota en Go?

iota es un identificador predeclarado que numera las líneas de un bloque const. Vale 0 en la primera línea, sube de uno en uno en cada línea siguiente y vuelve a empezar en cada bloque nuevo. Con un tipo con nombre y la repetición implícita, da a cada constante de una lista su propio valor, y así es como Go escribe los enums y los flags de bits.

¿Qué diferencia hay entre constantes con tipo y sin tipo en Go?

Una constante con tipo, como const maxRetries int32 = 3, tiene un tipo fijo y sigue las reglas normales de asignación. Una constante sin tipo, como const maxRetries = 3, no tiene tipo hasta que se usa, así que puede convertirse en un int, un int64, un float64 o un time.Duration. Las constantes sin tipo además conservan valores exactos, de precisión arbitraria, hasta que se convierten.

¿Tiene Go enums?

Go no tiene palabra clave enum, pero en la práctica sí tiene enums. Declaras un tipo con nombre como type OrderStatus int, listas sus valores en un bloque const con iota y añades un método String(), a menudo generado con stringer. El compilador no limita el tipo a los valores listados, así que valida la entrada y usa el linter exhaustive si necesitas que cada switch cubra todos los valores.

Fuentes

Escribe Go como un ingeniero sénior

Lecciones interactivas en tu navegador. Las primeras son gratis.

Prueba una lección gratisO crea una cuenta gratuita