Menu

Enum en Golang: cómo crear enums con const e iota

Go no tiene la palabra clave enum. Esta página muestra el sustituto idiomático: un tipo con nombre más un bloque const con iota, y cómo añadir String(), validación, parseo, flags de bits y soporte para JSON.

Esta página incluye editores ejecutables: edita, ejecuta y ve el resultado al instante.

Go no tiene la palabra clave enum. Un enum se construye con dos piezas: un tipo con nombre y un bloque const con valores de ese tipo, numerados con iota.

Sunday vale 0, y cada línea siguiente vale uno más. El tipo con nombre Weekday es lo que convierte esto en un enum y no en una lista de números: isWeekend dice en su firma lo que espera, y se pueden añadir métodos al tipo. La salida imprime 1 5 6 porque todavía nada le dice a Go cómo mostrar un Weekday como texto. Eso viene más abajo.

Cómo funciona iota

iota es un contador que el compilador proporciona dentro de un bloque const. Dos reglas explican todos los trucos basados en él:

  1. iota vale el índice de la línea actual en el bloque, empezando en 0, y vuelve a 0 en cada bloque const nuevo.
  2. Una línea sin = expression repite la expresión y el tipo de la línea anterior, evaluados con el nuevo iota.

Así que Monday en el ejemplo anterior es una abreviatura de Monday Weekday = iota, con iota valiendo ahora 1. Como la expresión se repite, puede ser cualquier expresión constante, no solo iota:

iota cuenta líneas, no nombres: dos constantes en la misma línea comparten un mismo valor de iota, como muestran X e Y.

Empezar en 1, y por qué quizá no quieras

Una variable de un tipo enum que nadie ha asignado contiene 0, su valor cero. Si 0 es un valor real como Sunday, no puedes distinguir "el usuario eligió domingo" de "el campo nunca se rellenó". Tres arreglos habituales:

La opción 1 es la más común en código de producción, y los enums de Go generados por protobuf la siguen (..._UNSPECIFIED = 0). Así el valor cero significa algo honesto.

Saltar valores

El identificador vacío _ consume un valor de iota sin crear un nombre. Úsalo para dejar huecos, por ejemplo para coincidir con números definidos por un protocolo o para retirar un valor sin renumerar el resto:

type Opcode byte

const (
	OpContinue Opcode = iota // 0
	OpText                   // 1
	OpBinary                 // 2
	_                        // 3, reserved
	_                        // 4, reserved
	_                        // 5, reserved
	_                        // 6, reserved
	_                        // 7, reserved
	OpClose                  // 8
	OpPing                   // 9
	OpPong                   // 10
)

Cuando los números los fija una especificación externa, como estos opcodes de WebSocket, escribirlos explícitamente (OpClose Opcode = 8) suele ser más claro que contar huecos. iota es para valores cuyo número exacto te da igual.

Nunca reordenes ni insertes en una lista con iota cuyos números se guardan en una base de datos o un archivo, o se envían por la red. Añadir una línea en medio desplaza todos los valores posteriores. Añade los valores nuevos al final o asigna los números explícitamente.

Añadir un método String

Dale al tipo un método String() string y fmt lo usará con %v, %s y Println:

Dos detalles de ese método importan:

  • La comprobación de límites. Sin ella, Weekday(9).String() provoca un panic por índice fuera de rango, y acabará pasando, porque nada impide que alguien cree Weekday(9).
  • El int(d) dentro de Sprintf. Formatear d directamente con %d está bien, pero formatearlo con %v volvería a llamar a String() y entraría en recursión hasta desbordar la pila.

%d sigue imprimiendo el número, así que tienes las dos formas: Wednesday is day 3.

Generar String con stringer

Para listas largas, la herramienta stringer escribe el método por ti:

//go:generate go run golang.org/x/tools/cmd/stringer@latest -type=Weekday
go generate ./...

Crea weekday_string.go con una implementación compacta de String(), más una comprobación en tiempo de compilación que rompe la compilación si las constantes cambian sin regenerar. El flag -linecomment usa un comentario al final de la línea como nombre, lo que viene bien para nombres con espacios.

Validar valores

Un enum de Go no es cerrado. Cualquier valor del tipo subyacente se convierte a él, y las constantes sin tipo se convierten implícitamente:

var d Weekday = 42     // compiles
d = Weekday(userInput) // compiles

Así que comprueba los valores que vienen de fuera de tu código (JSON, bases de datos, flags, otros paquetes):

El centinela no exportado colorCount al final del bloque mantiene correcto IsValid cuando añades colores nuevos, ya que siempre queda uno por encima del último valor real.

