A few months ago I made a meme about Go being my new favourite language (replacing TypeScript) and I just want to share a satisfying moment I had recently.
The Task
In the previous iteration of my website1, I
have a few lines in my config that fetches information about my published NPM
packages to be rendered on the
Projects page. I wanted to do
the same for my new website, outputted as a Markdown table, which can be easily
imported into the page with readFile
.
Initial Code
package main
import (
"encoding/json"
"fmt"
"log"
"os"
"path"
"github.com/bitfield/script"
)
const maintainer = "cbebe"
func main() {
// Create file for writing
f, err := os.Create("data/packages.md")
if err != nil {
log.Fatalf("failed to create file: %v", err)
}
defer f.Close()
// Fetch data from NPM and extract relevant data with jq
req := script.Get("https://registry.npmjs.org/-/v1/search?text=maintainer:" + maintainer)
out, err := req.JQ("[.objects | .[] | {href: .package.links.npm, description: .package.description}]").Bytes()
if err != nil {
log.Fatalf("error fetching packages: %v", err)
}
// Parse JSON into an array of structs
var packages []struct {
Href string
Description string
}
json.Unmarshal(out, &packages)
// Write Markdown into file
fmt.Fprint(f, "| package | description |\n|-|-|\n")
for _, p := range packages {
name := path.Base(p.Href)
fmt.Fprintf(f, "| [%s](%s) | %s |\n", name, p.Href, p.Description)
}
}
The initial code works fine for the task, but I also want it to be formatted
with deno fmt
(for no good
reason since it’s not even commited into version control). My first instinct was
to pipe it into deno fmt
so I wouldn’t have to create a temporary file. I
could do that with the script
2 package using a one-liner.
// equivalent to `echo "$str" | deno fmt - --ext md > data/packages.md`
script.Echo(str).Exec("deno fmt - --ext md").WriteFile("data/packages.md")
Refactoring
I noticed that I only call fmt.Fprint
and fmt.Fprintf
on the file, which
only needs the io.Writer
interface, so we can pull the Markdown writing code
into a function.
func writePackagesTable(w io.Writer) error {
req := script.Get("https://registry.npmjs.org/-/v1/search?text=maintainer:" + maintainer)
out, err := req.JQ("[.objects | .[] | {href: .package.links.npm, description: .package.description}]").Bytes()
if err != nil {
return fmt.Errorf("error fetching packages: %v", err)
}
var packages []struct {
Href string
Description string
}
json.Unmarshal(out, &packages)
// Write Markdown into a Writer
fmt.Fprint(w, "| package | description |\n|-|-|\n")
for _, p := range packages {
name := path.Base(p.Href)
fmt.Fprintf(w, "| [%s](%s) | %s |\n", name, p.Href, p.Description)
}
}
Then in main
I can simply pass the file to this new function.
func main() {
f, err := os.Create("data/packages.md")
if err != nil {
log.Fatalf("failed to create file: %v", err)
}
defer f.Close()
if err := writePackagesTable(f); err != nil {
log.Fatal(err)
}
}
The neat thing is that I can now pass in anything that implements the
io.Writer
interface, which is a lot of things in the standard library. This
includes *bytes.Buffer
, which I can convert into a string and pipe into
deno fmt
before saving the output to a file.
func main() {
- f, err := os.Create("data/packages.md")
- if err != nil {
- log.Fatalf("failed to create file: %v", err)
- }
- defer f.Close()
+ buf := bytes.NewBuffer(nil)
- if err := writePackagesTable(f); err != nil {
+ if err := writePackagesTable(buf); err != nil {
log.Fatal(err)
}
+ script.Echo(buf.String()).Exec("deno fmt - --ext md").WriteFile("data/packages.md")
}
Conclusion
Go’s standard library has a really nice API when it comes to I/O, which makes changes a breeze if you keep your interfaces small. Gems like this makes me love Go even more and it’s definitely going to be my go-to language from now on3.
-
I admit that the Docusaurus site is way cuter, but then I realized that no one should have to download half a megabyte of JS just to read some text on a browser ↩︎
-
Cross-platform Scripting with Go is more fun (and readable!) with the script package which I discovered from this article by the same author. ↩︎
-
Another satisfying moment I had with Go is porting the
docusaurus deploy
command for my website. This cuts down the deploy time for my website from 5 minutes to under 5 seconds!!! This probably has to do more with moving from Docusaurus to Hugo, but there is certainly a bump in speed when executing natively compared to Node.js. ↩︎