Define golang struct as type with arbitrary fields?

I'm very new to golang and the use of interfaces more generally. I've stubbed out some code here:

type Alerter interface {
    Alert()
}

type AlertConfig struct{}

type Alert struct {
    Config *AlertConfig
}

type AlertConfigurator interface {
    UpdateConfig(key, value interface{}) (*AlertConfig, error)
}

type EmailAlertConfig AlertConfig {
    Recipients []mail.Address,
}

type HTTPAlertConfig AlertConfig {
    Method string
    TargetURL url.URL
}

type HTTPAlert struct {
    Config *HTTPAlertConfig
}

type EmailAlert struct {
    Config *EmailAlertConfig
}

func (ea *EmailAlert) Alert() {
    // this would actually send an email using data in its Config
    return
}

func (ha *HTTPAlert) Alert() {
    // this would actually hit an HTTP endpoint using data in its Config
    return
}

I'm sure I have all kinds of assumptions & biases that are hobbling my ability to express what I want to accomplish with this:

I want to create different types of "Alert"s. Any alert I create should have an "Alert()" method that triggers the Alert to actually do something (send an email, or POST to a URL, for example.

The trouble comes in representing an Alert's "Config". Different Alerts have different fields in their Configs. However, for each Alert type, specific fields are required to be there. To accomplish that I wanted to create a base type "AlertConfig" as a struct with arbitrary fields, then define, say, EmailAlertConfig as type 'AlertConfig', but having these specific fields, and then type 'HTTPAlertConfig' as type 'AlertConfig' requiring different fields. This way, I can define the 'Alert' type as having a field 'Config *AlertConfig'.

I can almost emulate what I want if AlertConfig is defined as map[string]interface{}, but in this case I can't leverage golang's checking to validate that an EmailConfig has the required fields.

It seems pretty clear that I'm thinking about this the wrong way. I could use a hand in getting on the right track & appreciate your advice.


Solution 1:

Declare a type with the common fields:

 type AlertConfig struct { 
    ExampleCommonField string
 }

Embed that type in actual configurations:

type HTTPAlertConfig struct {
    AlertConfig
    Method string
    TargetURL url.URL
}

Based on the code in the question, the alert and config types can be combined.

func (ha *HTTPAlertConfig) Alert() {
    // this will hit an HTTP endpoint using data in the receiver
    return
}

Solution 2:

One way to deal with this problem is to leave configs as purely implementation specific:

type HTTPAlert struct {
  Config *HTTPAlertConfig
}

func (a *HTTPAlert) Alert() {...}

type EmailAlert struct {
  Config *EmailAlertConfig
}

func (e *EmailAlert) Alert() {...}


With this implementation, the actual Alert implementation can use the type-specific alert configuration, leaving the problem of initializing the config.

When you create the instance of an alert, you can perform type-specific initialization. For instance:

var alerts = map[string]func(configFile string) Alert {
   "htmlAlert": NewHTMLAlert,
   "emailAlert" NewEmailAlert,
}

func NewHTMLAlert(configFile string) Alert {
  var alert HTMLAlert
  // Read file, initialize alert
  return &alert
}
...