Golang doesn’t support enum. How can we implement it if we want to do grouping values?
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)
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