Golang Type Switch does not work for a pointer in struct?

eye-catch Golang

I wrote this article because I didn’t know the behavior of type switch well especially when the target type is pointer type. I wrote test code to see the behavior well, I will share it with you.

Sponsored links

What is type switch

I call the following a type switch.

switch v := value.(type){
    case int:
        // do something
    case string:
        // do something
    default:
        // do something
}

If a function takes a any data type parameter, we need to handle the value differently depending on the actual data type. This type switch enables us to handle the value properly.

Sponsored links

Confirm the behavior for literal type

Let’s write a simple code to test.

func doSwitchType(value any) {
    switch val := value.(type) {
    case int:
        fmt.Println("int")
    case *int:
        fmt.Println("int pointer")
    default:
        fmt.Println("default")
    }
}

If this function is called with a parameter, the data type is properly handled even if it’s a pointer.

intValue := int(10)
doSwitchType(intValue)  // int
doSwitchType("this is string") // default
doSwitchType(&intValue) // int pointer

This is simple and it’s handled properly. If we pass an address, it’s handled as a pointer. It makes sense.

Confirm the behavior for struct type

Okay. Let’s extend the function so that we can use it for struct too.

func doSwitchType(value any) {
    switch val := value.(type) {
    case int:
        fmt.Println("int")
    case *int:
        fmt.Println("int pointer")
    case person:
        fmt.Printf("(person) name: %s\n", val.Name)
    case *person:
        fmt.Printf("(person pointer) name: %s\n", val.Name)
    default:
        fmt.Println("default")
    }
}

If a struct is passed here, it’s also properly handled as expected.

yuto := person{
    Name: "Yuto",
}
doSwitchType(yuto)  // person
doSwitchType(&yuto) // person pointer

Then, if we pass another struct it should be handled as a default case.

type anyInStruct struct {
    value any
}

containsStruct := anyInStruct{
    value: yuto,
}

doSwitchType(containsStruct)  // default
doSwitchType(&containsStruct) // default

This is the expected behavior.

How does it behaves if we pass the target value via dot chain

Let’s go to one step more. Let’s check how it’s handled if we pass a property of a struct.

containsStruct := anyInStruct{
    value: yuto, // yuto is 'person' type
}

doSwitchType(containsStruct.value)    // person
doSwitchType(&containsStruct.value)   // default
doSwitchType(&(containsStruct.value)) // default

Wait!! The first one is okay. containsStruct.value is handled as person type since it holds person type. It’s natural.

However, the second and third ones are not handled as pointers of person type even though I passed an address of the value!!

Let’s check if an address is passed.

func checkStructInfo(obj any) {
    value := reflect.ValueOf(obj)
    fmt.Printf("struct size for [%s] is %d (any: %d)\n", value.Type().Name(), value.Type().Size(), unsafe.Sizeof(obj))

    for i := 0; i < value.Type().NumField(); i++ {
        typeField := value.Type().Field(i)
        field := value.Field(i)
        fmt.Printf("  %-10s: (offset: %d, align: %d, size: %d)\n", typeField.Name, typeField.Offset, field.Type().Align(), field.Type().Size())
    }
}

fmt.Printf("%p, %p, %p\n", &containsInt, &containsInt.value, &(containsInt.value))
// 0xc000014180, 0xc000014180, 0xc000014180
checkStructInfo(containsInt)
// struct size for [anyInStruct] is 16 (any: 16)
// value     : (offset: 0, align: 8, size: 16)

As you can see, the address of the property is the same as the struct. The address of the struct itself and the first property’s address are always the same. Is this the reason that it was not handled as a pointer?

Let’s change the struct a bit.

type anyInStruct struct {
    offset int  // <- new property
    value  any
}

This modification makes the address of value property different from the struct itself.

fmt.Printf("%p, %p, %p\n", &containsInt, &containsInt.value, &(containsInt.value))
// 0xc00012e030, 0xc00012e038, 0xc00012e038
checkStructInfo(containsInt)
// struct size for [anyInStruct] is 24 (any: 16)
//   offset    : (offset: 0, align: 8, size: 8)
//   value     : (offset: 8, align: 8, size: 16)

However, this modification doesn’t affect the result for the any data type. If it’s a concrete data type, it’s handled properly.

doSwitchType(containsInt.offset)    // int
doSwitchType(&containsInt.offset)   // int pointer
doSwitchType(&(containsInt.offset)) // int pointer
doSwitchType(containsInt.value)     // int
doSwitchType(&containsInt.value)    // default
doSwitchType(&(containsInt.value))  // default

Is a pointer handled properly?

We learned that type switch doesn’t handle a data property if an address is passed with ‘&‘ mark. What if we pass an actual pointer? Let’s check it here.

containsPointerInt := anyInStruct{
    value: &intValue,
}
containsPointerStruct := anyInStruct{
    value: &yuto,
}
doSwitchType(containsPointerInt.value)    // int pointer
doSwitchType(containsPointerStruct.value) // (person pointer) name: Yuto

This time it’s handled properly.

What should beware if we want to update the value?

This is the main concern. If we just want to read the value, it won’t be a problem but we should beware for data write. If we want to update data in a function, a pointer must be passed.

func updateData(value *any) {
    switch val := (*value).(type) {
    case int:
        fmt.Print("int  - ")
        val = 111
    case *int:
        fmt.Print("pointer int  - ")
        newValue := 999
        *val = newValue
    case person:
        fmt.Print("person  - ")
        val.Name = "someone"
    case *person:
        fmt.Print("pointer person  - ")
        val.Name = "someone2"
    default:
        fmt.Println("do nothing")
    }
}

We expect that the value is updated in this function but the result is actually not.

intValue := int(10)
yuto := person{
    Name: "Yuto",
}
containsPointerInt := anyInStruct{
    value: &intValue,
}
containsStruct := anyInStruct{
    value: yuto,
}
containsPointerStruct := anyInStruct{
    value: &yuto,
}
updateData(&containsInt.value)
fmt.Println(containsInt.value) 
// int  - 10
updateData(&containsStruct.value)
fmt.Println(containsStruct.value) 
// person  - {Yuto 0 }
updateData(&containsPointerStruct.value)
fmt.Println(containsPointerStruct.value) 
// pointer person  - &{someone2 0 }

As we saw in the previous section, if the actual data is not a pointer, it’s not handled as a pointer value. Therefore, the data modification isn’t reflected in the original struct. If a non-pointer value is assigned, the new value must be assigned on the caller side of the function.

However, it would be a better idea to pass a pointer of the struct to a function if it contains any data type. In this way, we don’t have to take care of the difference, and thus it doesn’t lead to a coding error.

We should know the behavior if we need to process in a loop. So, let’s finish with this example.

func printBySwitchType(value any) {
    switch val := value.(type) {
    case int:
        fmt.Println(val)
    case *int:
        fmt.Println(*val)
    case person:
        fmt.Println(val)
    case *person:
        fmt.Println(val)
    default:
        fmt.Println("default")
    }
}

intValue2 := 22
yuto = person{
    Name: "YUTO",
    Age:  36,
}
items := []anyInStruct{
    {
        value: 10,
    },
    {
        value: &intValue2,
    },
    {
        value: person{
            Name: "yuto",
        },
    },
    {
        value: &yuto,
    },
}

for _, item := range items {
    updateData(&item.value)
    printBySwitchType(item.value)
}
// int  - 10
// pointer int  - 999
// person  - {yuto 0 }
// pointer person  - &{someone2 36 }

The developer who created the function expected that the value is always updated in the function but actually not.

Comments

Copied title and URL