willworth.dev
View RSS feed

Understanding Goroutines: A JavaScript Developer's Guide to Go Concurrency

Published on

As a JavaScript developer, you're probably familiar with promises, async/await, and handling asynchronous operations. Go takes a different approach to handling concurrent operations using goroutines. Let's explore what they are and when you should use them.

What is a Goroutine?

Think of a goroutine as similar to running an async function in JavaScript, but with some key differences. Here's a quick comparison:

JavaScript:

async function fetchUser() {
const response = await fetch('https://api.example.com/user');
const data = await response.json();
console.log(data);
}

// Call the async function
fetchUser();
console.log("This runs immediately!");

Go:

func fetchUser() {
// Simulating an API call
time.Sleep(time.Second)
fmt.Println("User data fetched")
}

func main() {
// Start a goroutine
go fetchUser()
fmt.Println("This runs immediately!")

// Need to wait for goroutine to finish
time.Sleep(time.Second * 2)
}

When Do You Need Goroutines?

Let's look at some real-world scenarios where goroutines are helpful:

1. Making Multiple API Calls

In JavaScript, you might use Promise.all:

async function fetchData() {
const results = await Promise.all([
fetch('api/users'),
fetch('api/posts'),
fetch('api/comments')
]);
// Process results
}

In Go, you can use goroutines:

func main() {
usersChan := make(chan []User)
postsChan := make(chan []Post)
commentsChan := make(chan []Comment)

// Start all requests concurrently
go func() {
users := fetchUsers()
usersChan <- users
}()

go func() {
posts := fetchPosts()
postsChan <- posts
}()

go func() {
comments := fetchComments()
commentsChan <- comments
}()

// Gather all results
users := <-usersChan
posts := <-postsChan
comments := <-commentsChan

// Process results
fmt.Println("Got", len(users), "users")
fmt.Println("Got", len(posts), "posts")
fmt.Println("Got", len(comments), "comments")
}

2. Processing Files

Let's say you need to process multiple files. In JavaScript, you might do:

async function processFiles(files) {
for (const file of files) {
await processFile(file); // One at a time
}
}

With Go, you can process them concurrently:

func main() {
files := []string{"file1.txt", "file2.txt", "file3.txt"}
var wg sync.WaitGroup

for _, file := range files {
wg.Add(1)
go func(filename string) {
defer wg.Done()
processFile(filename) // Runs concurrently
}(file)
}

wg.Wait() // Wait for all files to be processed
}

3. Real-World Example: Image Processing Service

Here's a practical example of processing image uploads:

type ImageJob struct {
SourcePath string
ResultChan chan string
}

func processImage(job ImageJob) {
// Simulate image processing
time.Sleep(time.Second)
result := "processed-" + job.SourcePath
job.ResultChan <- result
}

func main() {
// Simulate receiving multiple image upload requests
uploads := []string{"profile.jpg", "banner.jpg", "avatar.jpg"}
results := make(chan string, len(uploads))

// Process each upload concurrently
for _, upload := range uploads {
go processImage(ImageJob{
SourcePath: upload,
ResultChan: results,
})
}

// Collect all results
for i := 0; i < len(uploads); i++ {
processedPath := <-results
fmt.Println("Processed:", processedPath)
}
}

Making Goroutines Wait

In JavaScript, we have await. In Go, we have several options:

1. Using WaitGroups (Most Common)

func main() {
var wg sync.WaitGroup

// Start 3 workers
for i := 1; i <= 3; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Printf("Worker %d doing work\n", id)
time.Sleep(time.Second)
}(i)
}

// Wait for all workers to finish
wg.Wait()
fmt.Println("All workers done!")
}

2. Using Channels

func main() {
done := make(chan bool)

go func() {
fmt.Println("Doing work...")
time.Sleep(time.Second)
done <- true
}()

<-done // Wait for work to complete
fmt.Println("Work finished!")
}

Common Mistakes to Avoid

  1. Forgetting to Wait:
func main() {
go fmt.Println("This might not print!")
// Program exits immediately
}
  1. Accessing Variables Incorrectly in Loops:
// WRONG
for i := 0; i < 3; i++ {
go func() {
fmt.Println(i) // Will likely print 3 three times
}()
}

// RIGHT
for i := 0; i < 3; i++ {
go func(num int) {
fmt.Println(num) // Prints 0, 1, 2
}(i)
}

When Not to Use Goroutines

Not everything needs to be concurrent. Avoid goroutines when:

  1. The operation is simple and quick
  2. You're only doing one thing
  3. The overhead of creating goroutines would be more than the time saved

For example, don't do this:

// Unnecessary use of goroutine
go fmt.Println("Hello!")

Practical Tips

  1. Start with simple synchronous code
  2. Add goroutines when you need to:
    • Make multiple independent API calls
    • Process multiple files/items simultaneously
    • Handle multiple independent operations
  3. Always have a plan for how goroutines will finish
  4. Use WaitGroups when you need to wait for multiple goroutines
  5. Use channels when goroutines need to communicate

Conclusion

As a JavaScript developer, think of goroutines as a powerful mix of async/await and Web Workers. They're great for handling concurrent operations, but remember:

  • They're not exactly like Promises
  • They need explicit synchronization (WaitGroups or channels)
  • They're best used for truly concurrent operations

Start small, test thoroughly, and gradually add more concurrent operations as you become comfortable with the patterns. The key is to identify when concurrent execution would actually benefit your application's performance.