Last Updated: July 27, 2016
·
1.029K
· jodosha

Go closures

I had two almost identical functions to parse an XML with XPath:

func xpathContent(root xml.Node, xpath string) string {
  result, _ := root.Search(xpath)

  if len(result) > 0 {
    return strings.TrimSpace(result[0].Content())
  } else {
    return ""
  }
 }

/* and */

func xpathAttribute(root xml.Node, xpath string, attr string) string {
  result, _ := root.Search(xpath)

  if len(result) > 0 {
    return strings.TrimSpace(result[0].Attr(attr))
  } else {
    return ""
  }
 }

Of course there is a lot of repetition, they diff only for the first branch of the if condition. That means we can wrap all the surrounding logic in a separate function and dynamically execute an arbitrary piece of code (content or attribute extraction).

Let's rewrite with closures in mind:

type xpathFn func(node xml.Node) string

func xpathSearch(root xml.Node, xpath string, fn xpathFn) string {
  result, _ := root.Search(xpath)

  if len(result) > 0 {
    return fn(result[0])
  } else {
    return ""
  }
 }

First of all, we need to declare a new type of function, so that we can pass it as argument. The second step is to extract the common logic into a third function and to replace the part we want to be dynamic, with the invocation (fn(result[0])).

Now we can use this new component to rewrite the previous functions:

func xpathContent(root xml.Node, xpath string) string {
  return xpathSearch(root, xpath, func(node xml.Node) string {
    return strings.TrimSpace(node.Content())
  })
}

func xpathAttribute(root xml.Node, xpath string, attr string) string {
  return xpathSearch(root, xpath, func(node xml.Node) string {
    return strings.TrimSpace(node.Attr(attr))
  })
}

Look at how we invoke xpathSearch the third argument is an anonymous xpathFn and its return will became the returning value, only if the search condition is satisfied.

Another aspect that deserves attention is the scope of the innermost function: it has access to the outer vars, like attr.