Go não tem a palavra-chave enum. Você monta um enum com duas peças: um tipo nomeado e um bloco const de valores desse tipo, numerados com iota.
Sunday vale 0, e cada linha seguinte vale um a mais. O tipo nomeado Weekday é o que faz disso um enum, e não uma lista de números: isWeekend diz na assinatura o que espera, e métodos podem ser acrescentados ao tipo. A saída imprime 1 5 6 porque nada ainda diz ao Go como exibir um Weekday como texto. Isso vem mais abaixo.
Como o iota funciona
iota é um contador que o compilador oferece dentro de um bloco const. Duas regras explicam todos os truques construídos em cima dele:
iotavale o índice da linha atual no bloco, começando em 0, e volta a 0 em cada novo blococonst.- Uma linha sem
= expressãorepete a expressão e o tipo da linha anterior, avaliados com o novoiota.
Então Monday, acima, é um atalho para Monday Weekday = iota, com iota valendo 1. Como a expressão é repetida, ela pode ser qualquer expressão constante, não só iota:
iota conta linhas, não nomes: duas constantes na mesma linha compartilham um mesmo valor de iota, como mostram X e Y.
Começar em 1, e por que talvez não
Uma variável de um tipo enum que ninguém definiu guarda 0, o seu valor zero. Se 0 é um valor real como Sunday, você não consegue diferenciar "o usuário escolheu domingo" de "o campo nunca foi preenchido". Três correções comuns:
A opção 1 é a mais comum em código de produção, e os enums Go gerados pelo protobuf a seguem (..._UNSPECIFIED = 0). O valor zero passa a significar algo honesto.
Pulando valores
O identificador em branco _ consome um valor de iota sem criar um nome. Use-o para deixar lacunas, por exemplo para bater com números definidos por um protocolo ou para aposentar um valor sem renumerar o 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
)
Quando os números são fixados por uma especificação externa, como estes opcodes do WebSocket, escrevê-los explicitamente (OpClose Opcode = 8) costuma ser mais claro do que contar linhas em branco. O iota é para valores cujos números exatos não importam.
Nunca reordene nem insira itens em uma lista de iota cujos números estejam guardados em um banco de dados, em um arquivo ou enviados pela rede. Acrescentar uma linha no meio desloca todos os valores seguintes. Acrescente valores novos no final, ou atribua os números explicitamente.
Acrescentando um método String
Dê ao tipo um método String() string e o fmt passa a usá-lo em %v, %s e Println:
Dois detalhes desse método importam:
- A verificação de limites. Sem ela,
Weekday(9).String()causa panic de índice fora da faixa, e isso vai acontecer em algum momento, porque nada impede quem chama de criarWeekday(9). - O
int(d)dentro doSprintf. Formatar o própriodcom%dnão tem problema, mas formatá-lo com%vchamariaString()de novo e entraria em recursão até estourar a pilha.
%d continua imprimindo o número, então você tem as duas formas: Wednesday is day 3.
Gerando o String com o stringer
Para listas longas, a ferramenta stringer escreve o método por você:
//go:generate go run golang.org/x/tools/cmd/stringer@latest -type=Weekday
go generate ./...
Ela cria weekday_string.go com uma implementação compacta de String(), mais uma verificação em tempo de compilação que quebra o build se as constantes mudarem sem que o arquivo seja gerado de novo. A flag -linecomment usa um comentário no fim da linha como nome, o que ajuda com nomes que têm espaços.
Validando valores
Um enum em Go não é fechado. Qualquer valor do tipo subjacente pode ser convertido para ele, e constantes não tipadas são convertidas implicitamente:
var d Weekday = 42 // compiles
d = Weekday(userInput) // compiles
Então verifique os valores que vêm de fora do seu código (JSON, bancos de dados, flags, outros pacotes):
A sentinela não exportada colorCount no fim do bloco mantém o IsValid correto quando você acrescenta cores novas, já que ela sempre fica uma posição depois do último valor real.
switch sobre um enum
Enums normalmente são consumidos por um switch. Go não verifica se um switch cobre todos os valores, então acrescente um default que reporte a surpresa:
func (c Color) Hex() string {
switch c {
case Red:
return "#ff0000"
case Green:
return "#00ff00"
case Blue:
return "#0000ff"
default:
return "#000000"
}
}
O linter de terceiros exhaustive (incluído no golangci-lint) reporta switches sobre tipos enum aos quais falta algum case, o que dá a maior parte do que uma verificação de enum exaustiva dá em outras linguagens.
Enums de bit flags
Quando os valores se combinam, como permissões, use um bit por valor com 1 << iota:
| combina flags, & as testa e &^ (o operador AND NOT do Go) as limpa. Um tipo subjacente sem sinal é a escolha certa aqui: uint8 comporta 8 flags, uint64 comporta 64.
Enums de string
Quando o valor é guardado ou enviado como texto de qualquer forma, um tipo baseado em string evita a camada de conversão:
type Env string
const (
EnvDev Env = "dev"
EnvStaging Env = "staging"
EnvProd Env = "prod"
)
Os valores são impressos e serializados de forma legível sem nenhum método String(), e uma coluna do banco guarda "prod" em vez de um número que depende da ordem de declaração. Os contras: as comparações são comparações de string, bit flags são impossíveis e a validação continua sendo sua, já que Env("banana") também compila.
Enums e JSON
Um enum inteiro é serializado como número por padrão. Para ler e escrever nomes, implemente encoding.TextMarshaler e encoding.TextUnmarshaler. O encoding/json os usa para valores e para chaves de map:
MarshalText tem receiver de valor, então funciona tanto em Level quanto em *Level; UnmarshalText precisa de receiver ponteiro porque altera o valor. Os mesmos dois métodos fazem o tipo funcionar com o TextVar do pacote flag e com a maioria das bibliotecas de configuração.
Armadilhas
- Conversão implícita de literais. Uma função que recebe um
Weekdaytambém aceita a constante não tipada42. Só valores tipados de outro tipo são rejeitados. - Esquecer o tipo na primeira linha. Em
const ( Red = iota; Green; Blue )as três são constantes inteiras não tipadas, não valoresColor, então os métodos deColornão se aplicam a elas. EscrevaRed Color = iotapara que a expressão repetida carregue o tipo. - Reordenar enums guardados. Inserir um valor no meio de um bloco
iotamuda em silêncio os números já gravados em outros lugares. - Recursão no String. Dentro de
String(), nunca formate o receiver com%vou%s. Converta para o tipo subjacente antes.
Perguntas frequentes
Go tem enums?
Não como recurso da linguagem. Não existe a palavra-chave enum. O substituto idiomático é um tipo nomeado mais um bloco de constantes tipadas, normalmente numeradas com iota:
type Color int
const (
Red Color = iota
Green
Blue
)
O tipo dá assinaturas legíveis e um lugar para pendurar métodos como String(). Ele não impede ninguém de escrever Color(42), então valide os valores que vêm de fora.
O que é iota em Go?
iota é um identificador pré-declarado que vale o índice da linha atual (especificação de constante) dentro de um bloco const, começando em 0. Ele volta a 0 em cada novo bloco const. Quando uma linha omite a expressão, o Go repete a expressão anterior com o próximo iota, e é isso que faz Red = iota; Green; Blue gerar 0, 1, 2.
Como fazer o iota começar em 1?
Escreva First Kind = iota + 1 na primeira linha, ou pule o zero com um identificador em branco: _ = iota e depois First. Muitos programadores Go preferem manter o 0 e chamá-lo de Unknown ou Invalid, para que uma variável não inicializada (cujo valor zero é 0) fique claramente fora das opções reais.
Como imprimir um enum como string em Go?
Dê ao tipo um método String() string. O fmt o chama para %v, %s e Println, então fmt.Println(Green) imprime Green em vez de 1. Você pode escrever o método à mão com um switch ou um array, ou gerá-lo com go run golang.org/x/tools/cmd/stringer@latest -type=Color.
Como converter uma string em enum em Go?
Escreva uma função de parsing que procura a string, normalmente em um map[string]Color ou em um switch, e devolve um erro para entradas desconhecidas: func ParseColor(s string) (Color, error). Implementar UnmarshalText com a mesma lógica faz JSON, flags e carregadores de configuração usarem a função automaticamente.