switch sobre un enum

Los enums se suelen consumir con un switch. Go no comprueba que un switch cubra todos los valores, así que añade un default que informe de la sorpresa:

func (c Color) Hex() string {
	switch c {
	case Red:
		return "#ff0000"
	case Green:
		return "#00ff00"
	case Blue:
		return "#0000ff"
	default:
		return "#000000"
	}
}

El linter de terceros exhaustive (incluido en golangci-lint) informa de los switch sobre tipos enum a los que les falta un caso, lo que te da casi todo lo que ofrece la comprobación exhaustiva de enums en otros lenguajes.

Enums de flags de bits

Cuando los valores se combinan, como los permisos, usa un bit por valor con 1 << iota:

| combina flags, & los comprueba y &^ (el operador AND NOT de Go) los borra. Aquí conviene un tipo subyacente sin signo: uint8 admite 8 flags, uint64 admite 64.

Enums de strings

Cuando el valor se guarda o se envía como texto de todos modos, un tipo basado en string evita la capa de conversión:

type Env string

const (
	EnvDev     Env = "dev"
	EnvStaging Env = "staging"
	EnvProd    Env = "prod"
)

Los valores se imprimen y se serializan de forma legible sin método String(), y una columna de la base de datos guarda "prod" en lugar de un número que depende del orden de declaración. Las contrapartidas: las comparaciones son comparaciones de strings, los flags de bits son imposibles y la validación sigue siendo cosa tuya, porque Env("banana") también compila.

Enums y JSON

Un enum entero se serializa como número por defecto. Para leer y escribir nombres, implementa encoding.TextMarshaler y encoding.TextUnmarshaler. encoding/json los usa para los valores y para las claves de los maps:

MarshalText tiene un receptor por valor para que funcione tanto con Level como con *Level; UnmarshalText necesita un receptor puntero porque cambia el valor. Esos mismos dos métodos hacen que el tipo funcione con TextVar del paquete flag y con la mayoría de librerías de configuración.

Trampas

  • Conversión implícita de literales. Una función que recibe un Weekday también acepta la constante sin tipo 42. Solo se rechazan los valores con tipo de otro tipo.
  • Olvidar el tipo en la primera línea. En const ( Red = iota; Green; Blue ) las tres son constantes enteras sin tipo, no valores Color, así que los métodos de Color no se aplican a ellas. Escribe Red Color = iota para que la expresión repetida lleve el tipo.
  • Reordenar enums que se guardan. Insertar un valor en medio de un bloque iota cambia en silencio los números ya guardados en otra parte.
  • Recursión en String. Dentro de String(), nunca formatees el receptor con %v ni %s. Conviértelo antes al tipo subyacente.

Preguntas frecuentes

¿Tiene Go enums?

No como característica del lenguaje. No existe la palabra clave enum. El sustituto idiomático es un tipo con nombre más un bloque de constantes con tipo, normalmente numeradas con iota:

type Color int

const (
	Red Color = iota
	Green
	Blue
)

El tipo te da firmas legibles y un sitio donde colgar métodos como String(). No impide que alguien escriba Color(42), así que valida los valores que vienen de fuera.

¿Qué es iota en Go?

iota es un identificador predeclarado que vale el índice de la línea actual (especificación de constante) dentro de un bloque const, empezando en 0. Vuelve a 0 en cada bloque const nuevo. Cuando una línea omite su expresión, Go repite la expresión anterior con el siguiente iota, y por eso Red = iota; Green; Blue produce 0, 1, 2.

¿Cómo hago que iota empiece en 1?

Escribe First Kind = iota + 1 en la primera línea, o salta el cero con el identificador vacío: _ = iota y luego First. Muchos programadores de Go prefieren conservar el 0 y llamarlo Unknown o Invalid, para que una variable sin inicializar (cuyo valor cero es 0) no parezca una opción real.

¿Cómo imprimo un enum como string en Go?

Dale al tipo un método String() string. fmt lo llama con %v, %s y Println, así que fmt.Println(Green) imprime Green en lugar de 1. Puedes escribir el método a mano con un switch o un array, o generarlo con go run golang.org/x/tools/cmd/stringer@latest -type=Color.

¿Cómo convierto un string en un enum en Go?

Escribe una función de parseo que busque el string, normalmente en un map[string]Color o con un switch, y devuelva un error para las entradas desconocidas: func ParseColor(s string) (Color, error). Implementar UnmarshalText con la misma lógica hace que JSON, los flags y los cargadores de configuración la usen automáticamente.

Coddy programming languages illustration

Aprende a programar con Coddy

COMENZAR