
Diving into Go by building a CLI application - eryb
https://eryb.space/2020/05/27/diving-into-go-by-building-a-cli-application.html
======
boyter
Go is a particularly good language for CLI's I have found. At least compared
to Java/C#/Python.

It's reasonably fast, compiles down to a simple to distribute binary, and the
language is forgiving enough that you can do exploratory programming in it.
Go-routines make it especially easy to deal with network calls in it as well.
For anything that needs absolute performance though look elsewhere, but even
then Go might be a good choice for prototyping.

I actually started learning Go with CLI applications. I have found that
[https://github.com/spf13/cobra](https://github.com/spf13/cobra) tends to be
one of the better CLI helpers you can get into but
[https://github.com/jpillora/opts](https://github.com/jpillora/opts) is one I
have been meaning to try following a presentation I saw on it once.

~~~
pansa2
> [Go] is forgiving enough that you can do exploratory programming in it.

I agree with most of your post, but I’m not sure I would describe Go as
“forgiving”. In fact, it’s well known for being strict. For example,
exploratory programming would be significantly easier if the Go compiler could
(optionally) ignore unused variables and unreachable code.

I’ve also found exploratory programming in Go is hindered by needing to
frequently cast between different integer types. Maybe I’m doing something
wrong, but my Go code is often littered with casts between different integer
sizes and signedness. It’s much easier with Python’s arbitrary-precision
integers.

~~~
kstenerud
I maintain a modified compiler
([https://github.com/kstenerud/go/blob/master/README.md#the-
go...](https://github.com/kstenerud/go/blob/master/README.md#the-go-
programming-language)) that can issue warnings instead for unused things.

I use it as my daily driver for go development.

main.go:

    
    
        package main
    
        import "fmt"
    
        func main() {
            var start int = 1
    
            breakOuter:
            // for x := start; x < 10; x++ {
            for x := 3; x < 10; x++ {
                for y := 0; y < 10; y++ {
                    result := y * 10 + x
                    // fmt.Printf("Result: %d\n", result)
                    // if result == 42 {
                    //  fmt.Printf("Breaking")
                    //  break breakOuter
                    // }
                }
            }
        }
    

Building:

    
    
        $ go build
         # example
         ./main.go:3:8: imported and not used: "fmt"
         ./main.go:8:5: label breakOuter defined and not used
        (compilation fails)
    
        $ go build -gcflags=-warnunused
         # example
         ./main.go:8:5: Warning: label breakOuter defined and not used
         ./main.go:3:8: Warning: imported and not used: "fmt"
         ./main.go:6:9: Warning: start declared and not used
         ./main.go:12:13: Warning: result declared and not used
        (compilation succeeds)

~~~
rat9988
That's cool! It is so annoying to just try to try something fast, and then the
compiler stops you...

------
dashwav
Since everyone else is throwing out recommendations I personally think
[https://github.com/spf13/cobra](https://github.com/spf13/cobra) is the best
CLI templating system, especially because of how well it pairs with
[https://github.com/spf13/viper](https://github.com/spf13/viper).

Large projects like Hugo and Kubernetes have used Cobra to build their CLI
tools, and it's fairly light as well even if you need simpler usage. We use it
at my workplace simply for wrapping our microservices and the few commands
(serve, migrate, etc)

~~~
Thorentis
Great stuff! Does anything like Cobra exist for Java?

~~~
bingo_cannon
Absolutely! I've used picocli[1] and airline[2]. There is always the Apache
Commons CLI if you feel like building it all yourself.

1: [https://picocli.info/](https://picocli.info/)

2: [https://github.com/airlift/airline](https://github.com/airlift/airline)

Bonus: picocli lets you create native images using Graal, so you can really
build native cli executable using Java.

------
sandreas
My Opinion:

The best cli lib I found:
[https://github.com/urfave/cli](https://github.com/urfave/cli)

For deployment I recommend:
[https://github.com/goreleaser/goreleaser](https://github.com/goreleaser/goreleaser)

During development I recommend: [https://github.com/golangci/golangci-
lint](https://github.com/golangci/golangci-lint) and
[https://github.com/stretchr/testify](https://github.com/stretchr/testify)

~~~
csixty4
I only recently discovered goreleaser and I can't get over how simple it was
to add to my CI setup. Distributing binaries was my goal for having a "real"
open source project people could use. After a year of avoiding the issue, I
discovered goreleaser and got it running in less than an hour.

~~~
sandreas
I wrote a few small cli tools and the first 2 things i always add is urfave
cli and goreleaser... even if you only deploy and use local, this is a great
way to release getopt compilant multiple platform builds with or without
subcommands.

------
chewxy
Everyone's throwing out suggestions for CLI libraries, so let me plug my
mate's: [https://github.com/jpillora/opts](https://github.com/jpillora/opts)

I definitely prefer it to cobra

------
pulkitsh1234
I wrote a Go CLI Boilerplate sometime back:
[https://github.com/pulkitsharma07/go-cli-
boilerplate](https://github.com/pulkitsharma07/go-cli-boilerplate), it
addresses some common issue which I faced while developing a full-fledged CLI.

Features (From Readme):

* Unit and Integration test structure for the CLI

* Opinionated directory structure for organizing code for commands.

* Docker-based cross-platform build pipeline

* Travis CI-based release workflow

* Makefile for common tasks like generating documentation and building the binary.

------
henvic
I love using Go for CLIs. Previously, I developed CLIs with JavaScript
(NodeGH, something with the same goal as GitHub's CLI), and "kind of" with PHP
as well (for internal tasks on a server). Nothing compares with writing one
with Go, though.

I used to be the maintainer of a CLI for a PaaS until a year ago:
[https://asciinema.org/a/192043](https://asciinema.org/a/192043)
[https://github.com/henvic/wedeploycli](https://github.com/henvic/wedeploycli)

------
maxioatic
If anyone would like a book on this subject, I recommend _Powerful Command-
Line Applications in Go_ : [https://pragprog.com/book/rggo/powerful-command-
line-applica...](https://pragprog.com/book/rggo/powerful-command-line-
applications-in-go)

It's currently in Beta but the first 6 chapters are finished and available. As
someone learning Go I found it a nice complement to reading _The Go
Programming Language_.

------
ayoisaiah
I've been building CLIs with Go as part of a 52 projects in 52 weeks
challenge[1] and I'm loving it so far. I released a batch renaming tool [2]
just a few days ago.

[1]:
[https://github.com/ayoisaiah/project52](https://github.com/ayoisaiah/project52)
[2]:
[https://github.com/ayoisaiah/goname](https://github.com/ayoisaiah/goname)

------
sascha_sl
Whatever you do, do not use the flag library in a package that might ever,
EVER be imported. Google did this in the horribly written glog port to go
which until recently was used everywhere in Kubernetes. The only way to
determine the value of the "v" flag they define globally for your entire
executable is to call the V(n) function with n incrementing until it returns
false.

pflag, which is used by cobra, is a much nicer library.

------
guggle
I want something like Argh
([https://github.com/neithere/argh/](https://github.com/neithere/argh/)) but
for Go... any hint ? There are a gazillion cli libs, it's hard to test all of
them.

------
integrii
I wrote my own flags package called flaggy and think its the easiest to use
and makes the most sense! Up to 600 stars om github now.
[https://github.com/integrii/flaggy](https://github.com/integrii/flaggy)

------
fermienrico
Is it possible to display images in terminal?

Terminal is simultaneously powerful and painful tool. I know a guy that
refuses to use anything but CLI and suffers a lot. But most basic apps can be
written it in like those BIOS menus from a 2005 dell computer.

~~~
emptychombu
Are you talking about ncurses like interfaces? I have been using tview -
[https://github.com/rivo/tview/](https://github.com/rivo/tview/) (based on
another library tcell -
[https://github.com/gdamore/tcell](https://github.com/gdamore/tcell)). Very
useful.

~~~
fermienrico
I am imagining like a gallery app such as Instagram purely in terminal lol
never mind the consequences of the user base being primarily sys admins and
software engineers. I could imagine browsing a rather weird Instagram clone.

------
christiansakai
Wow talk about timing. I just created a simple Markdown viewer with a CSS
switcher.

Shameless promotion:

[https://github.com/christiansakai/md](https://github.com/christiansakai/md)

------
maallooc
As much as I like Go I don't think it's ideal for CLI apps.

IMO Python is the best at creating simple to complex CLI apps due to it being
interpreted and simple. It's an overcharged bash.

~~~
pansa2
The problem with Python is that it’s really hard to distribute a Python app to
users. Nothing beats Go’s ability to compile into a self-contained binary.

Python is also slow and has poor support for parallelism.

Finally, “Python is simple” only really applies to its syntax. Overall, Go is
a significantly simpler language than Python.

~~~
throwaway894345
"Slow" isn't a typically big deal for CLI apps, but Python culture tends to do
a lot of work on initialization, and I've consequently seen a lot of Python
CLIs that take nearly 10 seconds to print --help. Because the way most apps
and libraries are written, you end up loading and initializing every single
library that any part of your CLI uses, even if the subcommand in question
doesn't use those libraries (e.g., printing --help). This is solvable via lazy
imports, but few go through the hassle of doing this, especially since it
makes the code ugly and non-idiomatic.

------
FlashBlaze
Is Go a good option to create some background tasks. For eg. monitor which
Spotify songs are playing and and store that in a json file, etc. Or is Python
more suitable for this?

~~~
eryb
Python has more libraries to write automation tasks than Go. It really depends
on you situation, if I have a lot of time I'll do it in Go because everybody
does it in Python :P

~~~
FlashBlaze
Thanks. If possible can you suggest some good libraries for background tasks
in Python?

------
zouhair
Just a note, xkcd has 2 image sizes.

Small:
[https://imgs.xkcd.com/comics/confidence_interval.png](https://imgs.xkcd.com/comics/confidence_interval.png)

Big:
[https://imgs.xkcd.com/comics/confidence_interval_2x.png](https://imgs.xkcd.com/comics/confidence_interval_2x.png)

Though not for older ones.

~~~
eryb
Thank you for pointing this out, I didn't about it. How did you come to know?
It isn't on their API info page.

~~~
zouhair
Webpage Source code.

------
radicalriddler
I had a similar idea a couple of months ago. Make a CLI in golang that allows
me to easily open all my favorite timewaster sites at the start of the work
day. But it ended up in the "Started, but never looked at again" pile. Maybe
I'll have another look at it on the weekend.

~~~
toomuchtodo
In case you don’t get around to writing that golang code.

[https://unix.stackexchange.com/questions/17659/opening-
multi...](https://unix.stackexchange.com/questions/17659/opening-multiple-
urls-from-a-text-file-as-different-tabs-in-firefox-chrome)

~~~
mikewhy
Also could be handled via `open` (macOS) / `xdg-open` (Linux) / `start`
(Windows).

------
mongol
I liked go-flags [https://github.com/jessevdk/go-
flags](https://github.com/jessevdk/go-flags)

------
subsaharancoder
Took your code, added a random number generator and threw it into a Go HTTP
server and deployed it as a GCP Cloud Function :-)

[https://us-central1-bookshelf-app-1103.cloudfunctions.net/Ra...](https://us-
central1-bookshelf-app-1103.cloudfunctions.net/RandomXkcd)

Voila!! Serverless Random XKCD..

~~~
eryb
Amazing... :)

------
verdverm
Have a look at [https://github.com/hofstadter-io/hofmod-
cli](https://github.com/hofstadter-io/hofmod-cli)

It makes framing out advanced Golang CLIs a breeze

*(creator)

------
assafmo
Bash Bonus #2:

    
    
       seq 1 10 | xargs -n 1 ./go-grab-xkcd -s -n

------
edem
Why would I dive into Go in the first place?

~~~
tmountain
Here are some arbitrary reasons that Go is worth a look:

It's easy to learn, tractable, consistent, performant, and safe.

It's great for writing services. The type system is predictable and catches a
lot of problems ahead of time.

It's opinionated about basic stuff which helps avoid clutter (no unused
variables or imports allowed, etc).

It fills a lot of the same niche as Python, Java, Ruby, etc, but with some
distinct advantages when compared to each of them (and some drawbacks
depending on your preferences).

Binaries are standalone, easy to compile (cross compilation), and easy to
distribute.

It's fun (subjective, but I enjoy writing Go).

------
winrid
I personally love building CLI tools in Node, since I and my co-workers all
have it installed. Nice synchronous STD lib for file manipulation and
async/await makes for compact async code.

If I worked in a Go shop I'd probably use Go, though.

~~~
mpfundstein
i always find it 'perverse' to start up a whole async event loop for a tool
that then transforms a csv or does some other single task :-)

also portability quite sucks. if you use features that my node version doesn't
support, i screwed.

with go (or C, Rust...), I just compile and distribute the binaries. look for
instance how easy it is to install nomad.

~~~
vips7L
> start up a whole async event loop

Doesn't Go start NCPU event loops every time?

~~~
mpfundstein
does it?

~~~
vips7L
I'm not a Go dev, but is that not what the go runtime is? An event loop for
NCPUs or a work stealing queue that uses NCPUs?

------
earthboundkid
Seems overcooked in some places and undercooked in others. I of course prefer
my own CLI template but so it goes: [https://github.com/carlmjohnson/go-
cli](https://github.com/carlmjohnson/go-cli)

~~~
eryb
Would you mind sharing some concrete feedback? I'll make improvements for next
time.

~~~
earthboundkid
Sure, I’m on a real keyboard now.

You don’t need separate client and models packages. You should have two
packages: main (which has only one line) and everything else (call it package
xkcd).

Your main continues even after it finds an error in fetching the comic. This
is because you can’t return err in main and you didn’t use panic/log.Fatal.
You also don’t exit with a non-zero code on error.

The solution to all of this is the one line main function. Main should look
something like os.Exit(xkcd.Run(os.Args[1:])). I have a helper package at
github.com/carlmjohnson/exitcode so you can return an error with an associated
exit code, but you can also be simple and just return an int.

Separate flag gathering/processing from execution. You’re not using global
flags, which is a good step, but you should go further and make an appEnv
struct that consolidates what you’re taking in from the flags. See here:
[https://play.golang.org/p/-gs5nqXBSuB](https://play.golang.org/p/-gs5nqXBSuB)

Your client package basically doesn’t need to exist. What you want are some
simple convenience wrappers around the http package and a URL builder for
XKCD. Make something generic for HTTP and you can copypasta it into your
future projects. Basically, it just needs to be httphelper.GetJSON(cl
∗http.Client, url string, data interface{}) error. Saving to disk is a
separate idea that you’re conflating with downloading. Make something like
jsonhelper.SaveToDisk(path string, data interface{}) error. The timeout stuff
you’re doing is overblown. Either just use context.WithTimeout or put a
∗http.Client in your appEnv and have ParseArgs set the default timeout on
that.

The stuff with the base URL is unnecessary. Obviously you know the XKCD base
URL is a constant and will never change. But don’t you need to set it in an
XKCDClient struct for testing purposes? The answer is no. If you take in a
∗http.Client, that can set a different http.RoundTripper for testing purposes
and the test RoundTripper can read from memory or do whatever you want. (You
can see this principle at work in Google’s Go http libraries. Once you realize
how powerful ∗http.Client is, it makes a lot of the hoops other libraries jump
through see like a waste of time. It can do all your auth stuff, caching
stuff, everything. It’s great.)

The model package should just be a file in your xkcd package. I find the names
Comic vs. ComicResponse confusing. Do you need Comic at all? Maybe just add
some nice helper methods onto ComicResponse. Don’t do cr.FormattedDate()
string. Do cr.Date() time.Time and let the output layers handler formatting,
not the model layer. The c.JSON() string method doesn’t need to exist. With
Go, you can run into this problem of trying to make things more convenient by
adding helpers but you end up with methods that just run two commands and
don’t actually make things more convenient on net. Is this really a model
level concern or should it just be in the xkcd.Run() function?

Anyway, not to be overly negative. For such a small app, none of this really
matters. I’ve been making a lot of Go CLIs for a long time[1] and my
experience is that the most important thing is to separate flag stuff from
execution, and everything else is not a big deal to let evolve over time. The
main challenge is avoiding create abstractions that don’t actually pay for
themselves in setup time vs. time saved in extension.

[1]: [https://blog.carlmjohnson.net/post/2018/go-cli-
tools/](https://blog.carlmjohnson.net/post/2018/go-cli-tools/)

~~~
eryb
I really appreciate you effort in pointing out the correct way to do things.

In my defence I would like to point that this post was to help beginners get
hands on experience writing Go code, adding design patterns or organising the
code base to make things "correct" will only confuse a person who has just
started to learn Go.

And to be frank, I too don't have much practical knowledge about it. If you
don't mind can you point me in the right direction?

~~~
earthboundkid
I wrote a blog post about my rewrite:
[https://blog.carlmjohnson.net/post/2020/go-cli-how-to-and-
ad...](https://blog.carlmjohnson.net/post/2020/go-cli-how-to-and-advice/)

