However, Go's poor type system also forces you to write worse and less safe code than you want.
For example, the lack of enums and sum types means it's hard to represent, safely and efficiently, anything that should be the same type but have different values within a strict set of allowed values. The nearest you get in Go to an enum is:
type Color int
const (
Red Color = iota
Blue
Green
)
Alternatively, can do the same with a string: type Color string
const Red Color = "red"
// etc.
This gives you some type safety, since a Color requires casting to force an int or string or whatever to become it. But that doesn't give you real safety, since you can do: var c Color = Color("fnord")
This comes up whenever you're handling foreign input. JSON unmarshaling, for example. You can override the unmarshalling to add some validation, but it won't solve your other cases; it will always be possible to accidentally accept something bad. Not to mention that the "zero value" of such a value is usually wrong: type Color string
var c color // Empty string, not valid!
A more safe way is to hide the value: type Color interface {
isColor()
}
type colorValue string
func (colorValue) isColor() {}
var Red Color = colorValue("red") // etc.
Now nobody can create an invalid value, and the zero value is nil, not an invalid enum value. But this is unnecessarily complicated and shouldn't be necessary in a modern language in 2019.The case of genuine sum types is worse. Your best bet is to use sealed interfaces:
type Action interface {
isAction()
}
type TurnLeft struct{}
func (TurnLeft) isAction() {}
type MoveForward struct{
Steps int
}
func (MoveForward) isAction() {}
There are some downsides. You have to use static analysis tools (go-sumtype is good) to make sure your type switches are exhaustive. You get a performance penalty from having to wrap all values in an interface. And if you're going to serialize or deserialize this (e.g. JSON), you will be writing a whole bunch of logic to read and write such "polymorphic" values.