¿Cuál es una forma idiomática de representar enumeraciones en Go?

523

Estoy tratando de representar un cromosoma simplificado, que consiste en N bases, cada una de las cuales solo puede ser una {A, C, T, G}.

Me gustaría formalizar las restricciones con una enumeración, pero me pregunto cuál es la forma más idiomática de emular una enumeración en Go.

carbocatión
fuente
44
En los paquetes estándar, se representan como constantes. Ver golang.org/pkg/os/#pkg-constants
Denys Séguret
77
@icza Esta pregunta se hizo 3 años antes. Esto no puede ser un duplicado de ese, suponiendo que la flecha del tiempo funcione correctamente.
carbocation
Echa un vistazo a la guía definitiva para ir enumeraciones .
Inanc Gumus

Respuestas:

659

Citando de las especificaciones de idioma: Iota

Dentro de una declaración constante, el identificador predefinido iota representa constantes enteras sin tipo sucesivas. Se restablece a 0 cuando la palabra reservada const aparece en la fuente y se incrementa después de cada ConstSpec. Se puede usar para construir un 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 una ExpressionList, el valor de cada iota es el mismo porque solo se incrementa después de 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 ejemplo explota la repetición implícita de la última lista de expresiones no vacías.


Entonces su código podría ser como

const (
        A = iota
        C
        T
        G
)

o

type Base int

const (
        A Base = iota
        C
        T
        G
)

si quieres que las bases sean un tipo separado de int.

zzzz
fuente
16
excelentes ejemplos (no recordé el comportamiento exacto de iota, cuando se incrementa, de la especificación). Personalmente, me gusta dar un tipo a una enumeración, para que pueda verificarse cuando se usa como argumento, campo, etc.
mna
16
Muy interesante @jnml. Pero estoy un poco decepcionado de que la verificación de tipos estática parezca floja, por ejemplo, nada me impide usar la Base n ° 42 que nunca existió: play.golang.org/p/oH7eiXBxhR
Deleplace
44
Go no tiene ningún concepto de tipos de subrango numérico, como por ejemplo, el de Pascal, por Ord(Base)lo que no se limita a, 0..3sino que tiene los mismos límites que su tipo numérico subyacente. Es una elección de diseño de lenguaje, compromiso entre seguridad y rendimiento. Considere las comprobaciones "seguras" de tiempo de ejecución cada vez que toque un Basevalor escrito. ¿O cómo se debe definir el comportamiento de Basevalor de 'desbordamiento' para la aritmética y para ++y --? Etc.
zzzz
77
Para complementar en jnml, incluso semánticamente, nada en el lenguaje dice que los consts definidos como Base representan el rango completo de Base válida, solo dice que estos consts particulares son de tipo Base. También se podrían definir más constantes en otro lugar como Base, y ni siquiera es mutuamente excluyente (por ejemplo, const Z Base = 0 se podría definir y sería válido).
mna
10
Puede usar iota + 1para no comenzar en 0.
Marçal Juan
87

Refiriéndose a la respuesta de jnml, puede evitar nuevas instancias de tipo Base al no exportar el tipo Base (es decir, escribirlo en minúsculas). Si es necesario, puede crear una interfaz exportable que tenga un método que devuelva un tipo base. Esta interfaz podría usarse en funciones externas que se ocupan de Bases, es decir

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 del paquete principal a.Baseres efectivamente como una enumeración ahora. Solo dentro del paquete puede definir nuevas instancias.

metakeule
fuente
10
Su método parece perfecto para los casos en los que basese usa solo como receptor de métodos. Si su apaquete exponga una función que toma un parámetro de tipo base, entonces se volvería peligroso. De hecho, el usuario podría simplemente llamarlo con el valor literal 42, que la función aceptaría baseya que se puede convertir a un int. Para evitar esto, hacer baseun struct: type base struct{value:int}. Problema: ya no puede declarar bases como constantes, solo variables de módulo. Pero 42 nunca se lanzará a un basede ese tipo.
Niriel
66
@metakeule Estoy tratando de entender su ejemplo, pero su elección en nombres de variables lo ha hecho extremadamente difícil.
anon58192932
1
Este es uno de mis errores en los ejemplos. FGS, me doy cuenta de que es tentador, ¡pero no nombre la variable igual que el tipo!
Graham Nicholls
27

