1. نقش مکانیسم‌های همگام‌سازی

در برنامه‌نویسی همروند، زمانی که چند گوروتین منابع را به اشتراک می‌گذارند، لازم است اطمینان حاصل شود که منابع تنها توسط یک گوروتین در هر زمان دسترسی داشته باشند تا از وقوع شرایط تداخلی جلوگیری شود. برای این کار نیاز به استفاده از مکانیسم‌های همگام‌سازی است. مکانیسم‌های همگام‌سازی می‌توانند ترتیب دسترسی گوروتین‌های مختلف به منابع اشتراکی را هماهنگ کنند، به منظور تضمین سازگاری داده و همگام‌سازی وضعیت در یک محیط همروند.

زبان Go یک مجموعه غنی از مکانیسم‌های همگام‌سازی فراهم می‌کند که شامل اما محدود به موردهای زیر است:

  • قفل‌ها (sync.Mutex) و قفل‌های خواندنی-نوشتنی (sync.RWMutex)
  • کانال‌ها
  • گروه‌های انتظار (WaitGroups)
  • توابع اتمی (بسته‌ی atomic)
  • متغیرهای شرایطی (sync.Cond)

2. ابزارهای همگام‌سازی

2.1 Mutex (sync.Mutex)

2.1.1 مفهوم و نقش قفل‌ها

قفل یک مکانیسم همگام‌سازی است که با اجازه دادن تنها به یک گوروتین برای نگهداری قفل و دسترسی به منبع اشتراکی در هر زمان، اطمینان از عملکرد ایمن منابع اشتراکی را فراهم می‌کند. قفل از طریق متدهای Lock و Unlock همگام‌سازی را دستیابی می‌کند. فراخوانی متد Lock تا زمانی که قفل آزاد نشده است، مسدود خواهد بود و در این نقطه، سایر گوروتین‌ها که سعی در تصرف قفل کردند، منتظر می‌مانند. فراخوانی Unlock قفل را آزاد می‌کند و اجازه می‌دهد که گوروتین‌های منتظر آن را تصرف کنند.

var mu sync.Mutex

func criticalSection() {
    // قفل را برای دسترسی انحصاری به منبع گرفته‌ایم
    mu.Lock()
    // اینجا به منبع اشتراکی دسترسی داریم
    // ...
    // قفل را آزاد کرده و اجازه می‌دهیم که گوروتین‌های دیگر آن را تصرف کنند
    mu.Unlock()
}

2.1.2 استفاده‌های عملی از قفل

فرض کنید نیاز داریم تا یک شمارنده سراسری را حفظ کنیم و چندین گوروتین نیاز دارند تا مقدار آن را افزایش دهند. استفاده از یک قفل می‌تواند برای اطمینان از دقت شمارنده استفاده شود.

var (
    mu      sync.Mutex
    counter int
)

func increment() {
    mu.Lock()         // قفل را قبل از اصلاح شمارنده قفل می‌کنیم
    counter++         // مقدار شمارنده را به صورت ایمن افزایش می‌دهیم
    mu.Unlock()       // پس از عملیات، قفل را آزاد می‌کنیم تا گوروتین‌های دیگر به شمارنده دسترسی یابند
}

func main() {
    for i := 0; i < 10; i++ {
        go increment()  // چندین گوروتین را برای افزایش مقدار شمارنده شروع می‌کنیم
    }
    // برای مدتی منتظر بمانیم (در عمل، باید از WaitGroup یا روش‌های دیگر برای انتظار برای تمامی گوروتین‌ها استفاده کنید)
    time.Sleep(1 * time.Second)
    fmt.Println(counter)  // مقدار شمارنده را خروجی دهید
}

2.2 قفل خواندنی-نوشتنی (sync.RWMutex)

2.2.1 مفهوم قفل خواندنی-نوشتنی

RWMutex نوعی خاص از قفل است که به چندین گوروتین اجازه می‌دهد تا به صورت همزمان منابع اشتراکی را بخوانند، در حالی که عملیات نوشتن انحصاری است. نسبت به قفل‌ها، قفل‌های خواندنی-نوشتنی می‌توانند عملکرد را در صورت‌های چندخواننده روی منابع بهبود بخشند. این دارای چهار متد RLock، RUnlock برای قفل کردن و بازکردن عملیات‌های خواندن و Lock، Unlock برای قفل کردن و بازکردن عملیات‌های نوشتن است.

2.2.2 موارد کاربردی عملی از RWMutex

