在Go语言中,表示枚举类型的惯用方式是什么?

772

我正在尝试表示一个简化的染色体,其中包含N个碱基,每个碱基只能是{A,C,T,G}中的一个。

我想用枚举来规范约束条件,但我想知道在Go语言中模拟枚举最惯用的方式是什么。


5
在Go语言标准包中,它们被表示为常量。请参见http://golang.org/pkg/os/#pkg-constants。 - Denys Séguret
2
什么时候应该使用包含另一种类型的结构体,什么时候应该只是一个类型? 相关: http://stackoverflow.com/questions/14236263/when-should-a-type-be-a-struct-containing-another-type-and-when-should-it-just - lbonn
1
@carbocation:这不是[so]上重复问题的处理方式。应该将重复的问题关闭为具有最佳内容的问题的副本,而不是最早的问题。 - Jörg W Mittag
3
既然1.18版本已经发布,我想知道泛型是否为在Go语言中概念化枚举开辟了新的机会。 - carbocation
14个回答

898

引用语言规范:iota

在常量声明中,预定义的标识符 iota 表示连续的无类型整数常量。每当源代码中出现保留字 const 时,它就会被重置为 0,并在每个 ConstSpec 之后递增。它可用于构建一组相关的常量:

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)

在表达式列表(ExpressionList)中,每个 iota 的值都相同,因为它只在每个 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
)

这个例子利用了最后一个非空表达式列表的隐式重复。
所以你的代码可能会像这样:
const (
        A = iota
        C
        T
        G
)

或者

type Base int

const (
        A Base = iota
        C
        T
        G
)

如果您想让基础类型与int类型分开,请参考以下内容。

29
很好的例子(我不记得规范中精确的 iota 行为——即何时进行自增)。个人喜欢给枚举类型指定类型,这样在其用作参数、字段等时可以进行类型检查。 - mna
38
很有趣 @jnml。但我有点失望静态类型检查似乎很松散,例如没有任何阻止我使用从未存在过的Base n°42:http://play.golang.org/p/oH7eiXBxhR。 - Deleplace
8
为了补充jnml,即使在语义上,语言中也没有说定义为Base的常量代表整个有效Base范围,它只是说这些特定的常量属于Base类型。其他常量也可以在其他地方被定义为Base,并且它甚至不是互斥的(例如,可以定义const Z Base = 0并且它是有效的)。 - mna
14
你可以使用 iota + 1 来避免从0开始计数。 - Marçal Juan
3
在当前版本中,使用A base = iota的方式,所有的值都正确地具有了类型Base。如果它们是未命名的数字常量,我们就可以将它们用作整数,但现在我们不能这样做。代码示例:https://play.golang.org/p/Dpop4S3qdNs - Deleplace
显示剩余5条评论

119

参考jnml的回答,您可以通过根本不导出Base类型(即将其写成小写)来防止新实例化。如果需要,您可以创建一个可导出的接口,该接口具有返回基础类型的方法。此接口可在外部处理Bases的函数中使用,例如:

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
}

在主包中,a.Baser 现在实际上类似于一个枚举类型。只有在 a 包内才能定义新的实例。


13
你的方法似乎非常适合base仅用作方法接收器的情况。如果你的a包公开一个以base类型参数为输入的函数,那就会变得危险。因为用户可以使用字面值42直接调用该函数,而该函数将接受它作为base类型,因为它可以转换为int。为了防止这种情况,让base成为一个结构体:type base struct{value:int}。但是问题是,你不能再声明base为常量,只能声明为模块变量。但是42永远不会被转换为该类型的base - Niriel
23
我会尝试理解你的示例,但是你所选择的变量名称使得它变得非常困难。 - fIwJlxSzApHEZIl
4
这是我在示例中的一个令人烦恼的问题。我意识到这样做很有诱惑力,但不要将变量命名为与类型相同的名称! - Graham Nicholls

68

您可以这样做:

type MessageType int32

const (
    TEXT   MessageType = 0
    BINARY MessageType = 1
)

使用此代码,编译器应检查枚举类型


