DEV Community

Cover image for 🦫10 Go Tricks Every Go Programmer Needs in Their Toolkit!
Bruno Ciccarino λ for learn go

Posted on

🦫10 Go Tricks Every Go Programmer Needs in Their Toolkit!

Go is like that friend who seems super chill but has surprising talents up their sleeve. It looks simple on the surface, but there’s so much to unlock. So, let’s dive into ten Go tricks every dev should know—and I’ll throw in some examples to make it extra clear!

1. Master the Power of Goroutines

Goroutines are like little worker bees that make multitasking a breeze. You kick them off with go and let them buzz around doing their thing. Just don’t forget to manage them! Enter sync.WaitGroup, which helps you keep track of when all your goroutines have finished.

package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done() // Signals this worker is done
    fmt.Printf("Worker %d starting\n", id)
    time.Sleep(time.Second) // Simulate some work
    fmt.Printf("Worker %d done\n", id)
}

func main() {
    var wg sync.WaitGroup
    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go worker(i, &wg)
    }
    wg.Wait() // Waits for all goroutines to finish
    fmt.Println("All workers done!")
}
Enter fullscreen mode Exit fullscreen mode

Run this, and you’ll see each “worker” doing its thing, then a nice “All workers done!” at the end. Sweet!

2. Channels: Go’s Secret Weapon

Channels are Go’s way of saying, “Pass the message!” With chan, you can send and receive data between goroutines. If you’ve got lots of chatter going on, use select to listen to multiple channels at once. It’s like a switchboard operator for goroutines.

package main

import "fmt"

func main() {
    ch := make(chan string)

    go func() {
        ch <- "Hello from goroutine!"
    }()

    message := <-ch
    fmt.Println(message) // Output: Hello from goroutine!
}
Enter fullscreen mode Exit fullscreen mode

Run it, and you’ll see the message from our goroutine pop out in the main function. Boom, instant communication!

3. Learn defer for Elegant Code Cleanup

The defer keyword is like saying, “Save this for last!” It’s perfect for cleanup—like closing files, releasing locks, etc. Your code stays neat, and you’re less likely to forget cleanup steps.

package main

import (
    "fmt"
    "os"
)

func main() {
    file, err := os.Create("example.txt")
    if err != nil {
        panic(err)
    }
    defer file.Close() // This will always run last

    fmt.Fprintln(file, "Hello, defer!") // Write to the file
}
Enter fullscreen mode Exit fullscreen mode

With defer, the file gets closed no matter what, even if there’s a panic. It’s like having a built-in safety net!

4. Error Handling Like a Pro

Go doesn’t do fancy exceptions; it does errors in a straightforward way. Check for errors every step of the way, and when you need custom messages, create your own error types!

package main

import (
    "errors"
    "fmt"
)

func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("cannot divide by zero")
    }
    return a / b, nil
}

func main() {
    result, err := divide(4, 0)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println("Result:", result)
}
Enter fullscreen mode Exit fullscreen mode

If you try dividing by zero here, you’ll get a nice custom error message. Way cleaner than mysterious crashes!

5. Interfaces: More Than Just Abstractions

In Go, interfaces are about behavior, not inheritance. They’re all about “if it looks like a duck and quacks like a duck, it’s a duck!” So keep them simple and focused.

package main

import "fmt"

type Speaker interface {
    Speak() string
}

type Dog struct{}
type Cat struct{}

func (d Dog) Speak() string { return "Woof!" }
func (c Cat) Speak() string { return "Meow!" }

func makeSound(s Speaker) {
    fmt.Println(s.Speak())
}

func main() {
    makeSound(Dog{}) // Output: Woof!
    makeSound(Cat{}) // Output: Meow!
}
Enter fullscreen mode Exit fullscreen mode

Now, any struct that has a Speak method can work with makeSound. Less code, more flexibility!

6. Optimize with Struct Tags

Struct tags are like little sticky notes on your struct fields. They tell external systems how to use your fields, whether you’re dealing with JSON, XML, or databases. Add json:"-" to ignore fields or omitempty to skip empty fields.

package main

import (
    "encoding/json"
    "fmt"
)

type Person struct {
    Name  string `json:"name"`
    Age   int    `json:"age,omitempty"` // Will skip if zero
    Email string `json:"-"`
}

