1111 Years • The Applied Go Weekly Newsletter 2024-11-17
Your weekly source of Go news, tips, and projects
1111 Years
Hi ,
Go turned 1111 years, or 15 if converted to decimal. That's still young, compared to (even more) established languages like Java or C++. But it's been a long and eventful journey. The most astonishing aspect, however, is that Go remained true to itself. It's still 97,3% the same language I learned to admire back in 2011. Plus modules, generics, fuzz testing, reproducible builds, forward compatibility, and more goodies. Go has accumulated a comprehensive feature set that few languages can match. (Ok, Go started with an unfair advantage. It had unique features that existing languages did not have in this combination: built-in concurrency, effortless cross-compiling, a darn fast compiler, built-in unit testing, etc. etc.)
Hoppy Birdy, Go!
Featured articles
Go Turns 15 - The Go Programming Language
Whoa. I still remember when I read about Go in a German IT magazine called c't and was immediately hooked. This was around the time when release 1.0 came out.
Looking back, it's impressive how the language (including its ecosystem) evolved without sacrificing the Go 1 backward compatibility promise. Many languages change so much over the years that you would barely consider current code and code from 5 years ago to belong to the same language. Go code written for v1.0 is about the same as code written for v1.23 (if you ignore type parameters for a moment, which are the only language addition that added new syntax).
Instead of messing with language syntax and semantics, the Go team focused on improving the toolchain, the standard library, and the ecosystem. As a result, Go's toolchain is a delight for developers. Go has even become forward-compatible through a toolchain that downloads required Go versions as required. A feature that is insanely important yet barely discussed is the perfectly reproducible, verifiable Go toolchain that is an enormous win in the battle against supply chain attacks.
I could go on and on. Bottom line, I am more than happy that I stumbled upon Go 15 years ago.
memory regions · golang/go · Discussion #70257 · GitHub
Garbage collection can diminish performance, even though Go's GC is highly optimized. Memory Arenas, a Go experiment from 2022, allowed allocating sets of memory objects that were later released in bulk, to take some pressure off of the GC. The experiment has stalled because of serious obstacles around integrating arenas with other language features.
Memory regions are an alternate approach to solving the same problem. They may blend much better into the language than arenas. An implementation of memory regions in Go would expose two functions, Do()
for creating a new memory region for a given function to be called, and Ignore()
that advises to ignore the region for an allocation that is known to outlive the context.
Currently, this is a discussion on GitHub but maybe it turns into a proposal and an experiment. Here is the full design document.
Go Runtime Finalizer and Keep Alive
If you moved from a fully object-oriented language to Go, chances are you have been missing finalizers for cleaning up resources when an object gets destroyed. Go does have a (quite obscure) way of setting finalizers—in the runtime
package.
However, as the author of this article, Phuong Le, points out, "if you find yourself needing to use finalizers, there’s a good chance it’s a sign of a design issue."
This being said, finalizers aren't entirely useless. They can serve as a fallback cleanup mechanism for objects that have an explicit cleanup function. (Think of "Close()" methods of files or database connections.) If users of such objects fail to call the cleanup function properly, the finalizer can kick in.
Podcast corner
Cup o' Go | 🎂 ¡Feliz quinceañera a Golang! 🪅
A summary of the events, proposals, and other stuff that happened last week.
Crawl, walk & run your way to usable CLIs in Go
Writing a CLI tool with Go is suuuuper easy. Unless it has to be ready for production use. Wesley Beary and Johnny Boursiquot discuss various obstacles on a way to build a non-trivial CLI tool.
Spotlight: When (not) to call package-level APIs
Libraries must not call package-level APIs of other libraries.
What do I mean by that? Let me first define "package-level API". Surely you know that some packages of the standard library provide a default object of a type, along with package-level functions.
Examples:
- The flag
package has a FlagSet
type and a package-level flag variable CommandLine
, with package-level functions that operate on this variable. Under the hood, these functions are only shallow wrappers around the methods of FlagSet
.
- The net/http
package has default Client
and ServeMux
variables and package level functions that wrap the corresponding methods.
Such package-level variables and functions build a package-level API. The main purpose of this is convenience. If you want to write a quick CLI tool or a simple server, using the package-level API saves you a few lines of code.
The catch
Of course, there is a catch. I would not have written this Spotlight if there wasn't a catch.
Package-level variables are, by definition, not thread-safe. This is not a problem if the package-level functions do not modify the state of the variable. For example, http.Get()
does not write anything to http.DefaultClient
, hence, no data race can occur. But as soon as state-changing methods are called without synchronization mechanisms, the data race game begins.
Here is a practical example: The log package provides a package-level logger. Clients can set log flags to control the prefix of every log output through log.SetFlags(…)
. This function is thread-safe. The Logger struct's flags
field is an atomic.Int32
that can be updated only through atomic operations. So, technically, the following code contains no data race, but the result is still not the desired one:
package main
import (
"log"
"time"
)
func main() {
done := make(chan struct{})
go func(done chan<- struct{}) {
for _ = range 10 {
log.SetFlags(log.Lshortfile)
time.Sleep(time.Microsecond)
log.Println("file")
}
done <- struct{}{}
}(done)
go func(done chan<- struct{}) {
for _ = range 10 {
log.SetFlags(log.Ltime)
time.Sleep(time.Microsecond)
log.Println("time")
}
done <- struct{}{}
}(done)
// wait for both goroutines
<-done
<-done
}
Output:
prog.go:18: file
prog.go:26: time
23:00:00 file
23:00:00 time
23:00:00 time
23:00:00 file
prog.go:26: time
prog.go:18: file
prog.go:18: file
prog.go:26: time
23:00:00 time
23:00:00 file
prog.go:18: file
prog.go:26: time
23:00:00 time
23:00:00 file
prog.go:18: file
prog.go:26: time
23:00:00 time
23:00:00 file
Program exited.
The prevention
Prevention is better than cure, they say, and prevention is actually pretty easy in this case.
If you write a binary, feel free to use package-level APIs but apply caution if your app uses gorutines.
If you write a library, make sure that your code only uses isolated instances of the package's types. After all, you have no control over the code of your library's clients. The Google Go Style Guide says, "Infrastructure libraries that can be imported by other packages must not rely on package-level state of the packages they import." So make your code watertight.
For example, instead of calling the package-level logger, each goroutine can create its own Logger
instance:
package main
import (
"log"
"os"
"time"
)
var yes = struct{}{}
func main() {
done := make(chan struct{})
go func(done chan<- struct{}) {
l := log.New(os.Stdout, "", 0)
for _ = range 10 {
l.SetFlags(log.Lshortfile)
time.Sleep(time.Microsecond)
l.Println("file")
}
done <- yes
}(done)
go func(done chan<- struct{}) {
l := log.New(os.Stdout, "", 0)
for _ = range 10 {
l.SetFlags(log.Ltime)
time.Sleep(time.Microsecond)
l.Println("time")
}
done <- yes
}(done)
// wait for both goroutines
<-done
<-done
}
Output:
prog.go:20: file
23:00:00 time
prog.go:20: file
23:00:00 time
23:00:00 time
prog.go:20: file
prog.go:20: file
23:00:00 time
23:00:00 time
prog.go:20: file
prog.go:20: file
23:00:00 time
23:00:00 time
prog.go:20: file
prog.go:20: file
23:00:00 time
prog.go:20: file
23:00:00 time
23:00:00 time
prog.go:20: file
So, while package-level APIs can be convenient, creating unique instances of package types is the safer bet.
Quote of the Week: Astounding Go
We began developing [a proof of concept] in Go. Upon deployment, the results were astounding—exceptional performance, significantly lower resource usage, and overall efficiency better than other languages we had in production at the time.
More articles, videos, talks
Go 1.23.3 and Go 1.22.9 are released
Go 1.23.3 includes fixes to the linker, the runtime, and the net/http, os, and syscall packages.
Go 1.22.9 includes fixes to the linker.
Creating a Brainrot Language Server in Golang | Jitesh's Blog
Doom-scrolling the internet leads to brain rot. You can recognize brain rot victims by their slang. To reach out to them, you have to translate your message into Brainrot slang. This language server helps you with that task.
Here u go besties, im ur munch fr fr 😭✨
fmt.Sprintf vs String Concat
You can have fast and elegant string concatenation. Pick one.
Use Your Golang gRPC Services Like GraphQL, Utilizing Dynamic Enrichment and Dataloader | by kazmerdome | Nov, 2024 | Medium
How to use GraphQL without using GraphQL. Or, how to implement interesting characteristics of GraphQL on top of gRPC.
ML in Go with a Python sidecar
Assume you have a local LLM running for interference jobs, but it runs in Python—how to use this model in Go? Eli Bendersky has an answer.
Projects
Libraries
GitHub - Kurt212/syncgroup: Golang goroutines synchronization primitive wihout extra dependencies
An alternative to errgroup
that returns all errors (including panics converted to errors) instead of just the first one. (See also: [sourcegraph/conc.
GitHub - obzva/bantam-go: A simple demo app for Pratt parsing. Originally written in Java, ported into Go.
Pratt parsing makes parsing certain constructs easier than a traditional recursive-descent parser. This is a Go implementation of the original Java code.
GitHub - zolstein/sync-map: A fork of Go's sync.Map, but with generics.
There are a couple of concurrency-safe maps with generics out there, but this one is a direct fork of the original sync.Map
. This approach stays as close to the original behavior as possible and avoids the overhead a wrapper would introduce.
Tools and applications
GitHub - GreedyKomodoDragon/Kontroler: A kubernetes native workflow orchestrator used to create DAGs that can run on a schedule as well as in an event driven manner
Schedule containers via YAML configuration and monitor the status in a web dashboard.
GitHub - BemiHQ/BemiDB: Postgres read replica optimized for analytics
Analytics tools love to read data in columnar format. SQL databases, however, are row-oriented. BemiDB
replicates a PostgreSQL database into a columnar format that makes analytics happy.
GitHub - Snawoot/go-win7: Golang backport which supports Windows 7
The much, much better option, of course, is to drop Windows 7 immediately. But I get that companies may be stuck on old, insecure OS versions due to legacy software and questionable C-level decisions.
GitHub - 0jk6/freight: simple code execution engine on top of k8s
Using Kubernetes to implement a REPL is probably overkill, but if you have k8s already around, freight
adds a code execution engine with very few ingredients. The author explains the concept in this video.
visualmd command - modernc.org/visualmd - Go Packages
Built with tk9, this Markdown editor provides markdown snippets by button click and an extensive configuration to match any Markdown dialect and fine-tune behavior like adding a blank target to links. By modernc.org a.k.a 0xjnml a.k.a. Jan Mercl.
GitHub - michurin/human-readable-json-logging: The tool to pretty print JSON log stream right from running process in human readable format
Turn JSON log gibberish into readable, colorized text.
GitHub - watzon/postpilot: Local SMTP testing made easy
Test your email server with a local app (that is, unlike Mailpit, PostPilot is designed as a desktop app (using Wails for the GUI)). Here is the introductory blog post.
Completely unrelated to Go
What I Wish Someone Told Me About Postgres
A truckload of tips for working with PostgreSQL databases.
Choose boring technology and LLMs
If you manage to build your daily work around "boring technology" (like Go and SQLite), you have more brain cells left for learning exciting new tech like the ins and outs of LLMs. (With a short but valuable list of resources for learning more about LLMs.)
Errata
Shortly after the previous issue got sent out, the article "A Golang Pipeline Abomination" vanished from the web—and the entire site with it. As of this writing, the site hasn't come back, so it seems the article is gone forever. (I checked archive.org to no avail.) The web is a lossy place.
Happy coding! ʕ◔ϖ◔ʔ
Questions or feedback? Drop me a line. I'd love to hear from you.
Best from Munich, Christoph
Not a subscriber yet?
If you read this newsletter issue online, or if someone forwarded the newsletter to you, subscribe for regular updates to get every new issue earlier than the online version, and more reliable than an occasional forwarding.
Find the subscription form at the end of this page.
How I can help
If you're looking for more useful content around Go, here are some ways I can help you become a better Gopher (or a Gopher at all):
On AppliedGo.net, I blog about Go projects, algorithms and data structures in Go, and other fun stuff.
Or visit the AppliedGo.com blog and learn about language specifics, Go updates, and programming-related stuff.
My AppliedGo YouTube channel hosts quick tip and crash course videos that help you get more productive and creative with Go.
Enroll in my Go course for developers that stands out for its intense use of animated graphics for explaining abstract concepts in an intuitive way. Numerous short and concise lectures allow you to schedule your learning flow as you like.
Christoph Berger IT Products and Services
Dachauer Straße 29
Bergkirchen
Germany