12 Personal Go Tricks That Transformed My Productivity
Phuong Le
Posted on October 3, 2023
Author
I typically share insights on System Design & Go at Devtrovert. Feel free to check out my LinkedIn Phuong Le for the latest posts.
While working on production projects, I noticed that I was frequently duplicating code and utilizing certain techniques without realizing it until later when reviewing my work.
To address this issue, I developed a solution that has proven to be quite helpful for me, and I thought it might be useful for others as well.
Below are some useful and versatile code snippets randomly picked from my utilities library, without any particular categorization or system-specific tricks.
1. Time elapsed trick
If you’re interested in tracking the execution time of a function in Go, there’s a simple and efficient trick you can use with just a single line of code using the “defer” keyword. All you need is a TrackTime function:
// Utility
func TrackTime(pre time.Time) time.Duration {
elapsed := time.Since(pre)
fmt.Println("elapsed:", elapsed)
return elapsed
}
func TestTrackTime(t *testing.T) {
defer TrackTime(time.Now()) // <--- THIS
time.Sleep(500 * time.Millisecond)
}
// elapsed: 501.11125ms
1.5. Two-Stage Defer
The power of Go’s defer isn't just about cleaning up after a task; it's also about preparing for one, consider the following:
func setupTeardown() func() {
fmt.Println("Run initialization")
return func() {
fmt.Println("Run cleanup")
}
}
func main() {
defer setupTeardown()() // <--------
fmt.Println("Main function called")
}
// Output:
// Run initialization
// Main function called
// Run cleanup
Thanks to Teiva Harsanyi for introducing this elegant approach.
The beauty of this pattern? In just one line, you can achieve tasks such as:
- Opening a database connection and later closing it.
- Setting up a mock environment and tearing it down.
- Acquiring and later releasing a distributed lock.
- ...
“Alright, this seems clever, but where’s the real-world utility?”
Remember the time-elapsed trick? We can do that too:
func TrackTime() func() {
pre := time.Now()
return func() {
elapsed := time.Since(pre)
fmt.Println("elapsed:", elapsed)
}
}
func main() {
defer TrackTime()()
time.Sleep(500 * time.Millisecond)
}
“But wait! What if I’m connecting to a database and it throws an error?”
True, patterns like defer TrackTime()()
or defer ConnectDB()()
won't handle errors gracefully. This trick is best for tests or when you're audacious enough to risk a fatal error, check this test-centric approach:
func TestSomething(t *testing.T) {
defer handleDBConnection(t)()
// ...
}
func handleDBConnection(t *testing.T) func() {
conn, err := connectDB()
if err != nil {
t.Fatal(err)
}
return func() {
fmt.Println("Closing connection", conn)
}
}
There, errors from database connections will be handled during testing.
2. Slice pre-allocation
As per the insights shared in the article “Go Performance Boosters”, pre-allocating a slice or map can significantly enhance the performance of our Go programs.
However, it’s worth noting that this approach can sometimes result in bugs if we inadvertently use “append” instead of indexing, like a[i].
Did you know that it’s possible to use a pre-allocated slice without specifying the length of the array (zero), as explained in the aforementioned article? This allows us to use append just like we would:
// instead of
a := make([]int, 10)
a[0] = 1
// use this
b := make([]int, 0, 10)
b = append(b, 1)
3. Chaining
The technique of chaining can be applied to function (pointer) receivers. To illustrate this, let’s consider a Person
struct with two functions, AddAge
and Rename
, that can be used to modify it.
type Person struct {
Name string
Age int
}
func (p *Person) AddAge() {
p.Age++
}
func (p *Person) Rename(name string) {
p.Name = name
}
If you’re looking to add age to a person and then rename them, the typical approach is as follows:
func main() {
p := Person{Name: "Aiden", Age: 30}
p.AddAge()
p.Rename("Aiden 2")
}
Alternatively, we can modify the AddAge
and Rename
function receivers to return the modified object itself, even if they don’t typically return anything.
func (p *Person) AddAge() *Person {
p.Age++
return p
}
func (p *Person) Rename(name string) *Person {
p.Name = name
return p
}
By returning the modified object itself, we can easily chain multiple function receivers together without having to add unnecessary lines of code:
p = p.AddAge().Rename("Aiden 2")
4. Go 1.20 enables parsing of slices into arrays or array pointers
When we need to convert a slice into a fixed-size array, we can’t assign it directly like this:
a := []int{0, 1, 2, 3, 4, 5}
var b[3]int = a[0:3]
// cannot use a[0:3] (value of type []int) as [3]int value in variable
// declaration compiler(IncompatibleAssign)
In order to convert a slice into an array, the Go team updated this feature in Go 1.17. And with the release of Go 1.20, the conversion process has become even easier with more convenient literals:
// go 1.20
func Test(t *testing.T) {
a := []int{0, 1, 2, 3, 4, 5}
b := [3]int(a[0:3])
fmt.Println(b) // [0 1 2]
}
// go 1.17
func TestM2e(t *testing.T) {
a := []int{0, 1, 2, 3, 4, 5}
b := *(*[3]int)(a[0:3])
fmt.Println(b) // [0 1 2]
}
Just a quick note: you can use a[:3] instead of a[0:3]. I’m mentioning this for the sake of clarity.
5. Using import with ‘_’ for package initialization
Sometimes, in libraries, you may come across import statements that combine an underscore (_
) like this:
import (
_ "google.golang.org/genproto/googleapis/api/annotations"
)
This will execute the initialization code (init function) of the package, without creating a name reference for it. This allows you to initialize packages, register connections, and perform other tasks before running the code.
Let’s consider an example to better understand how it works:
// underscore
package underscore
func init() {
fmt.Println("init called from underscore package")
}
// mainpackage main
import (
_ "lab/underscore"
)
func main() {}
// log: init called from underscore package
6. Use import with dot .
Having explored how we can use import with underscore, let’s now look at how the dot .
operator is more commonly used.
As a developer, the dot .
operator can be used to make the exported identifiers of an imported package available without having to specify the package name, which can be a helpful shortcut for lazy developers.
Pretty cool, right? This is especially useful when dealing with long package names in our projects, such as ‘externalmodel
’ or ‘doingsomethinglonglib
’
To demonstrate, here’s a brief example:
package main
import (
"fmt"
. "math"
)
func main() {
fmt.Println(Pi) // 3.141592653589793
fmt.Println(Sin(Pi / 2)) // 1
}
7. Multiple errors can now be wrapped into a single error with Go 1.20
Go 1.20 introduces new features to the error package, including support for multiple errors and changes to errors.Is
and errors.As
.
One new function added to errors is Join, which we’ll take a closer look at below:
var (
err1 = errors.New("Error 1st")
err2 = errors.New("Error 2nd")
)
func main() {
err := err1
err = errors.Join(err, err2)
fmt.Println(errors.Is(err, err1)) // true
fmt.Println(errors.Is(err, err2)) // true
}
If you have multiple tasks that contribute errors to a container, you can use the Join
function instead of manually managing the array yourself. This simplifies the error handling process.
8. Trick to Check Interface at Compile Time
Suppose you have an interface called Buffer
that contains a Write()
function. Additionally, you have a struct named StringBuffer
which implements this interface.
However, what if you make a typo mistake and write Writeee()
instead of Write()
?
type Buffer interface {
Write(p []byte) (n int, err error)
}
type StringBuffer struct{}
func (s *StringBuffer) Writeee(p []byte) (n int, err error) {
return 0, nil
}
You are unable to check whether StringBuffer has properly implemented the Buffer interface until runtime. However, by using this trick, the compiler will alert you via an IDE error message:
var _ Buffer = (*StringBuffer)(nil)
// cannot use (*StringBuffer)(nil) (value of type *StringBuffer)
// as Buffer value in variable declaration: *StringBuffer
// does not implement Buffer (missing method Write)
9. Ternary with generic (Should be avoided)
Go does not have built-in support for ternary operators like many other programming languages:
# python
min = a if a < b else b
// c#
min = x < y ? x : y
With Go’s generics in version 1.18, we now have the ability to create a utility that allows for ternary-like functionality in just a single line of code:
// our utility
func Ter[T any](cond bool, a, b T) T {
if cond {
return a
}
return b
}
func main() {
fmt.Println(Ter(true, 1, 2)) // 1
fmt.Println(Ter(false, 1, 2)) // 2
}
10. Avoid Naked Parameter
When dealing with a function that has multiple arguments, it can be confusing to understand the meaning of each parameter by just reading its usage. Consider the following example:
printInfo("foo", true, true)
What do the first ‘true’ and the second ‘true’ mean if you don’t inspect the printInfo? When you have a function with multiple arguments, it can be confusing to understand the parameter meaning.
However, we can use comments to make the code more readable. For example:
// func printInfo(name string, isLocal, done bool)
printInfo("foo", true /* isLocal */, true /* done */)
Some IDEs also support this feature by showing comments in function call suggestions, but it may need to be enabled in settings.
11. Ways to verify if an interface is truly nil
Even if an interface holds a value of nil, it doesn’t necessarily mean that the interface itself is nil. This can lead to unexpected errors in Go programs. So, it’s important to know how to check if an interface is actually nil or not.
func main() {
var x interface{}
var y *int = nil
x = y
if x != nil {
fmt.Println("x != nil") // <-- actual
} else {
fmt.Println("x == nil")
}
fmt.Println(x)
}
// x != nil
// <nil>
If you are not familiar with this concept, I recommend that you refer to my article about Go’s secrets regarding Interface{}: Nil is not Nil.
How can we determine whether an interface{} value is nil? Fortunately, there is a simple utility that can help us achieve this:
func IsNil(x interface{}) bool {
if x == nil {
return true
}
return reflect.ValueOf(x).IsNil()
}
12. Unmarshal time.Duration in JSON
When parsing JSON, using time.Duration
can be a cumbersome process as it requires adding 9 zeroes trailing of 1 second (i.e., 1000000000). To simplify this process, I created a new type called Duration
:
type Duration time.Duration
To enable parsing of strings like 1s
or 20h5m
into int64
durations, I also implemented a custom unmarshal logic for this new type:
func (d *Duration) UnmarshalJSON(b []byte) error {
var s string
if err := json.Unmarshal(b, &s); err != nil {
return err
}
dur, err := time.ParseDuration(s)
if err != nil {
return err
}
*d = Duration(dur)
return nil
}
However, it is important to note that the variable ‘d
’ should not be nil as it may lead to marshaling errors. Alternatively, you can also include a check for ‘d’ at the beginning of the function.”
I didn’t want to make the post too long and difficult to follow since these tricks don’t depend on any specific topic and cover various categories.
If you found these tricks useful or have any insights of your own to share, please feel free to leave a comment. I value your feedback and would be happy to like or recommend your ideas in response to this post.
Happy tricking!
Posted on October 3, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.