What different techniques and approaches should I follow when creating a Go app?. These articles will try to answer that.
Previosly On… Link to heading
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 🌟 Link to heading
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 toesmit-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 🎌 Link to heading
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:
- build your app, then ⤵
- Use
go tool nm ./cli-sandbox | grep Version
, to find where a variable calledVersion
may be located, - Craft your flag (e.g.
-X 'Version=1.2.3'
) and pass it to thebuild
command! - 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 🖥️ + 🌐 = 💡 Link to heading
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 struct
s 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 Link to heading
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.