O que é uma maneira idiomática de representar enums no Go?

522

Estou tentando representar um cromossomo simplificado, que consiste em N bases, cada uma das quais pode ser apenas uma {A, C, T, G}.

Gostaria de formalizar as restrições com um enum, mas estou me perguntando qual é a maneira mais idiomática de emular um enum no Go.

carbocação
fonte
4
Nos pacotes padrão, eles são representados como constantes. Veja golang.org/pkg/os/#pkg-constants
Denys Séguret
Relacionados / possível duplicata de golang: Criar um tipo de constante e Restrição de Valores do Tipo
icza
7
@icza Esta pergunta foi feita três anos antes. Isso não pode ser uma duplicata, assumindo que a flecha do tempo esteja funcionando corretamente.
carbocation
Confira o melhor guia para ir enums .
Inanc Gumus

Respostas:

658

Citando as especificações de idioma: Iota

Dentro de uma declaração constante, o identificador pré-declarado iota representa constantes inteiras sem tipo sucessivas. É redefinido para 0 sempre que a palavra reservada const aparece na fonte e é incrementada após cada ConstSpec. Pode ser usado para construir um conjunto de constantes relacionadas:

const (  // iota is reset to 0
        c0 = iota  // c0 == 0
        c1 = iota  // c1 == 1
        c2 = iota  // c2 == 2
)

const (
        a = 1 << iota  // a == 1 (iota has been reset)
        b = 1 << iota  // b == 2
        c = 1 << iota  // c == 4
)

const (
        u         = iota * 42  // u == 0     (untyped integer constant)
        v float64 = iota * 42  // v == 42.0  (float64 constant)
        w         = iota * 42  // w == 84    (untyped integer constant)
)

const x = iota  // x == 0 (iota has been reset)
const y = iota  // y == 0 (iota has been reset)

Dentro de um ExpressionList, o valor de cada iota é o mesmo porque é incrementado apenas após cada ConstSpec:

const (
        bit0, mask0 = 1 << iota, 1<<iota - 1  // bit0 == 1, mask0 == 0
        bit1, mask1                           // bit1 == 2, mask1 == 1
        _, _                                  // skips iota == 2
        bit3, mask3                           // bit3 == 8, mask3 == 7
)

Este último exemplo explora a repetição implícita da última lista de expressões não vazias.


Portanto, seu código pode ser como

const (
        A = iota
        C
        T
        G
)

ou

type Base int

const (
        A Base = iota
        C
        T
        G
)

se você deseja que as bases sejam um tipo separado do int.

zzzz
fonte
16
ótimos exemplos (não me lembrei do comportamento exato do iota - quando é incrementado - a partir das especificações). Pessoalmente, eu gostaria de dar um tipo de uma enumeração, por isso pode ser verificado tipo quando usado como argumento, campo, etc.
mna
16
@Jnml muito interessante. Mas estou meio decepcionado que a verificação estática de tipo pareça frouxa, por exemplo, nada me impede de usar a Base n ° 42 que nunca existiu: play.golang.org/p/oH7eiXBxhR
Deleplace
4
Go não tem nenhum conceito de tipos de subfaixas numéricas, como, por exemplo, o de Pascal, portanto Ord(Base)não está limitado a, 0..3mas tem os mesmos limites que seu tipo numérico subjacente. É uma escolha de design de linguagem, compromisso entre segurança e desempenho. Considere verificações de limite de tempo de execução "seguras" sempre que tocar em um Basevalor digitado. Ou como se deve definir o comportamento de Basevalor "transbordante" para aritmética e para ++e --? E
mail
7
Para complementar no jnml, mesmo semântica, nada na linguagem diz que os consts definidos como Base representam toda a faixa de Base válida, apenas diz que esses consts específicos são do tipo Base. Mais constantes também podem ser definidas em outros lugares como Base, e nem sequer são mutuamente exclusivas (por exemplo, const Z Base = 0 pode ser definida e seria válida).
quer
10
Você pode usar iota + 1para não começar a 0.
Marçal Juan
87

Referindo-se à resposta do jnml, você pode impedir novas instâncias do tipo Base não exportando o tipo Base (ex .: escreva-o em minúsculas). Se necessário, você pode criar uma interface exportável que possua um método que retorne um tipo de base. Essa interface pode ser usada em funções externas que lidam com bases, ou seja,

package a

type base int

const (
    A base = iota
    C
    T
    G
)


type Baser interface {
    Base() base
}

// every base must fulfill the Baser interface
func(b base) Base() base {
    return b
}


func(b base) OtherMethod()  {
}

package main

import "a"

// func from the outside that handles a.base via a.Baser
// since a.base is not exported, only exported bases that are created within package a may be used, like a.A, a.C, a.T. and a.G
func HandleBasers(b a.Baser) {
    base := b.Base()
    base.OtherMethod()
}


// func from the outside that returns a.A or a.C, depending of condition
func AorC(condition bool) a.Baser {
    if condition {
       return a.A
    }
    return a.C
}

Dentro do pacote principal a.Baserestá efetivamente como um enum agora. Somente dentro de um pacote você pode definir novas instâncias.

metakeule
fonte
10
Seu método parece perfeito para os casos em que baseé usado apenas como receptor de método. Se o seu apacote expor uma função usando um parâmetro do tipo base, isso se tornaria perigoso. De fato, o usuário poderia chamá-lo apenas com o valor literal 42, que a função aceitaria, basepois pode ser convertida para um int. Para evitar isso, faça baseum struct: type base struct{value:int}. Problema: você não pode mais declarar bases como constantes, apenas variáveis ​​de módulo. Mas 42 nunca serão convertidos para um basedesse tipo.
Niriel 29/09
6
@ metaetaule Estou tentando entender o seu exemplo, mas sua escolha em nomes de variáveis ​​tornou extremamente difícil.
precisa saber é o seguinte
1
Este é um dos meus bugbears em exemplos. FGS, percebo que é tentador, mas não nomeie a variável da mesma forma que o tipo!
Graham Nicholls
27

Você pode fazer isso assim:

type MessageType int32

const (
    TEXT   MessageType = 0
    BINARY MessageType = 1
)

Com este código, o compilador deve verificar o tipo de enum

Azat
fonte
5
As constantes são geralmente escritas em camelcase normal, nem todas em maiúsculas. A letra maiúscula inicial significa que a variável é exportada, o que pode ou não ser o que você deseja.
425nesp
1
Percebi no código-fonte Go que existe uma mistura em que algumas vezes as constantes são todas maiúsculas e outras, as camelcase. Você tem uma referência a uma especificação?
Jeremy Gailor
@ JeremyGailor Acho que o 425nesp está apenas observando que a preferência normal é que os desenvolvedores os usem como constantes não exportadas, portanto, use camelcase. Se o desenvolvedor determinar que deve ser exportado, fique à vontade para usar todas as maiúsculas ou maiúsculas, porque não há preferência estabelecida. Veja as Recomendações de Revisão do Código Golang e a Seção Efetiva de Ir sobre Constantes
waynethec
Há uma preferência. Assim como variáveis, funções, tipos e outros, os nomes de constantes devem ser mixedCaps ou MixedCaps, não ALLCAPS. Fonte: Comentários de Revisão de Código Go .
Rodolfo Carvalho
Observe que, por exemplo, funções que esperam um MessageType aceitarão com alegria consts numéricos sem tipo, por exemplo, 7. Além disso, você pode converter qualquer int32 para MessageType. Se você está ciente disso, acho que essa é a maneira mais idiomática em andamento.
Kosta
23

É verdade que os exemplos acima de usar conste iotasão as formas mais idiomáticas de representar enums primitivas no Go. Mas e se você estiver procurando uma maneira de criar uma enumeração mais completa, semelhante ao tipo que você veria em outra linguagem como Java ou Python?

Uma maneira muito simples de criar um objeto que começa a se parecer com um enum de string no Python seria:

package main

import (
    "fmt"
)

var Colors = newColorRegistry()

func newColorRegistry() *colorRegistry {
    return &colorRegistry{
        Red:   "red",
        Green: "green",
        Blue:  "blue",
    }
}

type colorRegistry struct {
    Red   string
    Green string
    Blue  string
}

func main() {
    fmt.Println(Colors.Red)
}

Suponha que você também queira alguns métodos utilitários, como Colors.List(), e Colors.Parse("red"). E suas cores eram mais complexas e precisavam ser uma estrutura. Então você pode fazer algo parecido com isto:

package main

import (
    "errors"
    "fmt"
)

var Colors = newColorRegistry()

type Color struct {
    StringRepresentation string
    Hex                  string
}

func (c *Color) String() string {
    return c.StringRepresentation
}

func newColorRegistry() *colorRegistry {

    red := &Color{"red", "F00"}
    green := &Color{"green", "0F0"}
    blue := &Color{"blue", "00F"}

    return &colorRegistry{
        Red:    red,
        Green:  green,
        Blue:   blue,
        colors: []*Color{red, green, blue},
    }
}

type colorRegistry struct {
    Red   *Color
    Green *Color
    Blue  *Color

    colors []*Color
}

func (c *colorRegistry) List() []*Color {
    return c.colors
}

