Structural data typing in Go

Categories: Programming

A common annoyance when doing certain types of API coding is Go’s lack of structural data typing. In my personal experience, this can result in having to write a massive amount of boilerplate to convert between concrete types that are broadly the same, but are either slightly different (say, a DB type that has a few extra fields than a domain type which in turn has a few extra fields than an API return type) or are just in different packages. Careful coding can of course avoid some of these issues, but if you’re generating those types, there’s nothing much you can do about it. You just have to accept that you have a bunch of boilerplate and dream longingly about Typescript’s type system.

But what if I told you that Go does support structural data typing, it’s just almost completely useless? Well it does, and it is in fact, almost completely useless. Lets first make a Typescript example to try to recreate:

interface ColourfulCircle {
    colour: string;
    radius: number;
}

function DescribeCircle(c: ColourfulCircle) {
    console.log(`Radius was ${c.radius}`)
    console.log(`Colour was ${c.colour}`)
}

interface AnotherColourfulCircle {
    colour: string;
    radius: number;
}

let c: AnotherColourfulCircle = {
    colour: "red",
    radius: 42,
}

DescribeCircle(colourfulCircle)

Ignoring the fact that this is a rather contrived example and you wouldn’t write Typescript this way, this works fine, if you run this it will print out

Radius was 42
Colour was red

So lets try making that in Go (playground link)

package main

import "fmt"

type ColourfulCircle struct {
	Colour string
	Radius int
}

func DescribeCircle(c ColourfulCircle) {
	fmt.Printf("Radius was %d\n", c.Radius)
	fmt.Printf("Colour was %s\n", c.Colour)
}

type AnotherColourfulCircle struct {
	Colour string
	Radius int
}

func main() {
	x := AnotherColourfulCircle{
		"red",
		42,
	}
	DescribeCircle(x)
}

and we get… a compile error :(

./prog.go:27:17: cannot use x (variable of struct type AnotherColourfulCircle) as ColourfulCircle value in argument to DescribeCircle

But what if we assign the AnotherColourfulCircle to a structurally identical anonymous struct first before passing it to DescribeCircle:

func main() {
	x := AnotherColourfulCircle{
		"red",
		42,
	}
	var y struct {
		Colour string
		Radius int
	} = x
	DescribeCircle(y)
}

And voila, Go supports structural data types!

Radius was 42
Colour was red

But wait you say, that’s kind of awkward, but what if the type definition in DescribeCircle’s parameters was anonymous? Does that work? And the answer is, yes, yes it does!

func DescribeCircle(c struct {
	Colour string
	Radius int
}) {
	fmt.Printf("Radius was %d\n", c.Radius)
	fmt.Printf("Colour was %s\n", c.Colour)
}

func main() {
	x := AnotherColourfulCircle{
		"red",
		42,
	}
	DescribeCircle(x)

	y := ColourfulCircle{
		"blue",
		23,
	}
	DescribeCircle(y)
}

When you run this you get:

Radius was 42
Colour was red
Radius was 23
Colour was blue

Woo! Go supports structural data types! But unfortunately as soon as you add another field to one of the named structs, it falls over again:

package main

import "fmt"

type ColourfulCircle struct {
	Colour string
	Radius int
}

func DescribeCircle(c struct {
	Colour string
	Radius int
}) {
	fmt.Printf("Radius was %d\n", c.Radius)
	fmt.Printf("Colour was %s\n", c.Colour)
}

type AnotherColourfulCircle struct {
	Colour       string
	Radius       int
	Transparency float64
}

func main() {
	x := AnotherColourfulCircle{
		"red",
		42,
		23.2,
	}
	DescribeCircle(x)

	y := ColourfulCircle{
		"blue",
		23,
	}
	DescribeCircle(y)
}

Gives a good ‘ol compile error when you try to run it.

./prog.go:32:17: cannot use x (variable of struct type AnotherColourfulCircle) as struct{Colour string; Radius int} value in argument to DescribeCircle

So Go supports structural data typing via anonymous struct definitions if and only if the structs are exactly identical. Even reordering the fields in the struct is enough to break it. So is this useful? No, not really. In my opinion it can be largely relegated to the corner where you keep language trivia that’s only useful for messing with your coworkers. But it is kind of neat, like most of the stuff that lives in that corner.