Slices of Slices of Slices in Go

Sep 10, 2013


I am working on building code to load polygons for the different Marine Forecast areas in the United States. These polygons need to be stored in MongoDB and there is a special way that needs to be done. It would not have been a big deal if it wasn’t for this fact. There isn’t just one polygon for each area. There is an external polygon and then zero to many interior polygons that need to be stored in relationship.

After staring at the problem for a bit I realized that I needed to create a slice of Marine Forecast areas, each of which contained a slice of polygons. To store each polygon ring I needed a slice of geographic coordinates. Finally each coordinate needed to be stored in a two dimensional array of floats.

A picture is worth a thousand words:


When the data is stored in MongoDB it needs to follow this data pattern:


My head is spinning just looking at the diagram and the picture. The diagram depicts how all the slices and objects need to be organized.

The picture shows how the polygons need to be stored in MongoDB. There will be multiple elements under coordinates, each with its own set of points.

I decided to build a test application to figure out how to structure and store the data.

The more I use slices the more I really love them. I love how I can pass them in and out of functions and not concern myself with handling references or how memory is being handled. A slice is a lightware data structure that can safely be copied in and out of functions.

I catch myself thinking all the time that I need to pass a reference of the slice so a copy of the data structure is not made on the stack. Then I remember, the data structure is 24 bytes, I am not copying all the data that is abstracted underneath it.

Read these two articles to learn more about slices:

http://www.goinggo.net/2013/08/understanding-slices-in-go-programming.html
http://www.goinggo.net/2013/08/collections-of-unknown-length-in-go.html

Let’s look at the data structure that will hold and store the data for MongoDB:

// Polygon defines a set of points that complete a ring
// around a geographic area
type Polygon [][2]float64

// PolygonRings defines a MongoDB Structure for storing multiple polygon rings
type PolygonRings struct {
    Type string           bson:"type"
    Coordinates []Polygon bson:"coordinates"
}

// Represents a marine station and its polygons
type MarineStation struct {
    StationId string      bson:"station_id"
    Polygons PolygonRings bson:"polygons"
}

The Polygon type represents a slice of 2 floating point numbers. This will represent each point that makes up the polygon.

The PolygonRings structure takes on the MongoDB format required for storing polygons. If you want to use MongoDB to perform geospatial searches against the polygons this is required.

The MarineStation structure simulates an individual station and the set of polygons associated with the station.

The test code is going to create one station with two polygons. Then it will display everything.  Let’s look at how to create the slice of marine stations and create a single marine station for testing:

// Create a nil slice to store the polygon rings
// for the different marine stations
var marineStations []MarineStation

// Create a marine station for AMZ123
marineStation := MarineStation{
    StationId: "AMZ123",
    Polygons: PolygonRings{
        Type: "Polygon",
        Coordinates: []Polygon{},
    },
}

The first line of code creates a nil slice that can hold MarineStation objects. Then we create a MarineStation object using a composite literal. Within the composite literal we have another composite literal to create an object of type PolygonRings for the Polygons property. Then within the creation of the PolygonRings object we create an empty slice that can hold Polygon objects for the Coordinates property.

To learn more about composite literals check out this document:

http://golang.org/ref/spec#Composite_literals

Now it is time to add a couple of polygons to the station:

// Create the points for the first polygon ring
point1 := [2]float64{-79.7291190729999, 26.9729398600001}
point2 := [2]float64{-80.0799532019999, 26.9692689500001}
point3 := [2]float64{-80.0803627959999, 26.970533371}
point4 := [2]float64{-80.0810508729999, 26.975004196}
point5 := [2]float64{-79.7291190729999, 26.9729398600001}

// Create a polygon for this ring
polygon := Polygon{point1, point2, point3, point4, point5}

// Add the polygon to the slice of polygon coordinates
marineStation.Polygons.Coordinates = append(marineStation.Polygons.Coordinates, polygon)

First we create five points. Notice the first and last point are identical. This completes the ring. Then we store all the points into a Polygon object, using a composite literal. Last, we append the Polygon object to the slice of Polygons for the marine station.

Then we do it all over again so we have two polygons associated with this marine station:

// Create the points for the second polygon ring
point1 = [2]float64{-80.4370117189999, 27.7877197270001}
point2 = [2]float64{-80.4376220699999, 27.7885131840001}
point3 = [2]float64{-80.4384155269999, 27.7885131840001}
point4 = [2]float64{-80.4370117189999, 27.7877197270001}

// Create a polygon for this ring
polygon = Polygon{point1, point2, point3, point4}

// Add the polygon to the slice of polygon coordinates
marineStation.Polygons.Coordinates = append(marineStation.Polygons.Coordinates, polygon)

This second polygon has four points instead of five. The last thing left to do is add the MarineStation object to the slice of stations and display everything:

// Add the marine station
marineStations = append(marineStations, marineStation)

Display(marineStations)

The display function uses the keyword range to iterator over all the slices:

func Display(marineStations []MarineStation) {
    for _, marineStation := range marineStations {
        fmt.Printf("\nStation: %s\n", marineStation.StationId)

        for index, rings := range marineStation.Polygons.Coordinates {
            fmt.Printf("Ring: %d\n", index)

            for _, coordinate := range rings {
                fmt.Printf("Point: %f,%f\n", coordinate[0], coordinate[1])
            }
        }
    }
}

The function takes a slice of MarineStation objects. Remember only the slice structure is being copied on the stack, not all the objects the slice represents.

When we iterate through the slice of MarineStation objects and all the internal slices that make up the object, we get the following result:

Station: AMZ123
Ring: 0
Point: -79.729119,26.972940
Point: -80.079953,26.969269
Point: -80.080363,26.970533
Point: -80.081051,26.975004
Point: -79.729119,26.972940
Ring: 1
Point: -80.437012,27.787720
Point: -80.437622,27.788513
Point: -80.438416,27.788513
Point: -80.437012,27.787720

Using slices to solve this problem is fast, easy and effective. I have placed a working copy of the test code in the Go Playground:

http://play.golang.org/p/UYO2HIKggy

Building this quick test application has shown me again how using slices has very real advantages. They will make you more productive and your code perform well. Not having to worry about memory management and handling references to pass data in and out of functions is huge. Take the time to learn how to use slices in your code, you will thank yourself later.


Ultimate Go Programming LiveLessons

Ultimate Go Programming LiveLessons provides an intensive, comprehensive, and idiomatic view of the Go programming language. This course focuses on both the specification and implementation of the language, including topics ranging from language syntax, design, and guidelines to concurrency, testing, and profiling. This class is perfect for anyone who wants a jump-start in learning Go or wants a more thorough understanding of the language and its internals.

Learn more

Go Training

We have taught Go to thousands of developers all around the world since 2014. There is no other company that has been doing it longer and our material has proven to help jump start developers 6 to 12 months ahead of their knowledge of Go. We know what knowledge developers need in order to be productive and efficient when writing software in Go.

Our Go, Web and Data Science classes are perfect for both experienced and beginning engineers. We start every class from the beginning and get very detailed about the internals, mechanics, specification, guidelines, best practices and design philosophies. We cover a lot about "if performance matters" with a focus on mechanical sympathy, data oriented design, decoupling and writing production software.

Learn More

To learn about Corporate training events, options and special pricing please contact:

William Kennedy
ArdanLabs (www.ardanlabs.com)
bill@ardanlabs.com