Go CLI from the ground up (Part 2)

GoLang What different techniques and approaches should I follow when creating a Go app?. These articles will try to answer that.

Previosly On…

In part 1, I created the basic scaffoling using Cobra 🐍, added a couple commands and a basic Makefile script to buld and run our app.

The code corresponding to this article can be found in my GitHub page, Go CLI sandbox - Part 2

Some things can be better 🌟

On second look here’s a few things I don’t like:

  • The version and binary name are harcoded inside cmd/version.go. Cobra actually has a much more powerful mechanism to handle this.
  • When the app is built with our Makefile, the binary name is set to esmit-cli-sandbox, which doesn’t match the current output at all.
  • There’s some remnants to be cleaned up from the originally borrowed (cough) file.
  • The app is always built for Mac OS by default.
  • The website flag simply prints out this site’s FQDN, not very useful.

LDFlags to the rescue 🎌

GO allows you to replace public string variables at compile time, passing a -X 'VarName=ValValue' flag to the build command, e.g.: go -ldflags="-X N=V", provided that your file looks like this, say main.go:

var X = "default value"

If the file is inside a package…it gets complicated, but not too much, basically:

  1. build your app, then ⤵
  2. Use go tool nm ./cli-sandbox | grep Version, to find where a variable called Version may be located,
  3. Craft your flag (e.g. -X 'Version=1.2.3') and pass it to the build command!
  4. For more details see DigitalOcean‘s article.

In my case I wanted to externally drive the value of both the binary name and version reported from the Usage section generated by Cobra, so I ended up with a variable called LDFLAGS, where I also added a tongue-in-cheek nickname for the release:

LDFLAGS=-X 'esmit.me/cli-sandbox/cmd.Version=$(BINARY_RELEASE) \
    ($(BINARY_RELEASE_NICKNAME))'

Last thing to do is putting it all together:

# before (in part 1)
GOBUILD=$(GOCMD) build

# after
BINARY_NAME=esmit-cli-sandbox
BINARY_RELEASE=2020.5.6
BINARY_RELEASE_NICKNAME=Margarita 🍸
LDFLAGS=-X 'esmit.me/cli-sandbox/cmd.Version=$(BINARY_RELEASE) \ 
    ($(BINARY_RELEASE_NICKNAME)) ' \
    -X 'esmit.me/cli-sandbox/cmd.CmdName=$(BINARY_NAME)'

GOBUILD=$(GOCMD) build -ldflags="$(LDFLAGS)"

Finally, I added some smarts to determine the current architecture and os of the system running the build:

GOARCH := $(shell go env GOARCH)
GOOS := $(shell go env GOOS)

All of the above, allows me to just remove ♻️ custom code in version.go and leverage what Cobra already does much better 🎉.

Talking to the world 🖥️ + 🌐 = 💡

My site is created with Hugo (see why I chose it here), and upon publishing it automagically generates RSS feeds for all the tags. I’m going to rely on this and make my little CLI app retrieve them.

Go’s built-in support for XML comes to us via the encoding/xml package, and using a RSS parsing library is probably overkill. I’m just gonna K.I.S.S it.

The XML returned by Hugo via /tags/index.xml looks like this:

<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
    <channel>
        <item>
            <title>tag1</title>
            <link>https://esmit.me/tags/tag1/</link>
        </item>
        <!-- many more tags here ... -->
        <item>
            <title>tagN</title>
            <link>https://esmit.me/tags/tagN/</link>
        </item>
    </channel>
</rss>

My corresponding set of simple structs for Go is then:

type Item struct {
	Title string `xml:"title"`
	Link  string `xml:"link"`
}

type Channel struct {
	Title string `xml:"title"`
	Link  string `xml:"link"`
	Item  []Item `xml:"item"`
}

type RSS struct {
	XMLName xml.Name `xml:"rss"`
	// Xml     string `xml:",innerxml"`
	Channel Channel `xml:"channel"`
}

Retrieving data from a URL in Go is super easy with its http package, just need to create a Client, open a GET request, get the bytes, and parse it!, here’s a simplified version:

// Connect and get body
client := &http.Client{}
res, _ := client.Get(fmt.Sprintf("%s/tags/index.xml", "https://some.site"))
defer res.Body.Close() // you HAVE to close the body, defer it so it eventually executes

// get the bytes and parse them
bodyBytes, _ := ioutil.ReadAll(res.Body)

var document RSS // <- this is our struct from before!
xml.Unmarshal(bodyBytes, &document); // let go's xml package figure out how to dump the XML into our struct

Et Voilà! 🇫🇷, we can now call our program passing the website flag and see LIVE data:

❯ ./esmit-cli-sandbox website

Retrieving https://esmit.me

angularjs
bash
bem
cli
cobra
codepen
costa_rica

What’s Next

On my next article, I’ll wrap things up with Viper and Docker, allowing me to further configure my little app that could and run it in the ☁️, because why not.