Generic function to work on different structs with common members from external package?

Issue

I want to write a single function that can add certain fields to Firebase message structs. There are two different types of message, Message and MulticastMessage, which both contain Android and APNS fields of the same types, but the message types don’t have an explicitly declared relationship with each other.

I thought I should be able to do this:

type firebaseMessage interface {
    *messaging.Message | *messaging.MulticastMessage
}

func highPriority[T firebaseMessage](message T) T {
    message.Android = &messaging.AndroidConfig{...}
    ....
    return message
}

but it gives the error message.Android undefined (type T has no field or method Android). And I can’t write switch m := message.(type) either (cannot use type switch on type parameter value message (variable of type T constrained by firebaseMessage)).

I can write switch m := any(message).(type), but I’m still not sure whether that will do what I want.

I’ve found a few other SO questions from people confused by unions and type constraints, but I couldn’t see any answers that helped explain why this doesn’t work (perhaps because I’m trying to use it with structs instead of interfaces?) or what union type constraints are actually useful for.

Solution

In Go 1.18 you cannot access common fields1, nor common methods2, of type parameters. Those features don’t work simply because they are not yet available in the language. As shown in the linked threads, the common solution is to specify methods to the interface constraint.

However the the types *messaging.Message and *messaging.MulticastMessage do not have common accessor methods and are declared in a library package that is outside your control.

Solution 1: type switch

This works fine if you have a small number of types in the union.

func highPriority[T firebaseMessage](message T) T {
    switch m := any(message).(type) {
    case *messaging.Message:
        setConfig(m.Android)
    case *messaging.MulticastMessage:
        setConfig(m.Android)
    }
    return message
}

func setConfig(cfg *messaging.AndroidConfig) {
    // just assuming the config is always non-nil
    *cfg = &messaging.AndroidConfig{}
}

Playground: https://go.dev/play/p/9iG0eSep6Qo

Solution 2: wrapper with method

This boils down to How to add new methods to an existing type in Go? and then adding that method to the constraint. It’s still less than ideal if you have many structs, but code generation may help:

type wrappedMessage interface {
    *MessageWrapper | *MultiCastMessageWrapper
    SetConfig(c foo.Config)
}

type MessageWrapper struct {
    messaging.Message
}

func (w *MessageWrapper) SetConfig(cfg messaging.Android) {
    *w.Android = cfg
}

// same for MulticastMessageWrapper

func highPriority[T wrappedMessage](message T) T {
    // now you can call this common method 
    message.SetConfig(messaging.Android{"some-value"})
    return message
}

Playground: https://go.dev/play/p/JUHp9Fu27Yt

Solution 3: reflection

If you have many structs, you’re probably better off with reflection. In this case type parameters are not strictly needed but help provide additional type safety. Note that the structs and fields must be addressable for this to work.

func highPriority[T firebaseMessage](message T) T {
    cfg := &messaging.Android{} 
    reflect.ValueOf(message).Elem().FieldByName("Android").Set(reflect.ValueOf(cfg))
    return message
}

Playground: https://go.dev/play/p/3DbIADhiWdO

Notes:

  1. How can I define a struct field in my interface as a type constraint (type T has no field or method)?
  2. In Go generics, how to use a common method for types in a union constraint?

Answered By – blackgreen

Answer Checked By – Clifford M. (GoLangFix Volunteer)

Leave a Reply

Your email address will not be published.