func (c *colorRegistry) Parse(s string) (*Color, error) {
    for _, color := range c.List() {
        if color.String() == s {
            return color, nil
        }
    }
    return nil, errors.New("couldn't find it")
}

func main() {
    fmt.Printf("%s\n", Colors.List())
}

Nesse ponto, com certeza funciona, mas você pode não gostar de como definir cores repetidamente. Se, nesse ponto, você quiser eliminar isso, use tags na sua estrutura e faça algumas reflexões sofisticadas para configurá-la, mas espero que isso seja suficiente para cobrir a maioria das pessoas.

Becca Petrin
fonte
19

A partir do Go 1.4, a go generateferramenta foi introduzida juntamente com o stringercomando que torna seu enum facilmente depurável e imprimível.

Moshe Revah
fonte
Você sabe que é uma solução oposta. Quero dizer string -> MyType. Como a solução unidirecional está longe de ser ideal. Aqui está a essência que faz o que eu quero - mas escrever à mão é fácil de cometer erros.
SR
11

Estou certo de que temos muitas boas respostas aqui. Mas, apenas pensei em adicionar a maneira como usei tipos enumerados

package main

import "fmt"

type Enum interface {
    name() string
    ordinal() int
    values() *[]string
}

type GenderType uint

const (
    MALE = iota
    FEMALE
)

var genderTypeStrings = []string{
    "MALE",
    "FEMALE",
}

func (gt GenderType) name() string {
    return genderTypeStrings[gt]
}

func (gt GenderType) ordinal() int {
    return int(gt)
}

func (gt GenderType) values() *[]string {
    return &genderTypeStrings
}

func main() {
    var ds GenderType = MALE
    fmt.Printf("The Gender is %s\n", ds.name())
}

Essa é de longe uma das maneiras idiomáticas em que poderíamos criar tipos enumerados e usar no Go.

Editar:

Adicionando outra maneira de usar constantes para enumerar

package main

import (
    "fmt"
)

const (
    // UNSPECIFIED logs nothing
    UNSPECIFIED Level = iota // 0 :
    // TRACE logs everything
    TRACE // 1
    // INFO logs Info, Warnings and Errors
    INFO // 2
    // WARNING logs Warning and Errors
    WARNING // 3
    // ERROR just logs Errors
    ERROR // 4
)

// Level holds the log level.
type Level int

func SetLogLevel(level Level) {
    switch level {
    case TRACE:
        fmt.Println("trace")
        return

    case INFO:
        fmt.Println("info")
        return

    case WARNING:
        fmt.Println("warning")
        return
    case ERROR:
        fmt.Println("error")
        return

    default:
        fmt.Println("default")
        return

    }
}

func main() {

    SetLogLevel(INFO)

}
wandermonk
fonte
2
Você pode declarar constantes com valores de sequência. Na IMO, é mais fácil fazer isso se você deseja exibi-los e não precisar realmente do valor numérico.
Cbednarski 23/05/19
4

Aqui está um exemplo que será útil quando houver muitas enumerações. Ele usa estruturas em Golang e se baseia em Princípios Orientados a Objetos para amarrá-los todos juntos em um pequeno embrulho. Nenhum do código subjacente será alterado quando uma nova enumeração for adicionada ou excluída. O processo é:

  • Defina uma estrutura de enumeração para enumeration items: EnumItem . Tem um número inteiro e um tipo de sequência.
  • Defina o enumerationcomo uma lista de enumeration items: Enum
  • Crie métodos para a enumeração. Alguns foram incluídos:
    • enum.Name(index int): retorna o nome para o índice especificado.
    • enum.Index(name string): retorna o nome para o índice especificado.
    • enum.Last(): retorna o índice e o nome da última enumeração
  • Adicione suas definições de enumeração.

Aqui está um código:

type EnumItem struct {
    index int
    name  string
}

type Enum struct {
    items []EnumItem
}

func (enum Enum) Name(findIndex int) string {
    for _, item := range enum.items {
        if item.index == findIndex {
            return item.name
        }
    }
    return "ID not found"
}

func (enum Enum) Index(findName string) int {
    for idx, item := range enum.items {
        if findName == item.name {
            return idx
        }
    }
    return -1
}

func (enum Enum) Last() (int, string) {
    n := len(enum.items)
    return n - 1, enum.items[n-1].name
}

var AgentTypes = Enum{[]EnumItem{{0, "StaffMember"}, {1, "Organization"}, {1, "Automated"}}}
var AccountTypes = Enum{[]EnumItem{{0, "Basic"}, {1, "Advanced"}}}
var FlagTypes = Enum{[]EnumItem{{0, "Custom"}, {1, "System"}}}
Aaron
fonte