Getting fancy with closures in Go

While at the hackday at Gophercon I thought it would be a good idea to do some code refactoring of Cortex.

Over the past 24 hours I made many changes, but one that I specially liked was that I could pass a function as a parameter to another function. This is possible in Go because functions are first class (values? | elements?) And this made the code a lot cleaner.

Before.

Initially I only had this one function to fetch the list of flows:

//fetchFlows fetches all the flows we have access to
func fetchFlows() {
	url := fmt.Sprintf("https://%s@api.flowdock.com/flows", config.FlowdockAccessToken)
	res, err := http.Get(url)
	if err != nil {
		log.Fatalf("Error getting list of flows: %v", err)
	} else if res.StatusCode != 200 {
		log.Fatalf("got status code %+v", res.StatusCode)
	}

	parseAvailableFlows(res.Body)
	res.Body.Close()
}

And this was to parse the json response:

func parseAvailableFlows(body io.ReadCloser) {
	flowsAsJon, err := ioutil.ReadAll(body)
	if err != nil {
		log.Fatalf("error reading body, got: %+v", err)
	}

	if ok := json.Unmarshal(flowsAsJon, &availableFlows); ok != nil {
		log.Fatalf("Error parsing flows data %+v", ok)
	}
}

But then I wanted to also fetch the list of current users, so I ended up with pretty much the same two functions duplicated, which wasn't that great.

Refactor step 1:

First I moved the logic to fetch data from a url to a generic function:

func performGet(path string) {
	url := fmt.Sprintf("https://%s@api.flowdock.com/%s", config.FlowdockAccessToken, path)
	res, err := http.Get(url)
	if err != nil {
		log.Fatalf("Error getting %+v: %v", path, err)
	} else if res.StatusCode != 200 {
		log.Fatalf("got status code %+v", res.StatusCode)
	}
	dataAsJon, err := ioutil.ReadAll(res.Body)
	if err != nil {
		log.Fatalf("error reading body, got: %+v", err)
	}
	//this is where I wanted to start parsing data
	res.Body.Close()
}

This was nice, but the question is, how do I run any specific function depending on what I'm parsing?

Enter closures.

Turned out it was pretty easy. I defined a new type type parseCallback func([]byte) this is the type signature of the parse functions I have

and then I had to change the parseAvailableFlows just a little bit:

func parseAvailableFlows() parseCallback {
	return func(payload []byte) {
		err := json.Unmarshal(payload, &availableFlows)
		if err != nil {
			log.Fatalf("Error parsing flows data %+v", err)
		}
	}
}

The way I like to think about it is that, we make the original function not take a parameter (you can if you need to, but here I didn't need it), then the signature of the function shows what you expect as input and output, here I expect a []byte as input and I don't return anything.

Then enclose the body of your original function into a return func(in type){ ... } (anonymous function)

So the end result for the generic performGet was:

func performGet(path string, f parseCallback) { // <== note how we expect a valud f of type `parseCallback`
	url := fmt.Sprintf("https://%s@api.flowdock.com/%s", config.FlowdockAccessToken, path)
	res, err := http.Get(url)
	if err != nil {
		log.Fatalf("Error getting %+v: %v", path, err)
	} else if res.StatusCode != 200 {
		log.Fatalf("got status code %+v", res.StatusCode)
	}
	dataAsJson, err := ioutil.ReadAll(res.Body)
	if err != nil {
		log.Fatalf("error reading body, got: %+v", err)
	}
	f(dataAsJson)// <== Here is where we call the callback function and
	//pass the json we just got
	//this also let's me close the body in this same function,
	//instead of passing it to whoever called this
	//function and make them close the `Body` value
	res.Body.Close()
}

Final API

So now I simply have:

//fetchFlows fetches all the flows we have access to
func fetchFlows() {
	performGet("flows", parseAvailableFlows())
}

and fetching users is:

func fetchUsers() {
	performGet("users", parseUsers())
}

which I think looks pretty nice.

Code.

If you would like to see this code in context, you can check the Cortex code at this commit

Updated based on @elimisteve 's comments

Thank you for reading and don't hesitate to leave a comment/question.

@fmpwizard

Diego