در یک برنامه پایگاه‌داده، عملیات‌های خواندن ممکن است از عملیات‌های نوشتن بسیار بیشتر باشند. استفاده از یک قفل خواندنی-نوشتنی می‌تواند باعث بهبود عملکرد سیستم شود زیرا به چندین گوروتین اجازه می‌دهد تا به صورت همزمان بخوانند.

var (
    rwMu  sync.RWMutex
    data  int
)

func readData() int {
    rwMu.RLock()         // قفل خواندن را گرفته و اجازه می‌دهد که عملیات‌های خواندن دیگر همزمان ادامه یابند
    defer rwMu.RUnlock() // اطمینان حاصل می‌کند که قفل با استفاده از defer آزاد می‌شود
    return data          // به صورت ایمن داده را می‌خوانیم
}

func writeData(newValue int) {
    rwMu.Lock()          // قفل نوشتن را گرفته و از عملیات‌های دیگر خواندن یا نوشتن در این زمان جلوگیری می‌کنیم
    data = newValue      // مقدار جدید را به صورت ایمن نوشته می‌کنیم
    rwMu.Unlock()        // پس از اتمام نوشتن، قفل را آزاد می‌کنیم
}

func main() {
    go writeData(42)     // یک گوروتین را برای انجام عملیات نوشتن شروع می‌کنیم
    fmt.Println(readData()) // گوروتین اصلی عملیات خواندن را انجام می‌دهد
    // از WaitGroup یا روش‌های همگام‌سازی دیگر برای اطمینان حاصل شود که تمامی گوروتین‌ها انجام شده‌اند
}

در مثال فوق، چندین خواننده می‌توانند به صورت همزمان تابع readData را اجرا کنند، اما یک نویسنده اجرای writeData را مسدود خواهد کرد و از خوانندگان جدید و نویسندگان دیگر جلوگیری می‌کند. این مکانیسم مزایای عملکردی را برای صورت‌هایی با بیشتر کارهای خواندن نسبت به نوشتن ارائه می‌دهد.

2.3 متغیرهای شرایطی (sync.Cond)

2.3.1 مفهوم متغیرهای شرطی

در مکانیزم همگام‌سازی زبان Go، متغیرهای شرطی برای انتظار یا اطلاع‌رسانی تغییرات شرایط به عنوان یک ابزار همگام‌سازی استفاده می‌شوند. متغیرهای شرطی همیشه همراه با قفل (sync.Mutex) استفاده می‌شوند که برای حفظ سازگاری شرط خود استفاده می‌شود.

مفهوم متغیرهای شرطی از دامنه سیستم‌عامل‌ها می‌آید و به گروهی از گوروتین‌ها امکان انتظار برای برآورده شدن یک شرط خاص را می‌دهد. به طور خاص‌تر، یک گوروتین ممکن است در حالت انتظار برای برآورده شدن یک شرط و تعلیق اجرا باشد، و یک گوروتین دیگر ممکن است با استفاده از متغیر شرطی شرایط دیگر را تغییر داده و سپس به سایر گوروتین‌ها اطلاع داده و آن‌ها را بیدار کند تا اجرایشان را ادامه دهند.

در کتابخانه استاندارد Go، متغیرهای شرطی از طریق نوع sync.Cond ارائه می‌شوند و اصلی‌ترین متدهای آن عبارتند از:

  • Wait: فراخوانی این متد باعث از دست دادن قفل نگه‌داری شده و مسدود شدن تا زمانی که یک گوروتین دیگر بر روی همان متغیر شرطی Signal یا Broadcast را فراخواند تا آن را بیدار کند. سپس دوباره سعی می‌کند تا قفل را بدست آورد.
  • Signal: یک گوروتین معلق را بیدار می‌کند که بر روی این متغیر شرطی منتظر است. اگر هیچ گوروتینی منتظر نباشد، فراخوانی این متد تأثیری نخواهد داشت.
  • Broadcast: تمام گوروتین‌هایی که بر روی این متغیر شرطی منتظر هستند را بیدار می‌کند.

متغیرهای شرطی نباید کپی شوند، بنابراین به طور کلی به عنوان یک فیلد اشاره‌گری از یک ساختار خاص استفاده می‌شوند.

2.3.2 موارد عملیاتی متغیرهای شرطی

در اینجا یک مثال از استفاده متغیرهای شرطی را برای نمایش یک مدل تولید‌کننده-مصرف‌کننده نشان می‌دهد:

package main

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

