If your application demands marshaling to JSON or another serialiazation format using Go, it can be tempting to design your structures to be cleanly serializable – often to the detriment of the implementation.
You might find yourself exporting variables you shouldn't be exporting, or creating elaborate ways to prevent mutability of attributes you care about.
If you find yourself doing this, stop, and consider the separation of concerns. JSON is a representation of your structure, and should not be directly tied to its underlying implementation, unless it's convenient. Although it's a bit more expensive, consider creating custom marshaling functions and generating one-off structs with the exact fields you need to decouple the implementation from representation.
In the following example, you don't want to export the Value
field from the Counter
struct, because it exposes your value to potentially unsafe operations (two incrementations at the same time, for example):
type Counter struct {
Name string `json:"name"`
Value int64 `json:"value"`
}
func (c *Counter) Incr(delta int64) {
atomic.AddInt64(&c.Value, delta)
}
Instead, create custom marshal or unmarshal functions to generate a representation of your counter, and keep the design of your interface safe. Yes, it's more code, but it no longer influences your internal API or implementation.
type Counter struct {
Name string
value int64
}
func (c *Counter) Incr(delta int64) {
atomic.AddInt64(&c.value, delta)
}
func (c *Counter) MarshalJSON() ([]byte, error) {
repr := struct {
Name string `json:"name"`
Value int64 `json:"value"`
}{
c.Name,
atomic.LoadInt64(&c.value),
}
data, err := json.Marshal(repr)
return data, err
}