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
}
...