From May 30 to June 3, we will ride from Paderborn in Germany to Aarhus in Denmark in four days. The route covers 625 kilometers of road cycling with 3,190 meters of elevation gain. The event is organized by the Rotary Fellowship Cycling to Serve in collaboration with local cycling clubs and everyone is invited to join! The Rad-Treff Borchen planned the day-to-day routes.
Pinned
Latest
Yet another article on the topic
In a programming situation an enum should provide:
- self-documenting constant identifiers,
- easy logging as well as marshaling and unmarshaling,
- type safety,
- and being able to iterate over the enum values.
Go does not support enums natively and some of the patterns developers use to handle enums in Go have drawbacks, as you can read in Safer Enums in Go. I've come to use a slug-based approach that is iterable and gets close enough to type-safety for practical purposes. In my view it gives a good balance of safety and implementation effort.
Slug-based
A slug refers to a string that is lowercase, contains no spaces (using - or _ instead), is human-readable, and safe to use in URLs, JSON, and databases. In Go it can be implemented like:
package job
// Status represents the lifecycle state of a job.
type Status string
// Possible status values for a job.
const (
StatusPending Status = "pending" // StatusPending means the job is waiting to be picked up by a handler.
StatusCancel Status = "cancel" // StatusCancel means cancellation has been requested but not yet completed.
StatusProcessing Status = "processing" // StatusProcessing means a handler is actively working on the job.
StatusCompleted Status = "completed" // StatusCompleted means the job finished successfully.
StatusCanceled Status = "canceled" // StatusCanceled means the job was successfully canceled.
StatusFailed Status = "failed" // StatusFailed means the handler encountered an unrecoverable error.
)
Slugs are self-documenting and work equally well for logging and marshaling, something the Go iota approach does not give you. However, the above code will not let you iterate over all constant values and it is not type-safe.
Type-safe
The slug-based approach cannot be made type-safe. The following is possible:
// s should hold only defined status values,
// but the following assignment of an arbitrary value
// will not raise a compiler complaint
var s = Status("arbitrary value")
To improve in this area, you can do:
var KnownStatuses = []Status{StatusPending, StatusCancel, StatusProcessing, StatusCompleted, StatusCanceled, StatusFailed}
var knownStatusesSet = MakeSet[Status](KnownStatuses)
// ParseStatus will parse s into a job status
func ParseStatus(s string) (Status, error) {
status := Status(s)
if _, ok := knownStatusesSet[status]; ok {
return status, nil
}
return "", fmt.Errorf("unknown job status: %s", s)
}
ParseStatus will parse any string and ensure it can be parsed into a valid Status, or an error is raised. It is still required by the programmer to call ParseStatus when transforming a string into a Status, like when unmarshaling from a JSON value to a Status value, but in this situation you have to do something like s = Status("processing") regardless, so you may as well do s = job.ParseStatus("processing").
A possible enhancement for a validation when you do not control the parsing situation, because someone else is handing over a Status to you, is:
// IsValidStatus will return true if s is a known job status
func IsValidStatus(s Status) (bool, error) {
if _, err := ParseStatus(string(s)); err != nil {
return false, err
}
return true, nil
}
When using ParseStatus and IsValidStatus at key locations in the code, I think I get something that is close enough to type-safety, which allows to work in all other code locations with unguarded access to Status.
MakeSet is a helper function that will transform a slice into a set , and as such, it can be useful as well in other situations. Note that Go does not have a native set type; they are typically implemented as maps with empty struct values since an empty struct consumes no memory.
func MakeSet[T comparable](values []T) map[T]struct{} {
m := make(map[T]struct{}, len(values))
for _, v := range values {
m[v] = struct{}{}
}
return m
}
Iterable
It is most straightforward to iterate over KnownStatuses:
for s := range KnownStatuses {
// do something with s
}
When the KnownStatuses should not be exposed outside of the job package, you can make a range-over-func (you need Go 1.23+ for this to work):
// keep knownStatus inside of the job package
var knownStatuses = []Status{StatusPending, StatusCancel, StatusProcessing, StatusCompleted, StatusCanceled, StatusFailed}
// AllStatuses is a range-over-func that
// exposes an iterator over all job status values
func AllStatuses(yield func(Status) bool) {
for _, s := range knownStatuses {
if !yield(s) {
return
}
}
}
Outside of the job package the range-over-func allows to iterate over all job status values without exposing the knownStatuses slice.
for s := range job.AllStatuses {
fmt.Println(s)
}
Selected posts
Here is a selection of posts I want to highlight. You can find more in the blog.
Web
- Accessibility
- Serverless
- Jamstack in 20 minutes
-
Simplifying light mode and dark mode CSS with the
light-dark()function - Text-to-Chart
- Prerender pages with the Speculation Rules API
- Searching a Jamstack site with Pagefind
- My first attempt with Tailwind CSS
- Toggles suck
- Checkboxes and radio buttons
- Responsive images that adapt in width and height