Go Beyond the Basics: Clever Tricks in Go

Go Beyond the Basics: Clever Tricks in Go

TB

Teqani Blogs

Writer at Teqani

September 19, 20255 min read
Go is known for its simplicity and readability, but it also harbors powerful tricks that can significantly improve your code's efficiency and clarity. This article explores some of these advanced techniques that every Go developer should be aware of, enhancing both performance and maintainability.

Reusing Arguments in fmt.Sprintf

The fmt.Sprintf function in Go is commonly used for formatting strings with a specified format and arguments. It returns the formatted string rather than printing it directly. Did you know that fmt.Sprintf can reuse arguments by index? This means you can reference the same argument multiple times without repeating it.
message := fmt.Sprintf("1st[%[1]s] 2nd[%[2]s] again[%[1]s]", "one", "two")
fmt.Println(message)
// Output: 1st[one] 2nd[two] again[one]

Printing Struct Field Names with %+v

When debugging Go programs, it's often necessary to inspect the values of struct fields. However, the default output only shows the values without their corresponding field names. By using the %+v format specifier in fmt.Printf, you can easily display both the field names and their values, making debugging much easier.
type Person struct {
    Name string
    Age  int
}

p := Person{"Alice", 30}
fmt.Printf("%+v\n", p)
// Output: {Name:Alice Age:30}

Width & Alignment in fmt.Printf

While fmt.Printf is great, you might feel you have less control over formatting of logs in terminal, however fmt allows width & alignment to your preference so that debugging becomes easier in crowded terminal logs.
fmt.Printf("|%-10s|%10s|\n", "left", "right")
// Output: |left      |     right|

Using copy for Slices

The built-in copy function is often underused but incredibly handy with simple syntax. It can be used for exact copies, copies to smaller destinations, or even shifting elements with a guarantee of safe behavior.
src := []int{1, 2, 3}
dst := make([]int, len(src))
n := copy(dst, src)
fmt.Println(dst, n)
// Output: [1 2 3] 3

dst2 := make([]int, 2) // smaller
n = copy(dst2, src)
fmt.Println(dst, n)
// Output: [1 2 3] 2

nums := []int{1, 2, 3, 4, 5}
copy(nums[1:], nums) // shift right by one
fmt.Println(nums)
// Output: [1 1 2 3 4]

Zero-Allocation String/Byte Conversion

Converting strings to byte slices can be costly due to memory allocation. Using the unsafe package, you can create a []byte that directly references the string’s memory, avoiding new allocation. However, be cautious, as modifying the resulting byte slice can corrupt the original string.
import "unsafe"

s := "hello"
b := *(*[]byte)(unsafe.Pointer(&s))
fmt.Println(b) // [104 101 108 108 111]

Slicing with Capacity Control

You can control the capacity of slices using the three-index slicing syntax. This allows you to limit the slice's capacity, preventing accidental modifications to the underlying array beyond a certain point. Slicing with capacity control is an essential trick in Go.
arr := []int{1, 2, 3, 4, 5}
s := arr[1:3:3]
fmt.Println(s, len(s), cap(s))
// Output: [2 3] 2 2

Using Maps as Sets

Go doesn't have a built-in set data structure, but you can simulate set behavior using maps. The convention is to use map[T]struct{}, where T is the type of the set elements. This is efficient for deduplication, membership testing, and fast lookups.
set := map[string]struct{}{}
set["go"] = struct{}{}
set["lang"] = struct{}{}

if _, exists := set["go"]; exists {
    fmt.Println("found go")
}
// Output: found go

Labelled Loops in Go

Labels allow breaking out of or continuing an outer loop from inside a nested loop.
outer:
for i := 1; i <= 3; i++ {
    for j := 1; j <= 3; j++ {
        fmt.Println("i:", i, "j:", j)
        if j == 2 {
            break outer // exit BOTH loops
        }
    }
}

Capturing Loop Variables in Go Routines

When launching Go routines within loops, ensure that loop variables are captured correctly to prevent data races. Pass loop variables explicitly as arguments to the Go routine's function.
for i := 0; i < 3; i++ {
    go func(x int) {
        fmt.Println(x)
    }(i)
}

Using sync.WaitGroup for Synchronization

sync.WaitGroup provides a counter to wait for multiple Go routines to complete before continuing. Use wg.Add(1) to increment the counter, defer wg.Done() to decrement the counter when a Go routine finishes, and wg.Wait() to block until the counter is zero.
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
    wg.Add(1) // increase counter
    go func(id int) {
        defer wg.Done() // decrease counter when finished
        fmt.Println("Worker", id, "done")
    }(i)
}
wg.Wait() // block until counter = 0

Timeouts with context.WithTimeout

Implement timeouts using context.WithTimeout to automatically cancel long-running Go routines after a specified duration. This prevents resources from being held indefinitely.
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel() // always cancel to release resources

select {
case res := <-ch:
    fmt.Println(res)
case <-ctx.Done():
    fmt.Println("Timeout exceeded:", ctx.Err())
}

Use init() for Setup

Go executes the init function before the main function, making it useful for initialization and setup code.
func init() {
    fmt.Println("This runs first!")
}
TB

Teqani Blogs

Verified
Writer at Teqani

Senior Software Engineer with 10 years of experience

September 19, 2025
Teqani Certified

All blogs are certified by our company and reviewed by our specialists
Issue Number: #f7bc19af-c077-4e61-ae12-886265c64ca4