Puedes hacerlo así:

type MessageType int32

const (
    TEXT   MessageType = 0
    BINARY MessageType = 1
)

Con este código el compilador debería verificar el tipo de enumeración

Azat
fuente
55
Las constantes generalmente se escriben en camello normal, no todas en mayúscula. La letra mayúscula inicial significa que la variable se exporta, que puede o no ser lo que desea.
425nesp
1
He notado que en el código fuente de Go hay una mezcla en la que a veces las constantes son todas mayúsculas y otras son camelcase. ¿Tiene una referencia a una especificación?
Jeremy Gailor
@JeremyGailor Creo 425nesp se acaba de señalar que la preferencia normal es que los desarrolladores utilizan como no exportadas constantes a fin de utilizar CamelCase. Si el desarrollador determina que debe exportarse, no dude en utilizar mayúsculas o mayúsculas porque no hay una preferencia establecida. Vea las recomendaciones de revisión del código de Golang y la sección efectiva de Go sobre constantes
waynethec
Hay una preferencia Al igual que las variables, funciones, tipos y otros, los nombres constantes deben ser mixedCaps o MixedCaps, no ALLCAPS. Fuente: Go Code Review Comments .
Rodolfo Carvalho
Tenga en cuenta que, por ejemplo, las funciones que esperan un MessageType aceptarán felizmente concursos numéricos sin tipo, por ejemplo, 7. Además, puede convertir cualquier int32 en MessageType. Si eres consciente de esto, creo que esta es la forma más idiomática.
Kosta hace
23

Es cierto que los ejemplos anteriores de uso consty iotason las formas más idiomáticas de representar enumeraciones primitivas en Go. Pero, ¿qué sucede si está buscando una manera de crear una enumeración más completa, similar al tipo que vería en otro lenguaje como Java o Python?

Una forma muy simple de crear un objeto que comienza a verse y sentirse como una enumeración de cadena en Python sería:

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)
}

Supongamos que también desea algunos métodos de utilidad, como Colors.List(), y Colors.Parse("red"). Y sus colores eran más complejos y necesitaban ser una estructura. Entonces podrías hacer algo así:

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())
}

En ese momento, seguro que funciona, pero es posible que no le guste cómo debe definir los colores de forma repetitiva. Si en este punto desea eliminar eso, puede usar etiquetas en su estructura y reflexionar un poco para configurarlo, pero con suerte esto es suficiente para cubrir a la mayoría de las personas.

Becca Petrin
fuente
19

A partir de Go 1.4, la go generateherramienta se ha introducido junto con el stringercomando que hace que su enumeración sea fácilmente depurable e imprimible.

Moshe Revah
fuente
¿Sabes que es una solución opuesta? Me refiero a la cadena -> MyType. Dado que la solución unidireccional está lejos de ser ideal. Aquí hay algunos conceptos básicos que hacen lo que quiero, pero escribir a mano es fácil de cometer errores.
SR
11

Estoy seguro de que tenemos muchas buenas respuestas aquí. Pero, solo pensé en agregar la forma en que he usado 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())
}

Esta es, con mucho, una de las formas idiomáticas en que podríamos crear tipos enumerados y usarlos en Go.

Editar:

Agregar otra forma 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
fuente
2
Puede declarar constantes con valores de cadena. En mi opinión, es más fácil hacerlo si tiene la intención de mostrarlos y realmente no necesita el valor numérico.
cbednarski
4

Aquí hay un ejemplo que resultará útil cuando haya muchas enumeraciones. Utiliza estructuras en Golang, y se basa en Principios Orientados a Objetos para unirlos a todos en un pequeño paquete ordenado. Ninguno de los códigos subyacentes cambiará cuando se agregue o elimine una nueva enumeración. El proceso es:

  • Defina una estructura de enumeración para enumeration items: EnumItem . Tiene un tipo entero y de cadena.
  • Defina el enumerationcomo una lista de enumeration items: Enum
  • Construir métodos para la enumeración. Se han incluido algunos:
    • enum.Name(index int): devuelve el nombre para el índice dado.
    • enum.Index(name string): devuelve el nombre para el índice dado.
    • enum.Last(): devuelve el índice y el nombre de la última enumeración
  • Agregue sus definiciones de enumeración.

Aquí hay un 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
fuente