---
title: "Goroutines"
description: "Using goroutines effectively with the TraceRTM Go SDK"
---

# Goroutines

Best practices for using goroutines with the TraceRTM Go SDK.

## Thread-Safe Client

The TraceRTM client is designed to be thread-safe:

```go
client := tracertm.NewClient(os.Getenv("TRACERTM_API_KEY"))

// Safe to share across goroutines
go func() { client.Projects.List(ctx, nil) }()
go func() { client.Items.List(ctx, projectID, nil) }()
```

---

## Basic Goroutine Patterns

### Fire and Forget

```go
// Fire and forget (not recommended for important operations)
go func() {
    _, err := client.Items.Update(ctx, itemID, &tracertm.UpdateItemInput{
        Tags: append(item.Tags, "processed"),
    })
    if err != nil {
        log.Printf("Error updating item: %v", err)
    }
}()
```

### Goroutine with Result

```go
resultCh := make(chan *tracertm.Item)
errCh := make(chan error)

go func() {
    item, err := client.Items.Get(ctx, itemID)
    if err != nil {
        errCh <- err
        return
    }
    resultCh <- item
}()

select {
case item := <-resultCh:
    fmt.Printf("Got item: %s\n", item.Title)
case err := <-errCh:
    log.Printf("Error: %v", err)
case <-ctx.Done():
    log.Println("Context cancelled")
}
```

### Multiple Goroutines with WaitGroup

```go
var wg sync.WaitGroup

for _, id := range itemIDs {
    wg.Add(1)
    go func(itemID string) {
        defer wg.Done()
        item, err := client.Items.Get(ctx, itemID)
        if err != nil {
            log.Printf("Error fetching %s: %v", itemID, err)
            return
        }
        fmt.Printf("Fetched: %s\n", item.Title)
    }(id)
}

wg.Wait()
fmt.Println("All items fetched")
```

---

## Coordinated Goroutines

### Load Multiple Resources

```go
func loadDashboardData(ctx context.Context, client *tracertm.Client, projectID string) (*DashboardData, error) {
    var wg sync.WaitGroup
    var mu sync.Mutex

    data := &DashboardData{}
    var errs []error

    // Load project
    wg.Add(1)
    go func() {
        defer wg.Done()
        project, err := client.Projects.Get(ctx, projectID)
        mu.Lock()
        defer mu.Unlock()
        if err != nil {
            errs = append(errs, fmt.Errorf("project: %w", err))
            return
        }
        data.Project = project
    }()

    // Load recent items
    wg.Add(1)
    go func() {
        defer wg.Done()
        items, err := client.Items.List(ctx, projectID, &tracertm.ListItemsOptions{
            Sort:  "updated_at",
            Order: "desc",
            Limit: 10,
        })
        mu.Lock()
        defer mu.Unlock()
        if err != nil {
            errs = append(errs, fmt.Errorf("items: %w", err))
            return
        }
        data.RecentItems = items.Data
    }()

    // Load coverage
    wg.Add(1)
    go func() {
        defer wg.Done()
        coverage, err := client.Reports.Coverage(ctx, &tracertm.CoverageReportInput{
            ProjectID:  projectID,
            SourceType: "requirement",
            TargetType: "test_case",
        })
        mu.Lock()
        defer mu.Unlock()
        if err != nil {
            errs = append(errs, fmt.Errorf("coverage: %w", err))
            return
        }
        data.Coverage = coverage
    }()

    // Load stats
    wg.Add(1)
    go func() {
        defer wg.Done()
        stats, err := client.Projects.GetStats(ctx, projectID)
        mu.Lock()
        defer mu.Unlock()
        if err != nil {
            errs = append(errs, fmt.Errorf("stats: %w", err))
            return
        }
        data.Stats = stats
    }()

    wg.Wait()

    if len(errs) > 0 {
        return nil, fmt.Errorf("errors: %v", errs)
    }
    return data, nil
}

type DashboardData struct {
    Project     *tracertm.Project
    RecentItems []*tracertm.Item
    Coverage    *tracertm.CoverageReport
    Stats       *tracertm.ProjectStats
}
```

---

## Goroutine Lifecycle Management

### Proper Cancellation

```go
func processWithCancellation(ctx context.Context, client *tracertm.Client, projectID string) error {
    // Create cancelable context
    ctx, cancel := context.WithCancel(ctx)
    defer cancel()

    errCh := make(chan error, 1)

    go func() {
        iter := client.Items.Iterate(ctx, projectID, nil)
        for iter.Next() {
            select {
            case <-ctx.Done():
                errCh <- ctx.Err()
                return
            default:
            }

            item := iter.Item()
            if err := processItem(item); err != nil {
                errCh <- err
                return
            }
        }
        errCh <- iter.Err()
    }()

    select {
    case err := <-errCh:
        return err
    case <-time.After(5 * time.Minute):
        cancel()
        return fmt.Errorf("timeout")
    }
}
```

### Graceful Shutdown

```go
func main() {
    client := tracertm.NewClient(os.Getenv("TRACERTM_API_KEY"))

    ctx, cancel := context.WithCancel(context.Background())

    // Handle shutdown signals
    sigCh := make(chan os.Signal, 1)
    signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)

    doneCh := make(chan struct{})

    go func() {
        defer close(doneCh)
        runWorker(ctx, client)
    }()

    // Wait for signal
    <-sigCh
    fmt.Println("Shutting down...")

    // Cancel context
    cancel()

    // Wait for goroutine to finish
    select {
    case <-doneCh:
        fmt.Println("Clean shutdown")
    case <-time.After(10 * time.Second):
        fmt.Println("Forced shutdown")
    }
}

func runWorker(ctx context.Context, client *tracertm.Client) {
    ticker := time.NewTicker(1 * time.Minute)
    defer ticker.Stop()

    for {
        select {
        case <-ctx.Done():
            return
        case <-ticker.C:
            syncData(ctx, client)
        }
    }
}
```

---

## Error Handling in Goroutines

### Collecting Errors

```go
func processAllItems(ctx context.Context, client *tracertm.Client, projectID string) error {
    var wg sync.WaitGroup
    errCh := make(chan error, 100)

    iter := client.Items.Iterate(ctx, projectID, nil)
    for iter.Next() {
        item := iter.Item()
        wg.Add(1)
        go func(i *tracertm.Item) {
            defer wg.Done()
            if err := processItem(i); err != nil {
                errCh <- fmt.Errorf("item %s: %w", i.ID, err)
            }
        }(item)
    }

    // Close error channel when all goroutines complete
    go func() {
        wg.Wait()
        close(errCh)
    }()

    // Collect errors
    var errs []error
    for err := range errCh {
        errs = append(errs, err)
    }

    if iter.Err() != nil {
        errs = append(errs, iter.Err())
    }

    if len(errs) > 0 {
        return fmt.Errorf("%d errors: %v", len(errs), errs)
    }
    return nil
}
```

### Using errgroup

```go
import "golang.org/x/sync/errgroup"

func processAllItems(ctx context.Context, client *tracertm.Client, projectID string) error {
    g, ctx := errgroup.WithContext(ctx)

    items, err := client.Items.List(ctx, projectID, nil)
    if err != nil {
        return err
    }

    for _, item := range items.Data {
        item := item // Capture loop variable
        g.Go(func() error {
            return processItem(item)
        })
    }

    return g.Wait()
}
```

### First Error Wins

```go
func fetchFirstAvailable(ctx context.Context, client *tracertm.Client, ids []string) (*tracertm.Item, error) {
    ctx, cancel := context.WithCancel(ctx)
    defer cancel()

    resultCh := make(chan *tracertm.Item, 1)
    errCh := make(chan error, len(ids))

    for _, id := range ids {
        go func(itemID string) {
            item, err := client.Items.Get(ctx, itemID)
            if err != nil {
                errCh <- err
                return
            }
            select {
            case resultCh <- item:
                cancel() // Cancel other goroutines
            default:
            }
        }(id)
    }

    select {
    case item := <-resultCh:
        return item, nil
    case <-ctx.Done():
        return nil, ctx.Err()
    }
}
```

---

## Resource Management

### Limiting Goroutine Count

```go
func processBatch(ctx context.Context, client *tracertm.Client, items []*tracertm.Item, maxConcurrent int) error {
    sem := make(chan struct{}, maxConcurrent)
    var wg sync.WaitGroup
    var mu sync.Mutex
    var errs []error

    for _, item := range items {
        wg.Add(1)
        sem <- struct{}{} // Acquire

        go func(i *tracertm.Item) {
            defer func() {
                <-sem // Release
                wg.Done()
            }()

            if err := processItem(i); err != nil {
                mu.Lock()
                errs = append(errs, err)
                mu.Unlock()
            }
        }(item)
    }

    wg.Wait()

    if len(errs) > 0 {
        return fmt.Errorf("%d errors occurred", len(errs))
    }
    return nil
}
```

### Goroutine Pool

```go
type WorkerPool struct {
    client  *tracertm.Client
    jobs    chan Job
    results chan Result
    wg      sync.WaitGroup
}

type Job struct {
    ItemID string
    Action string
}

type Result struct {
    ItemID string
    Item   *tracertm.Item
    Error  error
}

func NewWorkerPool(client *tracertm.Client, workers int) *WorkerPool {
    pool := &WorkerPool{
        client:  client,
        jobs:    make(chan Job, 100),
        results: make(chan Result, 100),
    }

    for i := 0; i < workers; i++ {
        pool.wg.Add(1)
        go pool.worker()
    }

    return pool
}

func (p *WorkerPool) worker() {
    defer p.wg.Done()
    ctx := context.Background()

    for job := range p.jobs {
        item, err := p.client.Items.Get(ctx, job.ItemID)
        p.results <- Result{
            ItemID: job.ItemID,
            Item:   item,
            Error:  err,
        }
    }
}

func (p *WorkerPool) Submit(job Job) {
    p.jobs <- job
}

func (p *WorkerPool) Close() {
    close(p.jobs)
    p.wg.Wait()
    close(p.results)
}
```

---

## Best Practices

### 1. Always Pass Context

```go
// Good
go func() {
    item, err := client.Items.Get(ctx, id)
    // ...
}()

// Bad - no way to cancel
go func() {
    item, err := client.Items.Get(context.Background(), id)
    // ...
}()
```

### 2. Capture Loop Variables

```go
// Good
for _, id := range ids {
    id := id // Capture
    go func() {
        client.Items.Get(ctx, id)
    }()
}

// Also good
for _, id := range ids {
    go func(itemID string) {
        client.Items.Get(ctx, itemID)
    }(id)
}

// Bad - race condition
for _, id := range ids {
    go func() {
        client.Items.Get(ctx, id) // id may change
    }()
}
```

### 3. Don't Leak Goroutines

```go
// Good - context cancellation stops goroutine
go func() {
    select {
    case <-ctx.Done():
        return
    case result := <-workCh:
        process(result)
    }
}()

// Bad - goroutine may never exit
go func() {
    result := <-workCh // May block forever
    process(result)
}()
```

### 4. Use Appropriate Synchronization

```go
// Good - mutex for shared state
var mu sync.Mutex
var results []*tracertm.Item

go func() {
    item, _ := client.Items.Get(ctx, id)
    mu.Lock()
    results = append(results, item)
    mu.Unlock()
}()

// Good - channels for communication
resultCh := make(chan *tracertm.Item)
go func() {
    item, _ := client.Items.Get(ctx, id)
    resultCh <- item
}()
```

---

## Related Documentation

- [Concurrency](/docs/api-reference/sdks/go/concurrency) - Concurrency patterns
- [Channels](/docs/api-reference/sdks/go/channels) - Channel patterns
- [Error Handling](/docs/api-reference/sdks/go/error-handling) - Error handling