// SafeQueue یک صف ایمن محافظت شده توسط یک mutex است
type SafeQueue struct {
    mu    sync.Mutex
    cond  *sync.Cond
    queue []interface{}
}

// Enqueue یک عنصر را به انتهای صف اضافه می‌کند و گوروتین‌های منتظر را اطلاع‌رسانی می‌کند
func (sq *SafeQueue) Enqueue(item interface{}) {
    sq.mu.Lock()
    defer sq.mu.Unlock()

    sq.queue = append(sq.queue, item)
    sq.cond.Signal() // اطلاع‌رسانی گوروتین‌های منتظر که صف خالی نیست
}

// Dequeue یک عنصر را از سر صف حذف می‌کند، اگر صف خالی باشد منتظر می‌ماند
func (sq *SafeQueue) Dequeue() interface{} {
    sq.mu.Lock()
    defer sq.mu.Unlock()

    // انتظار وقتی که صف خالی باشد
    for len(sq.queue) == 0 {
        sq.cond.Wait() // منتظر تغییر شرط می‌ماند
    }

    item := sq.queue[0]
    sq.queue = sq.queue[1:]
    return item
}

func main() {
    queue := make([]interface{}, 0)
    sq := SafeQueue{
        mu:    sync.Mutex{},
        cond:  sync.NewCond(&sync.Mutex{}),
        queue: queue,
    }

    // گوروتین تولید‌کننده
    go func() {
        for i := 0; i < 5; i++ {
            time.Sleep(1 * time.Second)         // شبیه‌سازی زمان تولید
            sq.Enqueue(fmt.Sprintf("item%d", i)) // تولید یک عنصر
            fmt.Println("تولید:", i)
        }
    }()

    // گوروتین مصرف‌کننده
    go func() {
        for i := 0; i < 5; i++ {
            item := sq.Dequeue() // مصرف یک عنصر، در صورتی که صف خالی باشد منتظر می‌ماند
            fmt.Printf("مصرف: %v\n", item)
        }
    }()

    // انتظار برای مدت کافی از زمان برای اطمینان از اتمام تولید و مصرف
    time.Sleep(10 * time.Second)
}

در این مثال، یک ساختار SafeQueue با یک صف داخلی و یک متغیر شرطی تعریف شده است. هنگامی که مصرف‌کننده متد Dequeue را فراخوانی کرده و صف خالی است، با استفاده از متد Wait در حالت انتظار قرار می‌گیرد. هنگامی که تولید‌کننده متد Enqueue را فراخوانی کرده و یک عنصر جدید را در صف قرار می‌دهد، از متد Signal برای بیدار کردن مصرف‌کننده منتظر استفاده می‌کند.

2.4 WaitGroup

2.4.1 مفهوم و استفاده از WaitGroup

sync.WaitGroup یک مکانیزم همگام‌سازی است که برای انتظار برای تکمیل یک گروه از گوروتین‌ها استفاده می‌شود. هنگامی که یک گوروتین را شروع می‌کنید، می‌توانید با فراخوانی متد Add شمارنده را افزایش دهید و هر گوروتین می‌تواند زمانی که کارش را تمام کرده است، متد Done را فراخوانی کند (که در واقع Add(-1) را انجام می‌دهد). گوروتین اصلی می‌تواند با فراخوانی متد Wait تا زمانی که شمارنده به 0 برسد (که نشان‌دهنده تمام شدن وظایف تمامی گوروتین‌هاست) مسدود شود.

هنگام استفاده از WaitGroup، نکات زیر باید مورد توجه قرار گیرد:

  • متدهای Add، Done و Wait ایمن نخ نیستند و نباید به صورت همزمان در چند گوروتین فراخوانی شوند.
  • متد Add باید قبل از شروع گوروتین تازه ایجاد شود فراخوانی شود.

2.4.2 موارد کاربرد عملی از WaitGroup

در ادامه، مثالی از استفاده از WaitGroup آورده شده است:

package main

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

func worker(id int, wg *sync.WaitGroup) {
	defer wg.Done() // پس از اتمام، WaitGroup را مطلع می‌کند

fmt.Printf("شروع کارگر %d\n", id)
time.Sleep(time.Second) // عملیات مصرف‌کننده زمان را شبیه‌سازی می‌کند
fmt.Printf("کارگر %d انجام شد\n", id)
}