18
常量通常使用普通驼峰命名法书写,而非全大写。首字母大写意味着该变量被导出,这可能符合或不符合您的需求。 - 425nesp
3
我注意到在Go源代码中,有时常量全部大写,有时则是驼峰式大小写。你有规范的参考资料吗? - Jeremy Gailor
@JeremyGailor 我认为425nesp只是指出开发人员通常将它们用作未公开常量,因此请使用驼峰命名法。如果开发人员确定应该导出它,则可以自由地使用全部大写或首字母大写,因为没有建立的偏好。参见Golang 代码审查建议Effective Go 常量部分 - waynethec
2
请注意,期望 MessageType 的函数将愉快地接受未经类型化的数字常量,例如 7。此外,您可以将任何 int32 强制转换为 MessageType。如果您已经了解这一点,我认为这是在 Go 中最符合惯用法的方式。 - Kosta
1
@MartinM。我会称之为“意外”——这些常量(KB、MB、GB等)恰好是已知单位的缩写(请参见https://github.com/golang/go/wiki/CodeReviewComments#initialisms)。 - Rodolfo Carvalho
显示剩余2条评论

65

确实,使用constiota的上述示例是在Go中表示基本枚举的最传统方式。但如果您正在寻找一种创建更完整功能的枚举类型(类似于Java或Python中看到的类型)的方法呢?

创建一个对象,使其开始看起来和感觉像Python中的字符串枚举类型非常简单:

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

假设您还需要一些实用方法,例如Colors.List()Colors.Parse("red")。而且您的颜色更加复杂,需要作为结构体。那么您可以按照以下方式进行操作:

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

在这一点上,当然它可以工作,但您可能不喜欢必须反复定义颜色的方式。如果您想消除这种情况,可以在结构体上使用标签并进行一些花哨的反射来设置它,但希望这足以涵盖大多数人。


有没有办法像Java一样使用switch语句来处理这个? - David Good
实际上,我已经使用你的示例中的公共“Colors”使其工作,例如case Colors.Red: ... - David Good

43

使用结构体命名空间可以解决这个问题。

好处是所有枚举变量都在特定的命名空间中,以避免污染。 问题是我们只能使用var 而不是const

type OrderStatusType string

var OrderStatus = struct {
    APPROVED         OrderStatusType
    APPROVAL_PENDING OrderStatusType
    REJECTED         OrderStatusType
    REVISION_PENDING OrderStatusType
}{
    APPROVED:         "approved",
    APPROVAL_PENDING: "approval pending",
    REJECTED:         "rejected",
    REVISION_PENDING: "revision pending",
}

1
这个语法看起来很好,但我对人们更改常量感到非常担心。请看这个例子:https://play.golang.org/p/9D1tMQJVmIc 。如果命名空间很重要,我会倾向于将它们放在自己的包中。 - Grokify
我该如何将其用作枚举? - Bruno Negrão Zica
1
我同意@Grokify的观点,虽然这看起来语法上很舒服,但在这里使用var是非常危险的,因为它容易改变,这违背了枚举的初衷。 - Gurleen Sethi

22
从Go 1.4开始,go generate工具与stringer命令一起推出,使您的枚举类型易于调试和打印。

你知道相反的解决方案吗?我的意思是字符串 -> MyType。因为单向解决方案远非理想。这里是一个Gist,可以做到我想要的——但手写容易出错。链接:https://gist.github.com/lummie/7f5c237a17853c031a57277371528e87 - S.R
在回答中添加一个代码示例一定会更清晰地表达您的意思。 - XDS

20

对于这样的用例,使用字符串常量可能很有用,因为它可以转换为JSON字符串。在下面的示例中,[]Base{A,C,G,T}将被转换为["adenine","cytosine","guanine","thymine"]

type Base string

const (
    A Base = "adenine"
    C      = "cytosine"
    G      = "guanine"
    T      = "thymine"
)

使用 iota 时,值会被编组成整数。在以下示例中,[]Base{A,C,G,T} 会被编组为 [0,1,2,3]

type Base int

const (
    A Base = iota
    C
    G
    T
)

以下是一个比较两种方法的示例:

https://play.golang.org/p/VvkcWvv-Tvj


19

我相信这里有很多好的答案。但是,我想分享一下我使用枚举类型的方法。

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

这绝对是我们可以在Go中创建枚举类型并使用的惯用方式之一。

编辑:

添加了另一种使用常量进行枚举的方法

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)

}

5
你可以使用字符串值声明常量。在我看来,如果你打算显示它们而不需要实际的数值,则这样做会更容易。 - cbednarski
我不理解第二个例子:你仍然可以使用任何int值调用“SetLogLevel”。使用枚举的目的是您无法使用任何其他值。 - bfontaine
仅仅为了定义常量而使用大写字母来指定常量并不是一个好主意,因为在 Go 语言中它具有完全不同的含义。在 Go 语言中,大写字母用于访问控制和管理代码的可见性。 - kafran

9
以下是一个示例,在有许多枚举时将证明其有用。它使用 Golang 中的结构,并借助面向对象原则将它们紧密地绑定在一起。当添加或删除新的枚举时,底层代码不会改变。具体步骤如下:
  • 为“枚举项”定义一个枚举结构:EnumItem。它具有整数和字符串类型。
  • 将“枚举”定义为“枚举项”的列表:Enum
  • 为枚举建立方法。其中包括以下几个:
    • enum.Name(index int):返回给定索引的名称。
    • enum.Index(name string):返回给定名称的索引。
    • enum.Last():返回最后一个枚举的索引和名称
  • 添加您的枚举定义。
以下是一些代码:
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"}}}

6

重构了https://dev59.com/aWYq5IYBdhLWcg3wfw1x#17989915以使其更易读:

package SampleEnum

type EFoo int

const (
    A EFoo = iota
    C
    T
    G
)

type IEFoo interface {
    Get() EFoo
}

func(e EFoo) Get() EFoo { // every EFoo must fulfill the IEFoo interface
    return e
}

func(e EFoo) otherMethod()  { // "private"
    //some logic
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接