Skip to content

Instantly share code, notes, and snippets.

@krolaw
Last active April 2, 2019 11:17
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save krolaw/f010ab0966fd379725ecc48e8bbcac1c to your computer and use it in GitHub Desktop.
Save krolaw/f010ab0966fd379725ecc48e8bbcac1c to your computer and use it in GitHub Desktop.
Should Go Casting be permitted when underlying data structures are the same?

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment