Golang How to define enum and understand how iota works

eye-catch Golang

Golang doesn’t support enum. How can we implement it if we want to do grouping values?

Sponsored links

Define enum like values

Let’s define string enum like values first in Golang. The values need to be grouped. For that reason, we need to define a new type.

type State string

const (
    Running State = "Running"
    Stop    State = "Stop"
    Ready   State = "Ready"
    Error   State = "Error"
)

Note: This is not a recommended way. It’s better to use iota which is explained later.

This is just an alias for string but we can use this new type for the enum. However, we must to know that a normal string value can be also assigned to a variable that State is used as a data type. Therefore, the following code is completely correct and the IntelliSense doesn’t say anything about it.

var state State
state = "foo"
fmt.Println(state)
Sponsored links

Check if a value is enum

As I mentioned above, a non-enum value can be assigned to the enum variable. Even though the variable is declared as a function parameter, the value needs to be checked before actual use.

func IsState(value State) bool {
    states := []State{Running, Stop, Ready, Error}
    for _, state := range states {
        if state == value {
            return true
        }
    }
    return false
}

A function might not be able to work properly without this check.

Let’s use it in a function.

func requireState(value state.State) {
    if !state.IsState(value) {
        fmt.Println("not State value")
        return
    }
    fmt.Println(value)
}

func RunEnum() {
    requireState(state.Error) // Error
    requireState(state.Ready) // Ready
    requireState("Ready")     // Ready
    requireState("non-enum")  // not State value
}

requireState() function requires State data type but a normal string value can be assigned there. If the string value is the same as one of the enum values, it is handled as the enum.

Define enum with iota

I know at least one programming language supports enum creation but not value assignment. That’s Dart language. Enum can be used as an ID. It means that we can use enum to identify that the value is one of the enum values. However, it can’t be compared with an int/string value for example by default. Golang supports a similar feature called iota

type Weather int

const (
    Cloudy Weather = iota // 0
    Sunny                 // 1
    Rainy                 // 2
    Snowy                 // 3
)

iota assigns int value automatically. In the above example, the value starts with 0 but it depends on the position of iota.

I put all the examples that I tried to save space.

const (
    Cloudy Weather = 22   // 22
    Sunny  Weather = iota // 1
    Rainy                 // 2
    Snowy                 // 3
)

const (
    Cloudy Weather = iota + 5 // 5
    Sunny                     // 6
    Rainy                     // 7
    Snowy                     // 8
)

const (
    Cloudy Weather = 22       // 22
    Sunny  Weather = iota * 2 // 2
    Rainy                     // 4
    Snowy                     // 6
)

const (
    Cloudy Weather = iota / 2 // 0
    Sunny                     // 0
    Rainy                     // 1
    Snowy                     // 1
)

const (
    Cloudy Weather = iota     // 0
    Sunny  Weather = iota + 3 // 4
    Rainy                     // 5
    Snowy                     // 6
)

const (
    Cloudy Weather = iota     // 0
    Sunny  Weather = iota + 1 // 2
    Rainy  Weather = iota     // 2
    Snowy                     // 3
)

const (
    Cloudy Weather = (iota * 2) + 2*(iota+1) // 2
    Sunny                                    // 6
    Rainy                                    // 10
    Snowy                                    // 14
)

It’s interesting.

  • Start value can be defined by + and - operand
  • Increment value can be defined by * and / operand or a formula
  • The same value can be assigned

It might not be a normal way to use iota but based on this info, we can assign a big value to the enum to avoid using the value that is often used like 0, 1, 2, 3, ....

type Weather int

const (
    Cloudy Weather = iota + 100000 // 100000
    Sunny                          // 100001
    Rainy                          // 100002
    Snowy                          // 100003
)

type Product int

const (
    Notebook Product = iota + 20000 // 20000
    Mouse                           // 20001
    UsbStick                        // 20002
)

This technique could be applied to our code where many enum definitions need to be used and the value needs to be converted to int. Hmm… but it is not a good way to rely on the value defined by iota because the value changes if we change the order of the definition.

