issues with go const

Here's a nice little gotcha. In go, "enums" are often defined using the iota generator like this:
type DaysOfTheWeek int

const (
 Sunday DaysOfTheWeek = iota
 Monday
 Tuesday
 Wednesday
 Thursday
 Friday
)
The downside, of course, is that when you want to print those values:
fmt.Println(Sunday) // prints 0, would rather it print "Sunday"
While the "stringer" code-generator is the recommend way of handling this, I'm often tempted to simply change the declaration:
type DaysOfTheWeek string

const (
 Sunday DaysOfTheWeek = "Sunday"
 Monday               = "Monday"
 Tuesday              = "Tuesday"
 Wednesday            = "Wednesday"
 Thursday             = "Thursday"
 Friday               = "Friday"
)
But, this is wrong in an important way.
Assertions, and reflection will fail.
import "reflect"

func ExampleDay() DaysOfTheWeek {
    return Friday
}
func main() {
    fmt.Println(reflect.DeepEqual(Friday, Friday)) // true
    fmt.Println(reflect.DeepEqual(Friday, ExampleDay())) // false!
}
We expect that both statements return true, but in fact the second statement is false. If you are using testify, an assertion on the value will also fail.
func TestSomething(t *testing.T) {
    // passes, these are the same type and same object.
    assert.Equal(t, Friday, Friday)
    // fails, these are of different types!
    assert.Equal(t, Friday, ExampleDay())
}
The reason is that DeepEqual requires types match, and that the definition of const lists, in effect, resets the type declaration for each line containing an equals sign.

In the first const example, using iota, there's only one equal sign: so all of the constants get the same type.

In the second const example, even though we started the list with Sunday as a type of "DaysOfTheWeek", the presence of the equal sign on the line which declares Monday resets the type declaration engine to its defaults. In this case, a string. So, only Sunday is of our enumerated type. Monday-Saturday are all of type "string".

Now, while ExampleDay() says it returns our enumerated type, Friday's actual type will never change. It's always and forever string. The fact the compiler lets this pass seems to violate the principle of least surprise, but there might be other common cases where this behavior makes more sense. ( While go is strongly typed, the way it handles literals and other edge cases sometimes leaves me wanting for warnings. )  I was writing off-the-cuff. Go underlying types only apply to interfaces, not primitive values.

The issue, then, for reflect.DeepEqual(Friday, ExampleDay()) is that the first parameter, Friday, is of type "string", while the second parameter, ExampleDay(), is of type "DaysOfTheWeek". The types don't match and DeepEqual returns false. Testify assert, which relies on reflect, therefore also fails.

If we redeclare our const list:
const (
 Sunday    DaysOfTheWeek = "Sunday"
 Monday    DaysOfTheWeek = "Monday"
 Tuesday   DaysOfTheWeek = "Tuesday"
 Wednesday DaysOfTheWeek = "Wednesday"
 Thursday  DaysOfTheWeek = "Thursday"
 Friday    DaysOfTheWeek = "Friday"
)
Then reflect, and asserts, will work as expected:
func main() {
    fmt.Println(reflect.DeepEqual(Friday, ExampleDay()) // true!
}
func TestSomething(t *testing.T) {
    // passes, these are of now the same type!
    assert.Equal(t, Friday, ExampleDay())
}
Unfortunately, unlike with iota, there's no pretty way to declare all of those strings without repeating the type. The closest we can get looks like this:
const (
 ParamTypeUnknown,
 ParamTypeCommand,
 ParamTypeArray,
 ParamTypePrim,
 ParamTypeBlob ParamType = "ParamTypeUnknown",
  "ParamTypeCommand",
  "ParamTypeArray",
  "ParamTypePrim",
  "ParamTypeBlob"
)
Which is error prone, and looks a little gross with the default go formatter, but is at least easy to generate, and more importantly: it's correct.

0 comments: