When Go 1.8 came out, I read with glee in the language changes:
"When explicitly converting a value from one struct type to another, as of Go 1.8 the tags are ignored. Thus two structs that differ only in their tags may be converted from one to the other:"
func example() {
type T1 struct {
X int `json:"foo"`
}
type T2 struct {
X int `json:"bar"`
}
var v1 T1
var v2 T2
v1 = T1(v2) // now legal
}
The Booking struct below is from real production code, used to standardise booking information downloaded from different online booking sites. Anyway, I naively thought that I could therefore convert this Booking struct (with minimal tags for improved storage):
type Booking struct {
Source string `json:"s,omitempty"`
SourcePropertyId string `json:"pid,omitempty"`
SourceId string `json:"sId,omitempty"`
Source2 string `json:"s2,omitempty"`
Source2Id string `json:"s2Id,omitempty"`
Mode Mode `json:"m,omitempty"`
Created time.Time `json:"at,omitempty"`
Client
Comment string `json:"cm,omitempty"`
Stays []Stay `json:"st,omitempty"`
Payments []Payment `json:"py,omitempty"`
Extras []Extra `json:"ao,omitempty"`
}
To this (more readable tags for convenient debugging and analysis):
type BookingDisplay struct {
Source string
SourcePropertyId string
SourceId string
Source2 string
Source2Id string
Mode ModeDisplay
Created time.Time
Client `json:"Client"`
Comment string
Stays []StayDisplay `json:",omitempty"`
Payments []PaymentDisplay `json:",omitempty"`
Extras []ExtraDisplay `json:",omitempty"`
}
Because the actual structure of the data (all the way down to Go's basic types) is identical, I don't understand the necessities blocking this functionality. For example:
type Extra struct {
Type string `json:"t,omitempty"`
Amount int `json:"am,omitempty"` // cents
Comment string `json:"cm,omitempty"`
}
type ExtraDisplay struct {
Type string
Amount int
Comment string
}
Besides better naming for JSON tags, it can be useful for changing behaviour:
type Mode int
const (
Mode_Update Mode = 0
Mode_New Mode = 1
Mode_Cancel Mode = 2
)
type ModeDisplay int
func (m ModeDisplay) MarshalText() ([]byte, error) {
var t string
switch m {
case Mode_Update:
t = "Update"
case Mode_New:
t = "New"
case Mode_Cancel:
t = "Cancel"
default:
t = "Unknown"
}
return []byte(t), nil
}
Now, I could have copied across various values to the display struct, instead of casting. But that means new fields I may add to the original struct may not be added in the Display version. I couldn't use unsafe as this code runs on Google's std app engine which forbids it - plus I wouldn't get a compile error if the data structure was in any way different. At the moment, this type of casting generates compiler errors when the field names are different, even though what they hold is identical.
It turns out, I've hit this challenge at least a couple of times before, including:
On line 46 of: https://github.com/krolaw/xsd/blob/master/example_test.go
// golibxml._Ctype_xmlDocPtr can't be cast to xsd.DocPtr, even though they are both
// essentially _Ctype_xmlDocPtr. Using unsafe gets around this.
if err := xsdSchema.Validate(xsd.DocPtr(unsafe.Pointer(doc.Ptr))); err != nil {
fmt.Println(err)
return
}
Here I'm getting around the problem by using unsafe. I believe it shouldn't be necessary as the data structure is identical. Using the unsafe solution, I won't get a compiler error if either structure changes, just hard to pin down runtime explosions.
In a closed source router project with a transparent proxy I used a copy of net.TCPConn in order to access the non-exported fields. Alas, I couldn't simply cast, I had to use unsafe. Again, this creates the same potential hazard, as if net.TCPConn ever changes its structure, things may not work as expected.
func GetOrigDstTCP(c *net.TCPConn) (ipv4 net.IP, port uint16, err error) {
return getOrigDst((*Conn)(unsafe.Pointer(c)))
}
func getOrigDst(c *Conn) (ipv4 net.IP, port uint16, err error) {
addr, err := syscall.GetsockoptIPv6Mreq(c.fd.sysfd, syscall.IPPROTO_IP, SO_ORIGINAL_DST)
if err != nil {
return nil, 0, err
}
a := addr.Multiaddr
return net.IP(a[4:8]), uint16(a[2])<<8 + uint16(a[3]), nil
}
So, there we have it. Three examples in production where a 'missing' feature in Go has meant additional complexity and lack of safety. Not sure if anyone else has hit these issues, but it's been my greatest annoyance.
Thanks for your time.