brandur.org

ValOrDefault

Update: It turns out that everything I wrote here has been superseded as of Go 1.22, which added cmp.Or, which works almost identically. It can be used to easily assign defaults:

config = &Config{
    CancelledJobRetentionPeriod: cmp.Or(config.CancelledJobRetentionPeriod, maintenance.CancelledJobRetentionPeriodDefault),
    CompletedJobRetentionPeriod: cmp.Or(config.CompletedJobRetentionPeriod, maintenance.CompletedJobRetentionPeriodDefault),
    DiscardedJobRetentionPeriod: cmp.Or(config.DiscardedJobRetentionPeriod, maintenance.DiscardedJobRetentionPeriodDefault),
}

Thanks @earthboundkid for the correction.


Especially with respect to certain major pain points, Go’s traditionally lived by an accidental butchering of Larry Wall’s old quote of, “Make easy things easy, and hard things possible” to the much more unfortunate variant of, “Make easy things hard, and hard things impossible.”

Historically, an example of this has been initializing default values, like you might do in a configuration struct:

type Config struct {
    CancelledJobRetentionPeriod time.Duration
    CompletedJobRetentionPeriod time.Duration
    DiscardedJobRetentionPeriod time.Duration
}

The caller may have set some properties on an incoming instance, but the others would default to their zero values. Until quite recently, setting defaults on any properties without a value required separate conditionals for each:

if config.CancelledJobRetentionPeriod == 0 {
    config.CancelledJobRetentionPeriod = maintenance.CancelledJobRetentionPeriodDefault
}

if config.CompletedJobRetentionPeriod == 0 {
    config.CompletedJobRetentionPeriod = maintenance.CompletedJobRetentionPeriodDefault
}

if config.DiscardedJobRetentionPeriod == 0 {
    config.DiscardedJobRetentionPeriod = maintenance.DiscardedJobRetentionPeriodDefault
}

The zealots would cite this as perfectly fine and a-feature-not-a-bug because Go prefers more verbosity to make code more readable, while ignoring the fact that every other programming language on Earth has a way to do this that’s not only legible, but almost certainly more so than the Go version.

e.g. Ruby:

config.cancelled_job_retention_period ||= CANCELLED_JOB_RETENTION_PERIOD_DEFAULT
config.completed_job_retention_period ||= COMPLETED_JOB_RETENTION_PERIOD_DEFAULT
config.discarded_job_retention_period ||= DISCARDED_JOB_RETENTION_PERIOD_DEFAULT

Agree or not, Go’s necessary if conditions sure made for some ugly ladders of code. In something like a test data factory, which aims to make test objects easy to build with minimum options, but also to allow every separate property to be overridden if necessary, you could have hundreds of lines worth of ifs for a large object, each of which is a potential bug liability in case of a copy/pasta error.

Thankfully, this is an area where Go 1.18’s generics really bailed the language out. Most of my projects now have a valutil.ValOrDefault 1, the implementation of which is trivial:

// ValOrDefault returns the given value if it's non-zero, and otherwise returns
// the default.
func ValOrDefault[T comparable](val, defaultVal T) T {
    var zero T
    if val != zero {
        return val
    }
    return defaultVal
}

Update: See the top of the article, but Go 1.22’s built-in cmp.Or behaves identically to ValOrDefault, and should be preferred over this custom implementation.

With it, we can tighten up the code above considerably:

config = &Config{
    CancelledJobRetentionPeriod: ptrutil.ValOrDefault(config.CancelledJobRetentionPeriod, maintenance.CancelledJobRetentionPeriodDefault),
    CompletedJobRetentionPeriod: ptrutil.ValOrDefault(config.CompletedJobRetentionPeriod, maintenance.CompletedJobRetentionPeriodDefault),
    DiscardedJobRetentionPeriod: ptrutil.ValOrDefault(config.DiscardedJobRetentionPeriod, maintenance.DiscardedJobRetentionPeriodDefault),
}

There’s also a ValOrDefaultFunc variant that lazily marshals a default by invoking a function, but only if necessary.

// ValOrDefault returns the given value if it's non-zero, and otherwise invokes
// defaultFunc to produce a default value.
func ValOrDefaultFunc[T comparable](val T, defaultFunc func() T) T {
    var zero T
    if val != zero {
        return val
    }
    return defaultFunc()
}
ClientID: valutil.ValOrDefaultFunc(config.ID, func() string { return defaultClientID(time.Now().UTC()) }),

ValOrDefault is a very small improvement, but one that could probably help nicen up most Go projects, at least a little. See River’s valutil implementation for reference.

1 The helper’s package is named valutil due to my policy on util packages:

Did I make a mistake? Please consider sending a pull request.