I manually removed personal bookmarks before sharing the list. :p
I shared it because I found it interesting revisiting them and thought that others might too enjoy.
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
While following along locally I got an unhelpful error the first time I ran the program. Ultimately the bug was that I was using a lowercase 'o' instead of a zero '0' in the url builder. The API returned a 404, but the code ignored that and tried to marshal the html error response to the struct and failed.
For your guide, it would be helpful to include a status code check on the response and returning a helpful error message around that.
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
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.
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?
One method I've used which puts some additional pressure on me to work on side projects is to write them out in the open in a public GitHub repo. Even though there's probably nobody following your development, the idea that anyone could be following along can provide some motivation to keep going.
Is your goal to learn go (or other language), or to accomplish a specific task (both valid and wonderful goals!)? The reason I ask because if it's the second, I've become dramatically more productive by really learning shell. Just as an example:
That took maybe a minute for me to write, vs. like 20 or so minutes for Go. I personally can get stuck in a such a paralysis of doing things the "right" way in a "real" language. So, it's nice to be able to bang out a prototype super quickly and then iterate from there if I want.
edit: getting them all, because who can resist some fun code golf:
Semi-abandoned just like my side projects ;)