DEV Community

Cover image for 12 Personal Go Tricks That Transformed My Productivity
Phuong Le
Phuong Le

Posted on • Edited on • Originally published at blog.devtrovert.com

12 Personal Go Tricks That Transformed My Productivity

12 Personal Go Tricks That Transformed My Productivity

Photo by Lucas Santos on Unsplash

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

“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)
  }
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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")
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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]
}
Enter fullscreen mode Exit fullscreen mode

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" 
)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
// c#
min = x < y ? x : y
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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 */)
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (12)

Collapse
 
teivah profile image
Teiva Harsanyi

Thanks for your post.

About 1., a slight variation that I even prefer if you don't need to return time.Duration: go.dev/play/p/D0qkU5zB1Co

Collapse
 
func25 profile image
Phuong Le

Hmm... that's a good one but just thinking, the "()()" might trip some folks up if they miss the second "()". Still, it's an excellent trick that I'd never thought of.

Collapse
 
teivah profile image
Teiva Harsanyi

You're right, that's something to consider before submitting such a code.

But yeah as it was a "tricks" post I thought that might be interesting :)

I also use it when in a function, I need to do a pre and post action: go.dev/play/p/J31oyRhJzQ-
It's pretty handy during testing, for example.

Thread Thread
 
func25 profile image
Phuong Le • Edited

Oh shoot, I see the benefit of this Teiva, it's really great for handling actions both before and after a function, could I possibly include this tip in the post?

By the way, do you think this trick can be safely applied in production?

Thread Thread
 
teivah profile image
Teiva Harsanyi

could I possibly include this tip in the post?

Sure, please

do you think this trick can be safely applied in production

Yes, there's nothing unsafe. The only caveat is that Go devs might be slightly confused at first when reading the code.

Collapse
 
soulsbane profile image
Paul Crane

Great post!

Collapse
 
mikeschinkel profile image
Mike Schinkel

When have you found a need to use arrays vs. slices?

I have been coding in Go for half a decade now and still have yet to come across a single time I felt I needed one. I am sure they exist, but I do not yet know of an example. Can you elaborate?

Also, for naked parameter avoidance, I far prefer to great an "args" type, e.g:

printInfo("foo", InfoArgs{
   IsLocal: true,
   Done: true,
})
Enter fullscreen mode Exit fullscreen mode

The beauty of the above is that if you need to add a 3rd parameter — or a 4th, 5th, etc. — it does not break your function signature. And in some IDEs, it autocompletes better.

Collapse
 
func25 profile image
Phuong Le

Arrays are more suitable for fixed sizes when the size is already known, like hashing (where the SHA-256 algorithm operates on 512-bit blocks) or ID:

var hash [32]byte 

type ObjectID [12]byte // MongoDB
Enter fullscreen mode Exit fullscreen mode

Using a slice for these types is possible, no problem. But it lacks clarity and readability, expressing that explicitly is preferable.

About the naked trick, like in your example, I typically use structs for functions > 3 arguments. Yet, IMO, break a function's signature is better, it serves as a compile-time noti for us, unless the added argument is optional.

Collapse
 
mikeschinkel profile image
Mike Schinkel • Edited

Hi Phuong,

Thank you for the answer.

Yes, that makes perfect sense regarding arrays; not sure why that did not occur to me before.

As for using structs, I should have explicitly stated that I meant them only for optional parameters. I guess I have gotten so accustomed to using them like that over the years I forget to point out that detail. And in hindsight, I guess the your commenting is still applicable for required parameters.

Lastly, and I will admit that I probably care more about this than other developers given my experience in team projects, but I actively hate it when code requires function signatures to be changed over time. Changing signatures can be disruptive in PRs and for other developers, and they can cause untold heartache when 3rd parties used packages which contain signatures you've broken.

I guess there is one caveat and that being I guess it is okay if only one developer is going to be working on the code at any one time and nobody else is going to depend on it, although I still think it is a good idea to keep in practice even on solo projects.

I do appreciate that some languages do not do a good job of making this practice practical, but whenever possible I would argue it is a best practice to code your functions with your best intention that they would never need to change their signature. BTW, I will point out that Go does a poor job of enabling this best practice given how context.Context and error are idiomatically used.

If a function has even just one optional parameter, unless by its very nature it could never need an additional optional parameter then IMO it is best to implement those parameters with a signature that won't break, be it an args struct, or (IMO, less preferable) a variadic of set funcs.

And if you need to add a new required parameter, I argue you should add an additional function with a new name to include that new parameter and leave the old function around for backward compatibility, if at all possible.

#fwiw

-Mike

P.S. One more thing. I recently came across your blog posts and subscribed. I commend you for your excellent articles, which IMO are a cut well above that which most developers are writing about programming topics. Kudos, and keep up the great work!

Thread Thread
 
func25 profile image
Phuong Le

One thing I'd like to point out is that, you can use a single struct as an argument for a function with the "required" tag from the validator/v10 package. This approach is kinda handy for functions with numerous arguments.

Your hashtag #fwiw (for what it's worth) sums it up well. We all bring our unique experiences and biases to the table, and I appreciate you sharing yours.

Regarding my blog, thank you, Mike. I'm truly honored by your kind words. Feedback like yours motivates me to continue writing and sharing my insights with the community.

Thread Thread
 
mikeschinkel profile image
Mike Schinkel • Edited

Interesting idea about the "required" tag. I can see where that would be useful for ensuring that types have required values, although I do like to minimize dependencies so unless I felt the need to use a lot of its functionality I would likely still shy away.

OTOH, it would not be hard to implement a required tag of my own and not require a dependency, so thanks; that is something for me to consider.

As for args, I have used the strategy of positional required args and optional args as structs for over well more than a decade and across numerous languages and have found it to be easy to be consistent and have never found it to fail me, so I doubt I personally would want to change to using all parameters in structs.

I do wish that Go and other languages had more bespoke functionality for dealing with required and optional parameters such as an automatic struct optional args, as well as multiple stacks for things like context.Context and error that might not be added initially but that could be discovered to be needed later. However, we do have to live in the world we've got and not in the one we only envision.

But we as both agreed on these topics, #fwiw. 🙂

Collapse
 
jan_semmelink_98d129f0c34 profile image
Jan Semmelink

Nice. Knew some of those, some are new to me and really useful.