Golang What would be the simplest way to set the default value?

eye-catch Golang

I needed to set the default value if a user did not put the property. There were several ways to set a default value, but reaching the simplest way took me a while.

The possible solutions are as follows.

  • switch-case or if-else
  • map
  • strategy pattern
  • function
  • struct

Using struct would be the simplest. Let’s look at the code one by one.
I ran into the issue. I first implemented it in a strategy pattern but changed it to a simple solution.

Sponsored links

The struct that we want to set default values

Let’s think about a hotel booking. We have some plans and each plan has its default value.

Let’s define the default values in the table below.

ItemsABC
bedTypesingleBeddoubleBedqueenBed
persons121
numberOfBeds121
numberOfNights113

Then, we define the struct and the necessary info.

type planType string
type bedType string

const (
    planTypeA planType = "A"
    planTypeB planType = "B"
    planTypeC planType = "C"
)

const (
    singleBed bedType = "single"
    doubleBed bedType = "double"
    queenBed  bedType = "queen"
    kingBed   bedType = "king"
)

type planInfo struct {
    plan           planType
    bedType        *bedType
    persons        *int
    numberOfBeds   *int
    numberOfNights *int
}

func (m planInfo) print() {
    fmt.Printf("plan: %s, ", m.plan)

    if m.bedType == nil {
        fmt.Printf("bedType: nil")
    } else {
        fmt.Printf("bedType: %s", *m.bedType)
    }

    if m.persons == nil {
        fmt.Printf(", persons: nil")
    } else {
        fmt.Printf(", persons: %d", *m.persons)
    }

    if m.numberOfBeds == nil {
        fmt.Printf(", numberOfBeds: nil")
    } else {
        fmt.Printf(", numberOfBeds: %d", *m.numberOfBeds)
    }

    if m.numberOfNights == nil {
        fmt.Printf(", numberOfNights: nil\n")
    } else {
        fmt.Printf(", numberOfNights: %d\n", *m.numberOfNights)
    }
}

Assume that the values are set by someone else. However, we write the following code to make it simple and easy to read. funcName will be replaced with one of the actual function name. The first 3 function calls should add the default value but the last one shouldn’t because it already specifies all properties.

func SetDefaultValue() {
    fmt.Println("---funcName---")
    funcName(planInfo{plan: planTypeA})
    funcName(planInfo{plan: planTypeB})
    funcName(planInfo{plan: planTypeC})

    chosenBedType := kingBed
    numberOfPersons := 2
    funcName(planInfo{plan: planTypeC, bedType: &chosenBedType, persons: &numberOfPersons})
}

Let’s look at the actual implementation based on this code.

Sponsored links

Switch-case or if-else

The default value must be set only when the target property is not set. In each property check, we add the default value there.

func useSwitchCase1(planInfo planInfo) {
    if planInfo.persons == nil {
        var defaultValue int
        switch planInfo.plan {
        case planTypeA:
            defaultValue = 1
        case planTypeB:
            defaultValue = 2
        case planTypeC:
            defaultValue = 1
        }
        planInfo.persons = &defaultValue
    }

    if planInfo.bedType == nil {
        var defaultValue bedType
        switch planInfo.plan {
        case planTypeA:
            defaultValue = singleBed
        case planTypeB:
            defaultValue = doubleBed
        case planTypeC:
            defaultValue = queenBed
        }
        planInfo.bedType = &defaultValue
    }

    if planInfo.numberOfBeds == nil {
        var defaultValue int
        switch planInfo.plan {
        case planTypeA:
            defaultValue = 1
        case planTypeB:
            defaultValue = 2
        case planTypeC:
            defaultValue = 1
        }
        planInfo.numberOfBeds = &defaultValue
    }

    if planInfo.numberOfNights == nil {
        var defaultValue int
        switch planInfo.plan {
        case planTypeA:
            defaultValue = 1
        case planTypeB:
            defaultValue = 1
        case planTypeC:
            defaultValue = 3
        }
        planInfo.numberOfNights = &defaultValue
    }

    planInfo.print()
}

But this looks horrible. switch-case is written in each if clause. We should use the switch-case only once.

func useSwitchCase2(planInfo planInfo) {
    var defaultPersons int
    var defaultBedType bedType
    var defaultNumberOfBeds int
    var defaultNumberOfNights int

    switch planInfo.plan {
    case planTypeA:
        defaultPersons = 1
        defaultBedType = singleBed
        defaultNumberOfBeds = 1
        defaultNumberOfNights = 1
    case planTypeB:
        defaultPersons = 2
        defaultBedType = doubleBed
        defaultNumberOfBeds = 2
        defaultNumberOfNights = 1
    case planTypeC:
        defaultPersons = 1
        defaultBedType = queenBed
        defaultNumberOfBeds = 1
        defaultNumberOfNights = 3
    }

    if planInfo.persons == nil {
        planInfo.persons = &defaultPersons
    }

    if planInfo.bedType == nil {
        planInfo.bedType = &defaultBedType
    }

    if planInfo.numberOfBeds == nil {
        planInfo.numberOfBeds = &defaultNumberOfBeds
    }

    if planInfo.numberOfNights == nil {
        planInfo.numberOfNights = &defaultNumberOfNights
    }

    planInfo.print()
}

