Search code examples
gointerfacegarbage-collectiongetter-setter

Avoid writing too many getters and setters in Go


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 Msgs since I want the library users to define the concrete type of Msgs.

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 Msgs are created too many times in the execution and dynamically allocating a MsgBase would increase the garbage collection overhead. I really want all the Msgs 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
}


Solution

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