Should you use a zero "enum" value to indicate an invalid value

Viewed 924

Having used C for decades I got into the habit of using the zero value of an enum as a special undefined/unknown/error value. Over the years I believe this has saved me not hours or even days but months of debugging time since it makes it obvious when a value has not been initialized. (I wouldn't do this for simple enums where there is a sensible default value and no possibility of uninitialized values.)

It seems to me that this practice is even more useful in Go as values are automatically zero-initialised for you. However, I have been told that "idiomatic" Go zero-values should be valid values. I think this "rule" was invented for structs, where it makes a lot of sense (in the absence of constructors) to have a newly created "zeroed" struct ready for use, but there are cases where there is no logical default value (for structs and enums).

If you need it here is an example:

type Base int

const (
        Invalid Base = iota
        A
        C
        T
        G
)

Note that I have searched extensively for this question on SO and was surprised that this specific topic has not been covered. I realise that my question is somewhat subjective and may be flagged but I think it is useful. I am looking for evidence that using zero values to indicate error conditions is acceptable Go practice. Any examples of this use, eg. from the standard Go library, would be appreciated.

1 Answers

A true enum type should only be assigned a value from a list of pre-defined constant values. The go language, however, does not have such a type-value enforcement.

go has const which typically uses a derivative type of say int. There is no compile/run-time mechanism to enforce a value is strictly within a pre-defined list.

So what does this mean in practice?

Is your enum value mandatory or optional? That is, when deserializing the 'enum' value, is it:

  • optional - then use the zero-value signifies the default value
  • mandatory - then the zero-value indicates an initialization error

Depending on your common use-case, choose one of these two options.


EDIT: Deserializing is not the only concern. One has to be careful when branching on enum values. For example:

type role int
const (
    user role = iota
    helpdesk
    admin
)

func greet(r role) {
    switch r {
        case admin:
            fmt.Println("hi admin")
        case helpdesk:
            fmt.Println("hi helpdesk")
        default:
            fmt.Println("hi user") // right?
    }
}

This works:

var r role
r = admin
greet(r) // hi admin

But what about this?

r = 12
greet(r) // 'hi user' ?!! 

So be sure to pedantically validate on valid values only:

func validateRole(r role) (err error) {
    switch r {
    case user, helpdesk, admin: // all valid values
    default:
        err = fmt.Errorf("invalid `role` enum %d", r)
    }
    return
}

Playground

Related