func main() {
var wg sync.WaitGroup

for i := 1; i <= 5; i++ {
wg.Add(1) // قبل از شروع گوروتین، شمارنده را افزایش می‌دهد
go worker(i, &wg)
}

wg.Wait() // منتظر می‌ماند تا همه گوروتین‌های کارگر اتمام کار شوند
fmt.Println("همه کارگرها انجام شدند")
}

در این مثال، تابع worker اجرای یک وظیفه را شبیه‌سازی می‌کند. در تابع اصلی، ما پنج worker گوروتین را شروع می‌کنیم. قبل از شروع هر گوروتین، ما wg.Add(1) را صدا می‌زنیم تا به WaitGroup اطلاع دهیم که یک وظیفه جدید در حال اجراست. هر زمان که تابع کارگر کامل می‌شود، defer wg.Done() را فراخوانی می‌کند تا به WaitGroup اطلاع دهد که وظیفه انجام شده است. پس از شروع همه گوروتین‌ها، تابع اصلی با wg.Wait() در محلی مسدود می‌شود تا همه کارگرها پایان کار را گزارش دهند.

2.5 عملیات اتمی (sync/atomic)

2.5.1 مفهوم عملیات اتمی

عملیات اتمی به عملیات در برنامه‌نویسی همروند اشاره دارد که غیرقابل تقسیم هستند، به این معنی که در حین اجرا توسط عملیات دیگر مختل نمی‌شوند. برای چندین گوروتین، استفاده از عملیات اتمی می‌تواند همگرایی داده و هماهنگی وضعیت را بدون نیاز به قفل‌گذاری اطمینان‌بخشد، زیرا خود عملیات اتمی اتمیت اجرا را تضمین می‌کند.

در زبان Go، بسته sync/atomic عملیات حافظه‌اتمی پایین‌سطح را فراهم می‌کند. برای انواع داده‌های پایه‌ای مانند int32، int64، uint32، uint64، uintptr و pointer، می‌توان از متدهای بسته sync/atomic برای عملیات همروند ایمن استفاده کرد. اهمیت عملیات اتمی در این است که عمده‌بازیگر ساخت قبایل همروندی دیگر (مانند قفل‌ها و متغیرهای شرطی) است و اغلب بهتر از مکانیسم‌های قفل‌گذاری کارا هم هستند.

2.5.2 موارد کاربرد عملیات اتمی

فرض کنید که به یک سناریو نیاز داریم که تعداد همزمان بازدید‌کنندگان وب‌سایت را پیگیری کنیم. با استفاده از یک متغیر شمارنده ساده به طور شهودی، تعداد بازدید‌کنندگان را زمانی که یک بازدید‌کننده می‌آید افزایش می‌دهیم و زمانی که یک بازدید‌کننده می‌رود کاهش می‌دهیم. با این حال، در یک محیط همزمان، این رویکرد به مسابقه داده منجر می‌شود. بنابراین، ما می‌توانیم از بسته sync/atomic برای به‌طور ایمن انجام دادن عملیات برروی شمارنده استفاده کنیم.

package main

import (
	"fmt"
	"sync"
	"sync/atomic"
	"time"
)

var visitorCount int32

func incrementVisitorCount() {
atomic.AddInt32(&visitorCount, 1)
}

func decrementVisitorCount() {
atomic.AddInt32(&visitorCount, -1)
}

func main() {
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
incrementVisitorCount()
time.Sleep(time.Second) // زمان بازدید از بازدید‌کننده
decrementVisitorCount()
wg.Done()
}()
}
wg.Wait()
fmt.Printf("شمارش بازدید‌کننده‌های فعلی: %d\n", visitorCount)
}

در این مثال، ما ۱۰۰ گوروتین ایجاد می‌کنیم تا ورود و خروج بازدید‌کنندگان را شبیه‌سازی کنند. با استفاده از تابع atomic.AddInt32()، ما اطمینان حاصل می‌کنیم که افزایش و کاهش شمارنده حتی در شرایط بسیار همروند نیز اتمی هستند و از این رو دقت visitorCount تضمین می‌شود.

2.6 مکانیزم هماهنگسازی کانال

2.6.1 ویژگی‌های هماهنگسازی کانال‌ها

کانال‌ها راهی برای ارتباط بین گوروتین‌ها در زبان Go در سطح زبان فراهم می‌کنند. یک کانال امکان ارسال و دریافت داده‌ها را فراهم می‌کند. زمانی که یک گوروتین سعی می‌کند داده‌ای را از یک کانال بخواند و کانال دارای داده نباشد، تا وقتی داده در دسترس باشد مسدود می‌شود. به همین ترتیب، اگر کانال پر باشد (برای یک کانال غیرپیش‌بارگذاری، این به معنای داشتن داده است)، گوروتینی که سعی می‌کند داده ارسال کند هم مسدود می‌شود تا فضای نوشتن وجود داشته باشد. این ویژگی کانال‌ها را برای هماهنگ‌سازی بین گوروتین‌ها بسیار مفید می‌کند.

2.6.2 استفاده از موارد همگام سازی با کانال‌ها

فرض کنید که یک وظیفه وجود دارد که باید توسط چند گوروتین انجام شود، هرکدام از آنها یک زیروظیفه را انجام می‌دهند، سپس نیاز است که نتایج تمام زیروظایفه‌ها را جمع‌آوری کنیم. می‌توانیم از یک کانال برای انتظار تمام گوروتین‌ها برای پایان کار استفاده کنیم.

package main

import (
    "fmt"
    "sync"
)

func worker(id int, wg *sync.WaitGroup, resultChan chan<- int) {
    defer wg.Done()
    // انجام چند عملیات...
    fmt.Printf("شروع کارگر %d\n", id)
    // فرض کنید نتیجه زیروظیفه شناسه کارگر است
    resultChan <- id
    fmt.Printf("کارگر %d انجام شد\n", id)
}

func main() {
    var wg sync.WaitGroup
    numWorkers := 5
    resultChan := make(chan int, numWorkers)

    for i := 0; i < numWorkers; i++ {
        wg.Add(1)
        go worker(i, &wg, resultChan)
    }

    go func() {
        wg.Wait()
        close(resultChan)
    }()

    // جمع‌آوری تمام نتایج
    for result := range resultChan {
        fmt.Printf("نتیجه دریافت شد: %d\n", result)
    }
}

در این مثال، ما 5 گوروتین برای انجام وظایف راه‌اندازی می‌کنیم و نتایج را از طریق کانال resultChan جمع‌آوری می‌کنیم. گوروتین اصلی منتظر این است که کل کار را در یک گوروتین جداگانه انجام دهد و سپس کانال نتیجه را ببندد. سپس، گوروتین اصلی تمامی عناصر کانال resultChan را بررسی کرده و نتایج همه گوروتین‌ها را چاپ می‌کند.

2.7 اجرای یک‌باره (sync.Once)

sync.Once یک ابزار همگام‌سازی است که اطمینان می‌یابد تا زمانی که برنامه اجرا می‌شود، عملیات فقط یک‌بار انجام می‌شود. استفاده‌ی معمول از sync.Once در مقداردهی اولیه یک شیء یکتا یا در سناریوهای نیازمندی‌های اولیه به تأخیر است. بدون توجه به اینکه چند گوروتین این عملیات را فراخوانی کنند، تنها یک‌بار اجرا می‌شود، به همین دلیل تابع Do نام دارد.

sync.Once به طور کامل مسائل همزمانی و کارآیی اجرا را تعادل بخشیده و نگرانی‌های مربوط به مشکلات عملکرد ناشی از مقداردهی تکراری را از بین می‌برد.

یک مثال ساده برای نشان دادن استفاده از sync.Once:

package main

import (
    "fmt"
    "sync"
)

var once sync.Once
var instance *Singleton

type Singleton struct{}

func Instance() *Singleton {
    once.Do(func() {
        fmt.Println("اکنون اقدام به ایجاد نمونه‌ی تکی می‌کنیم.")
        instance = &Singleton{}
    })
    return instance
}

func main() {
    for i := 0; i < 10; i++ {
        go Instance()
    }
    fmt.Scanln() // منتظر بمانید تا خروجی را مشاهده کنید
}

در این مثال، حتی اگر تابع Instance به صورت همروند چندین بار فراخوانی شود، ایجاد نمونه‌ی Singleton فقط یک‌بار اتفاق می‌افتد. فراخوانی‌های بعدی به صورت مستقیم نمونه‌ی تکی ایجاد شده در اولین بار را برمی‌گردانند وحتماً یکتایی نمونه را تضمین می‌کند.

2.8 گروه خطا (ErrGroup)

ErrGroup یک کتابخانه در زبان Go است که برای همگام‌سازی چندین گوروتین و جمع‌آوری خطاهای آنها استفاده می‌شود. این بخشی از بسته "golang.org/x/sync/errgroup" است و روشی مختصر برای رفع موارد خطا در عملیات همروند فراهم می‌کند.

2.8.1 مفهوم ErrGroup

ایده اصلی ErrGroup این است که یک گروه از وظایف مرتبط را (معمولاً به صورت همروند اجرا می‌شوند) به یکدیگر متصل کنیم و اگر یکی از وظایف شکست بخورد، اجرای کل گروه لغو می‌شود. همزمان، اگر هرکدام از این عملیات همروند خطا برگرداند، ErrGroup این خطا را ضبط و برمی‌گرداند.

برای استفاده از ErrGroup، ابتدا باید بسته را وارد کنید:

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

سپس، یک نمونه از ErrGroup ایجاد کنید:

var g errgroup.Group

بعد، می‌توانید وظایف را به صورت اسناد محضی به ErrGroup منتقل کنید و با فراخوانی متد Go یک گوروتین جدید را راه‌اندازی کنید:

g.Go(func() error {
    // انجام یک وظیفه خاص
    // اگر همه چیز خوب پیش برود
    return nil
    // اگر خطایی رخ دهد
    // return fmt.Errorf("خطا رخ داده است")
})

سرانجام، متد Wait را فراخوانی کنید که منتظر ماندن و انجام تمام وظایف را بلوکه می‌کند. اگر هرکدام از این وظایف خطا برگرداند، Wait این خطا را برمی‌گرداند:

if err := g.Wait(); err != nil {
    // با خطا برخورد کنید
    log.Fatalf("خطای اجرای وظیفه: %v", err)
}

2.8.2 مورد عملی ErrGroup

یک سناریو را در نظر بگیرید که ما نیاز داریم به صورت همزمان داده ها را از سه منبع داده مختلف دریافت کنیم، و اگر یکی از منابع داده با مشکل مواجه شود، می خواهیم به طور فوری سایر عملیات دریافت داده را لغو کنیم. این وظیفه را می توان با استفاده از ErrGroup به آسانی انجام داد:

package main

import (
    "fmt"
    "golang.org/x/sync/errgroup"
)

func fetchDataFromSource1() error {
    // شبیه سازی دریافت داده از منبع 1
    return nil // یا برگرداندن خطا برای شبیه سازی یک خطا
}

func fetchDataFromSource2() error {
    // شبیه سازی دریافت داده از منبع 2
    return nil // یا برگرداندن خطا برای شبیه سازی یک خطا
}

func fetchDataFromSource3() error {
    // شبیه سازی دریافت داده از منبع 3
    return nil // یا برگرداندن خطا برای شبیه سازی یک خطا
}

func main() {
    var g errgroup.Group

    g.Go(fetchDataFromSource1)
    g.Go(fetchDataFromSource2)
    g.Go(fetchDataFromSource3)

    // منتظر شدن برای تکمیل شدن همه گوروتین ها و جمع آوری خطاهای آنها
    if err := g.Wait(); err != nil {
        fmt.Printf("هنگام دریافت داده خطا رخ داد: %v\n", err)
        return
    }

    fmt.Println("تمام داده ها با موفقیت دریافت شدند!")
}

در این مثال، توابع fetchDataFromSource1، fetchDataFromSource2 و fetchDataFromSource3 شبیه سازی دریافت داده از منابع داده مختلف هستند. آنها به متد g.Go ارسال می‌شوند و در گوروتین‌های جداگانه اجرا می‌شوند. اگر هر یک از توابع یک خطا برگردانند، g.Wait به طور فوری آن خطا را برمی‌گرداند و امکان برخورداری از اندازه‌گیری مناسب هنگام رخداد آن خطا را فراهم می‌کند. اگر همه توابع با موفقیت اجرا شوند، g.Wait مقدار nil را برمی‌گرداند که نمایان‌گر این است که تمام وظایف با موفقیت انجام شده اند.

یک ویژگی مهم دیگر از ErrGroup این است که اگر هر یک از گوروتین‌ها در حالت خشونتیلا بیافتد، تلاش می‌کند تا این خشونت را دیده‌باندازد و آن را به عنوان یک خطا برگرداند. این کمک می‌کند تا از شکستن غیرفعالیت های دیگر به طور زیبایی پایان یابد. البته، اگر بخواهید وظایف به سیگنال‌های لغوی بیرونی پاسخ دهند، می‌توانید تابع WithContext از errgroup را با بسته متن منسجم کنید تا یک متن قابل لغو ارائه دهد.

به این ترتیب، ErrGroup به یک مکانیسم هماهنگ سازی و پردازش خطای عملیاتی در عملیات همزمان برنامه نویسی Go تبدیل می‌شود.