I am implementing a message passing system in Go. So I have a general interface called Msg
. The Msg
interface defines many common fields such as source, destination, send time, receive time, etc. I cannot define a full list of Msg
s since I want the library users to define the concrete type of Msg
s.
To provide a concrete type of Msg
, a user would need to implement a large list of getters and setters, which is very annoying.
One solution I tried is to provide a simple base class like MsgBase
and defines all the common properties and getters and setters. And for each concrete type of Msg
, I embed a pointer to the MsgBase
. This solution works.
But then, I want to embed a value version of the MsgBase
in the concrete Msg
types. This is because such Msg
s are created too many times in the execution and dynamically allocating a MsgBase
would increase the garbage collection overhead. I really want all the Msg
s are allocated statically since they are passed by the components and should never be shared. If I use the value version of the MsgBase
, I cannot use the setters defined in the MsgBase
.
I wonder if there is any simple solution to this problem?
EDIT: Adding sample code
type Msg interface {
// Agent is another interface
Src() Agent
SetSrc(a Agent)
Dst() Agent
SetDst(a Agent)
... // A large number of properties
}
type MsgBase struct {
src, dst Agent
... // Properties as private fields.
}
func (m MsgBase) Src() Agent {
return m.src
}
func (m *MsgBase) SetSrc(a Agent) {
m.src = a
}
... // Many other setters and getters for MsgBase
type SampleMsg struct {
MsgBase // option1
*MsgBase // option2
}
Remember that Go doesn't have object-oriented inheritance in the same way Java does. This sounds like you're trying to write an abstract base class that encapsulates all of the parts of a "message"; that's not really typical Go style.
The fields you're describing are typical message metadata. You can encapsulate this metadata in a pure-data structure. It doesn't necessarily need any behavior, and it doesn't necessarily need getter and setter methods.
type MessageMeta struct {
Source Agent
Destination Agent
}
The more object-oriented approach is to say a message has a (mutable) metadata block and a (immutable, encoded) payload.
import "encoding"
type Message interface {
encoding.BinaryMarshaler // requires MarshalBinary()
Meta() *MessageMeta
}
type SomeMessage struct {
MessageMeta
Greeting string
}
func (m *SomeMessage) Meta() *MessageMeta {
return &m.MessageMeta
}
func (m *SomeMessage) MarshalBinary() ([]byte, error) {
return []byte(m.Greeting), nil
}
A more procedural approach that passes these two things separately is also reasonable. In this case there's no interface for what's a "message", you just pass on the encoded payload; standard-library interfaces like encoding.BinaryMarshaler
could make sense here. You might include this in a lower-level interface that's part of your library.
func Deliver(meta *MessageMeta, payload []byte) error { ... }
Translating one to the other is easy
func DeliverMessage(m Message) error {
payload, err := m.Payload()
if err != nil {
return err
}
meta := m.Meta()
return Deliver(meta, payload)
}
If one of the metadata fields is "delivered at", making sure to pass a pointer to the metadata object all the way through the chain lets you update that field in the original object.
I would not worry about garbage collection as a first-class consideration, except to avoid being overtly wasteful, and to check up on object allocation if GC starts showing up in profiles. Creating two objects instead of one probably isn't going to be a huge problem here.