It looks easier to read. The code looks similar if we use if-else instead.

Set default values in map

We can use map instead of switch-case or if-else. We need to define the default values for each plan. Therefore, we can define the map in the following way.

var defaultValueMap = map[planType]map[string]any{
    planTypeA: {
        "bedType":        singleBed,
        "persons":        1,
        "numberOfBeds":   1,
        "numberOfNights": 1,
    },
    planTypeB: {
        "bedType":        doubleBed,
        "persons":        2,
        "numberOfBeds":   2,
        "numberOfNights": 1,
    },
    planTypeC: {
        "bedType":        queenBed,
        "persons":        1,
        "numberOfBeds":   1,
        "numberOfNights": 3,
    },
}

But we should check whether the key exists when we extract a value from a map. So additional check is required for each value. If all properties have the same data type, the code becomes easier but if it has a different type, the code gets more complicated because of conversion.

func useMap(planInfo planInfo) {
    defaultMap, ok := defaultValueMap[planInfo.plan]
    if !ok {
        fmt.Printf("no default for plan %s", planInfo.plan)

        return
    }

    if planInfo.persons == nil {
        if val, ok := defaultMap["persons"]; ok {
            if intValue, err := convertAnyToInt(val); err == nil {
                planInfo.persons = intValue
            }
        }
    }

    if planInfo.bedType == nil {
        if val, ok := defaultMap["bedType"]; ok {
            if bedTypeValue, err := convertAnyToBedType(val); err == nil {
                planInfo.bedType = bedTypeValue
            }
        }
    }

    if planInfo.numberOfBeds == nil {
        if val, ok := defaultMap["numberOfBeds"]; ok {
            if intValue, err := convertAnyToInt(val); err == nil {
                planInfo.numberOfBeds = intValue
            }
        }
    }

    if planInfo.numberOfNights == nil {
        if val, ok := defaultMap["numberOfNights"]; ok {
            if intValue, err := convertAnyToInt(val); err == nil {
                planInfo.numberOfNights = intValue
            }
        }
    }

    planInfo.print()
}

func convertAnyToInt(val any) (*int, error) {
    ret, ok := val.(int)
    if !ok {
        return nil, errors.New("conversion error")
    }

    return &ret, nil
}

func convertAnyToBedType(val any) (*bedType, error) {
    ret, ok := val.(bedType)
    if !ok {
        return nil, errors.New("conversion error")
    }

    return &ret, nil
}

This is even harder to read than switch-case…

Apply strategy pattern for default values

Strategy pattern is also one of the possibilities. Golang can’t define properties in interface, so we have to define functions to return a default value.

type getDefaults interface {
    getPersons() int
    getBedType() bedType
    getNumberOfBeds() int
    getNumberOfNights() int
}

type planADefaults struct{}

func (planADefaults) getPersons() int        { return 1 }
func (planADefaults) getBedType() bedType    { return singleBed }
func (planADefaults) getNumberOfBeds() int   { return 1 }
func (planADefaults) getNumberOfNights() int { return 1 }

type planBDefaults struct{}

func (planBDefaults) getPersons() int        { return 2 }
func (planBDefaults) getBedType() bedType    { return singleBed }
func (planBDefaults) getNumberOfBeds() int   { return 2 }
func (planBDefaults) getNumberOfNights() int { return 1 }

type planCDefaults struct{}

func (planCDefaults) getPersons() int        { return 1 }
func (planCDefaults) getBedType() bedType    { return queenBed }
func (planCDefaults) getNumberOfBeds() int   { return 1 }
func (planCDefaults) getNumberOfNights() int { return 3 }

We need to assign the actual struct in switch-case but the function has fewer lines than using only switch-case that I showed above.

func useStrategy(planInfo planInfo) {
    var defaultsGetter getDefaults
    switch planInfo.plan {
    case planTypeA:
        defaultsGetter = planADefaults{}
    case planTypeB:
        defaultsGetter = planBDefaults{}
    case planTypeC:
        defaultsGetter = planCDefaults{}
    }

    if planInfo.persons == nil {
        val := defaultsGetter.getPersons()
        planInfo.persons = &val
    }

    if planInfo.bedType == nil {
        val := defaultsGetter.getBedType()
        planInfo.bedType = &val
    }

    if planInfo.numberOfBeds == nil {
        val := defaultsGetter.getNumberOfBeds()
        planInfo.numberOfBeds = &val
    }

    if planInfo.numberOfNights == nil {
        val := defaultsGetter.getNumberOfNights()
        planInfo.numberOfNights = &val
    }

    planInfo.print()
}

But I think applying strategy pattern is too much for this purpose because it doesn’t have any logic there. Each struct has just a value in each function.

Define a function

If strategy pattern is too much, function is also another solution.

type getDefaultValues func() (int, bedType, int, int)

