Useful and Versatile Go Code Snippets

Useful and Versatile Go Code Snippets

During the development of production projects, I often find myself repeating code and unconsciously using certain techniques until I realize them later during a retrospective.

To address this issue, I developed a solution that has been very helpful for me, and I believe it might be useful for others as well.

Here are some useful and versatile code snippets randomly selected from my utility library, without specific categorization or system-specific techniques.

# 1. Timing Execution

If you want to track the execution time of a function in Go, there’s a simple and efficient technique with just one line of code, using the defer keyword. You only need a TrackTime function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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)
}

// Output:
// elapsed: 501.11125ms

# 1.5. Two-Stage Deferred Execution

Go’s defer is not only for cleanup tasks but can also be used for preparation tasks. Consider the following example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

The beauty of this pattern is that with just one line of code, you can accomplish tasks like:

  • Opening a database connection and then closing it.
  • Setting up a mock environment and then tearing it down.
  • Acquiring a distributed lock and then releasing it.

“Well, that seems clever, but how practical is it in real-world scenarios?”

Remember the timing execution technique? We can do something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
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)
}

Note! What if there’s an error when I connect to the database?

Indeed, patterns like defer TrackTime() or defer ConnectDB() might not handle errors properly. This technique is most suitable for testing or when you are willing to take the risk of fatal errors, as shown in the following test-oriented approach:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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)
}
}

This way, database connection errors can be handled during testing.

# 2. Pre-allocate Slices

According to insights from the article “Performance Improvements in Go,” pre-allocating slices or maps can significantly improve the performance of a Go program. However, it’s worth noting that if we are not careful and use append instead of indexing (like a[i] ), this approach may sometimes lead to errors. Did you know that we can use a pre-allocated slice without specifying the array length (zero) as explained in the mentioned article? This allows us to use the pre-allocated slice just like using append :

1
2
3
4
5
6
7
// Instead of
a := make([]int, 10)
a[0] = 1

// Use it like this
b := make([]int, 0, 10)
b = append(b, 1)

# 3. Method Chaining

Method chaining technique can be applied to function (pointer) receivers. To illustrate this, let’s consider a Person struct with two functions AddAge and Rename for modifications:

1
2
3
4
5
6
7
8
9
10
11
12
type Person struct {
Name string
Age int
}

func (p *Person) AddAge() {
p.Age++
}

func (p *Person) Rename(name string) {
p.Name = name
}

If you want to add age to a person and then rename them, the conventional way is:

1
2
3
4
5
6
func main() {
p := Person{Name: "Aiden", Age: 30}

p.AddAge()
p.Rename("Aiden 2")
}

Alternatively, we can modify the AddAge and Rename functions’ receivers to return the modified object itself, even though they typically don’t return anything:

1
2
3
4
5
6
7
8
9
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 adding unnecessary lines of code:

1
p = p.AddAge().Rename("Aiden 2")

# 4. Go 1.20 Allows Slicing into an Array or Array Pointer

When we need to convert a slice to a fixed-size array, direct assignment is not possible, for example:

1
2
3
4
a := []int{0, 1, 2, 3, 4, 5}
var b [3]int = a[0:3]

// Cannot assign a[0:3] (type []int) to a variable of type [3]int (incompatible assignment)

With the introduction of this feature in Go 1.17, and with the release of Go 1.20, the conversion process becomes simpler with more convenient literals:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 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 reminder: you can use a[:3] instead of a[0:3] . I mention this for clarity.

# 5. Package Initialization with “import _”

Sometimes, in a library, you might come across import statements combining an underscore (_) like this:

1
2
3
import (
_ "google.golang.org/genproto/googleapis/api/annotations"
)

This executes the package’s initialization code (init function) without needing to create a named reference to it. This allows you to initialize packages, register connections, and perform other tasks before running the code.

Let’s better understand how it works with an example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Underscore
package underscore

func init() {
fmt.Println("init called from underscore package")
}
// Main
package main

import (
"fmt"
_ "lab/underscore"
)

func main() {}
// Output: init called from underscore package

# 6. Importing with “import .”

After understanding how to import with an underscore, let’s see how more commonly used is the dot (.) operator.

As a developer, the dot (.) operator can be used to import exported identifiers from an imported package without specifying the package name. This is a useful shortcut for lazy developers, especially when dealing with long package names in a project, like externalmodel or doingsomethinglonglib .

To demonstrate, here’s a simple example:

1
2
3
4
5
6
7
8
9
10
11
package main

import (
"fmt"
. "math"
)

func main() {
fmt.Println(Pi) // 3.141592653589793
fmt.Println(Sin(Pi/2)) // 1
}

# 7. Go 1.20 Allows Merging Multiple Errors into a Single Error

Go 1.20 introduces new features to the errors package, including support for multiple errors and changes to errors.Is and errors.As . One of the new functions added in errors is Join , which we’ll discuss in detail below:

1
2
3
4
5
6
7
8
9
10
11
12
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 there are multiple tasks causing errors, you can use the Join function instead of manually managing arrays. This simplifies the error handling process.

# 8. Checking if an Interface Is Truly Nil

Even if an interface holds a value of nil , it doesn’t mean the interface itself is nil . This can lead to unexpected errors in a Go program. Therefore, it’s important to know how to check if an interface is truly nil .

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
var x interface{}
var y *int = nil
x = y

if x != nil {
fmt.Println("x != nil") // <-- Actual output
} else {
fmt.Println("x == nil")
}

fmt.Println(x)
}

// Output:
// x != nil
// <nil>

How do we determine if the interface{} value is nil ? Fortunately, there’s a simple tool to help us with that:

1
2
3
4
5
6
7
func IsNil(x interface{}) bool {
if x == nil {
return true
}

return reflect.ValueOf(x).IsNil()
}

# 9. Parsing time.Duration in JSON

When parsing JSON, using time.Duration can be a cumbersome process because it requires adding 9 zeros (i.e., 1000000000) after the second. To simplify this process, I created a new type called Duration :

1
type Duration time.Duration

To parse strings (like “1s” or “20h5m”) into an int64 type duration, I also implemented custom parsing logic for this new type:

1
2
3
4
5
6
7
8
9
10
11
12
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’s worth noting that the variable ‘d’ should not be nil, or it might result in marshaling errors. Alternatively, you can also check ‘d’ at the beginning of the function.

author

Beck moulton

Published on

2024-05-01

Updated on

2024-05-31

license agreement