func main() {
    p := Person{Name: "John", Age: 0, Email: "john@example.com"}
    data, _ := json.Marshal(p)
    fmt.Println(string(data)) // Output: {"name":"John"}
}
Enter fullscreen mode Exit fullscreen mode

Here, Email is ignored, and Age isn’t shown because it’s zero. Perfect for cleaner, lighter JSON!

7. Benchmarking for Lightning Performance

Want to know how fast your code really is? Go’s testing package has built-in benchmarking. You can find bottlenecks and optimize before things go live.

package main

import (
    "testing"
)

func add(a, b int) int {
    return a + b
}

func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {
        add(1, 2)
    }
}
Enter fullscreen mode Exit fullscreen mode

Run go test -bench . and get instant feedback on performance. Even small changes can make a big difference in speed!

8. Leverage Slices for Dynamic Arrays

Slices are Go’s dynamic arrays, but with a little extra flexibility. You can expand them with append, and preallocate extra space to keep things efficient.

package main

import "fmt"

func main() {
    numbers := make([]int, 0, 5) // Capacity of 5
    numbers = append(numbers, 1, 2, 3)
    fmt.Println(numbers)      // Output: [1 2 3]
    fmt.Println(cap(numbers)) // Output: 5
}
Enter fullscreen mode Exit fullscreen mode

Slices grow as you add elements, but preallocating capacity (like here with 5) gives you a little speed boost.

9. Map Like a Boss

Maps are fast and easy, but they’re not thread-safe. So if you’re using them with goroutines, wrap them in a sync.RWMutex to avoid panics and weird bugs.

package main

import (
    "fmt"
    "sync"
)

func main() {
    m := make(map[string]int)
    var mu sync.RWMutex

    // Write
    mu.Lock()
    m["key"] = 42
    mu.Unlock()

    // Read
    mu.RLock()
    fmt.Println(m["key"]) // Output: 42
    mu.RUnlock()
}
Enter fullscreen mode Exit fullscreen mode

With sync.RWMutex, you can safely read and write to your map in a concurrent environment. Smooth and safe!

10. Sorting Like a Pro with Go’s Built-In Sort Package

In Go, ordering slices is super easy. Need to sort integers, strings or any other basic slice? The sort standard library has ready-made functions for this! But the best part is the flexibility of ordering slices of custom structs using sort.Slice. You can sort ascending, descending, or with any criteria you want!

Example: Ordering Strings, Numbers and Custom Structs
Let's start with a simple example: sorting integers and strings.

package main

import (
    "fmt"
    "sort"
)

func main() {
    numbers := []int{5, 2, 7, 3, 9}
    sort.Ints(numbers)
    fmt.Println("Sorted Numbers:", numbers) // Output: [2 3 5 7 9]

    words := []string{"banana", "apple", "cherry"}
    sort.Strings(words)
    fmt.Println("Sorted Words:", words) // Output: [apple banana cherry]
}
Enter fullscreen mode Exit fullscreen mode

Did you see that? The sort.Ints and sort.Strings functions sort everything quickly.

Sorting Structs: Example of Custom Sorting with sort.Slice

For something more advanced, like sorting a list of structs, the sort.Slice function allows you to define the sorting criteria. Here is an example with a list of people, ordered by age.

package main

import (
    "fmt"
    "sort"
)

type Person struct {
    Name string
    Age  int
}

func main() {
    people := []Person{
        {"Alice", 30},
        {"Bob", 25},
        {"Charlie", 35},
    }

    // Sort by age
    sort.Slice(people, func(i, j int) bool {
        return people[i].Age < people[j].Age // increasing age
    })

    fmt.Println("Sorted by Age:", people)

    // Sort by name (alphabetical)
    sort.Slice(people, func(i, j int) bool {
        return people[i].Name < people[j].Name
    })

    fmt.Println("Sorted by Name:", people)
}
Enter fullscreen mode Exit fullscreen mode

No matter how complex your data structure is, sort.Slice allows you to sort it in a personalized way, simply by changing the function that compares two items. This greatly simplifies life and avoids the need to implement sorting algorithms from scratch.

Happy hacking, Gophers! 🦫

Top comments (0)