func getDefaultValuesForPlanA() (int, bedType, int, int) {
    person := 1
    bedType := singleBed
    numberOfBeds := 1
    numberOfNights := 1

    return person, bedType, numberOfBeds, numberOfNights
}

func getDefaultValuesForPlanB() (int, bedType, int, int) {
    person := 2
    bedType := doubleBed
    numberOfBeds := 2
    numberOfNights := 1

    return person, bedType, numberOfBeds, numberOfNights
}

func getDefaultValuesForPlanC() (int, bedType, int, int) {
    person := 1
    bedType := queenBed
    numberOfBeds := 1
    numberOfNights := 3

    return person, bedType, numberOfBeds, numberOfNights
}

getDefaultValues type is not used here because the function is called in the following function.

func useFunction(planInfo planInfo) {
    var defaultPersons int
    var defaultBedType bedType
    var defaultNumberOfBeds int
    var defaultNumberOfNights int

    switch planInfo.plan {
    case planTypeA:
        defaultPersons, defaultBedType, defaultNumberOfBeds, defaultNumberOfNights = getDefaultValuesForPlanA()
    case planTypeB:
        defaultPersons, defaultBedType, defaultNumberOfBeds, defaultNumberOfNights = getDefaultValuesForPlanB()
    case planTypeC:
        defaultPersons, defaultBedType, defaultNumberOfBeds, defaultNumberOfNights = getDefaultValuesForPlanC()
    default:
    }

    if planInfo.persons == nil {
        planInfo.persons = &defaultPersons
    }

    if planInfo.bedType == nil {
        planInfo.bedType = &defaultBedType
    }

    if planInfo.numberOfBeds == nil {
        planInfo.numberOfBeds = &defaultNumberOfBeds
    }

    if planInfo.numberOfNights == nil {
        planInfo.numberOfNights = &defaultNumberOfNights
    }

    planInfo.print()
}

But if we want, we can extract the switch-case to outside and make the function easier to read.

func useFunction(planInfo planInfo, cb getDefaultValues) {
    defaultPersons, defaultBedType, defaultNumberOfBeds, defaultNumberOfNights := cb()

    if planInfo.persons == nil {
        planInfo.persons = &defaultPersons
    }

    if planInfo.bedType == nil {
        planInfo.bedType = &defaultBedType
    }

    if planInfo.numberOfBeds == nil {
        planInfo.numberOfBeds = &defaultNumberOfBeds
    }

    if planInfo.numberOfNights == nil {
        planInfo.numberOfNights = &defaultNumberOfNights
    }

    planInfo.print()
}

It looks better than before but this way has a problem. The function could return those default values in a different order by mistake. This can’t be checked by a tool.

The simplest and best solution is using struct

The simplest and best solution would be using struct. It doesn’t have any order of the properties. When we assign a default value, we have to write the property name as well.

type planDefaults struct {
    bedType        bedType
    persons        int
    numberOfBeds   int
    numberOfNights int
}

func useStruct(planInfo planInfo) {
    var defaultsGetter planDefaults
    switch planInfo.plan {
    case planTypeA:
        defaultsGetter = planDefaults{
            bedType:        singleBed,
            persons:        1,
            numberOfBeds:   1,
            numberOfNights: 1,
        }
    case planTypeB:
        defaultsGetter = planDefaults{
            bedType:        doubleBed,
            persons:        2,
            numberOfBeds:   2,
            numberOfNights: 1,
        }
    case planTypeC:
        defaultsGetter = planDefaults{
            bedType:        queenBed,
            persons:        1,
            numberOfBeds:   1,
            numberOfNights: 3,
        }
    }

    if planInfo.persons == nil {
        planInfo.persons = &defaultsGetter.persons
    }

    if planInfo.bedType == nil {
        planInfo.bedType = &defaultsGetter.bedType
    }

    if planInfo.numberOfBeds == nil {
        planInfo.numberOfBeds = &defaultsGetter.numberOfBeds
    }

    if planInfo.numberOfNights == nil {
        planInfo.numberOfNights = &defaultsGetter.numberOfNights
    }

    planInfo.print()
}

We can pass the struct as a parameter. Then, the function looks much easier to read.

func useStruct(planInfo planInfo, defaultsGetter planDefaults) {
    if planInfo.persons == nil {
        planInfo.persons = &defaultsGetter.persons
    }

    if planInfo.bedType == nil {
        planInfo.bedType = &defaultsGetter.bedType
    }

    if planInfo.numberOfBeds == nil {
        planInfo.numberOfBeds = &defaultsGetter.numberOfBeds
    }

    if planInfo.numberOfNights == nil {
        planInfo.numberOfNights = &defaultsGetter.numberOfNights
    }

    planInfo.print()
}

I didn’t come up with this idea until my team member pointed it out in my PR. Think simple, easy. But it’s not always the first idea to catch the simplest, easiest way.

I will take this technique into my tool pocket.

By the way, it could also be a good idea to implement functions to set the default value in the struct and call it in the business logic (useStruct func in this case). Then, there will be no if-else in the function. We can write the test cases one by one for the corresponding function.

Comments

Copied title and URL