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
- Forgetting to Wait:
func main() {
go fmt.Println("This might not print!")
// Program exits immediately
}
- 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:
- The operation is simple and quick
- You're only doing one thing
- 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
- Start with simple synchronous code
- Add goroutines when you need to:
- Make multiple independent API calls
- Process multiple files/items simultaneously
- Handle multiple independent operations
- Always have a plan for how goroutines will finish
- Use WaitGroups when you need to wait for multiple goroutines
- 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.