How to define string value with iota

When it comes to logging, we want the enum name of the value for better reading. It’s hard to find the corresponding value from a value 0 because it is used everywhere.

Define String() function for the enum in this case.

func (w Weather) String() string {
    switch w {
    case Cloudy:
        return "Cloudy"
    case Sunny:
        return "Sunny"
    case Rainy:
        return "Rainy"
    case Snowy:
        return "Snowy"
    default:
        return ""
    }
}

Once String() is defined, the value is automatically converted to the defined string value.

type Weather int

const (
    Cloudy Weather = iota
    Sunny
    Rainy
    Snowy
)

func requireWeather(value state.Weather) {
    if !state.IsWeather(value) {
        fmt.Println("not Weather value")
        return
    }
    fmt.Printf("key: %s, value: %d\n", value, value)
}

func RunEnum() {
    requireWeather(state.Cloudy) // key: Cloudy, value: 0
    requireWeather(state.Sunny)  // key: Sunny, value: 1
    requireWeather(0)            // key: Cloudy, value: 0
    requireWeather(3)            // key: Snowy, value: 3
    requireWeather(4)            // not Weather value
}

fmt.Printf() requires a string at the first parameter. Thus String() function is called and the desired string value is used there.

Define a struct

The previous ways declare an alias for enum. So an int value can be used as one of the enum values. If you don’t like this, the only way is maybe to create a struct.

This is not a good way but if you really want the feature, a struct needs to be defined.

type MyState struct {
    key   string
    value int
}

func (state MyState) String() string {
    return state.key
}

func (state MyState) GetValue() string {
    return state.key
}
func (state *MyState) GetID() int {
    return state.value
}

func (MyState) Running() MyState {
    return MyState{key: "Running", value: 0}
}
func (MyState) Stop() MyState {
    return MyState{key: "Stop", value: 1}
}
func (MyState) Ready() MyState {
    return MyState{key: "Ready", value: 2}
}
func (MyState) Error() MyState {
    return MyState{key: "Error", value: 3}
}

var errNotMyState = errors.New("key is not MyState")

func (MyState) GetValueByKey(key string) (int, error) {
    switch key {
    case "Running":
        return MyState{}.Running().value, nil
    case "Stop":
        return MyState{}.Stop().value, nil
    case "Ready":
        return MyState{}.Ready().value, nil
    case "Error":
        return MyState{}.Error().value, nil
    default:
        return 0, errNotMyState
    }
}
func (MyState) GetKeyByValue(value int) (string, error) {
    switch value {
    case 0:
        return MyState{}.Running().key, nil
    case 1:
        return MyState{}.Stop().key, nil
    case 2:
        return MyState{}.Ready().key, nil
    case 3:
        return MyState{}.Error().key, nil
    default:
        return "", errNotMyState
    }
}

A normal int/string value can’t be used if we define a struct.

func requireMyState(value state.MyState) {
    fmt.Printf("key: %s, value: %d\n", value.GetValue(), value.GetID())
}

func RunEnum() {
    requireMyState(state.MyState{}.Running()) // key: Running, value: 0
    requireMyState(state.MyState{}.Ready())   // key: Ready, value: 2

    // cannot use "abcde" (untyped string constant) as state.MyState value in argument to requireMyState
    // requireMyState("abcde")

    fmt.Println(state.MyState{}.GetKeyByValue(0))         // Running <nil>
    fmt.Println(state.MyState{}.GetKeyByValue(2))         // Ready <nil>
    fmt.Println(state.MyState{}.GetKeyByValue(5))         //  key is not MyState

    fmt.Println(state.MyState{}.GetValueByKey("Running")) // 0 <nil>
    fmt.Println(state.MyState{}.GetValueByKey("Ready"))   // 2 <nil>
    fmt.Println(state.MyState{}.GetValueByKey("Unknown")) // 0 key is not MyState

    fmt.Println(state.MyState{}.Running()) // Running
}

Hmm…

We need to write a lot of code for an enum… This is not a good way.

Use iota with String()

Comments

Copied title and URL