1. บทบาทของกลไกการซิงโครไนเซชัน

ในการเขียนโปรแกรมแบบคอนคัร์เรนต์ (concurrent programming) เมื่อ Goroutines หลายตัวแบ่งปันทรัพยากร จะต้องใช้กลไกการซิงโครไนเซชันเพื่อให้แน่ใจว่าทรัพยากรสามารถเข้าถึงได้โดย Goroutines เพียงตัวเดียวในเวลาใดเพื่อป้องกันเงื่อนไขแข่งขัน (race conditions) ซึ่งจะต้องใช้กลไกการซิงโครไนเซชัน โดยกลไกการซิงโครไนเซชันจะสามารถประสานลำดับการเข้าถึงของ Goroutines ต่าง ๆ ไปยังทรัพยากรที่ถูกแบ่งปัน และทำใให้มั่นใจได้ว่าข้อมูลสอดคล้องและสถานะสะท้อนระหว่างการทำงานในสภาพแวดล้อมที่เป็นพร้อมกัน

Go language มีชุดกว้างของกลไกการซิงโครไนเซชัน ซึ่งรวมไปถึง แต่ไม่จำกัดอยู่ที่:

  • Mutexes (sync.Mutex) และ read-write mutexes (sync.RWMutex)
  • Channels
  • WaitGroups
  • ฟังก์ชัน atomic (atomic package)
  • ตัวแปรเงื่อนไข (sync.Cond)

2. พื้นฐานของกลไกการซิงโครไน

2.1 Mutex (sync.Mutex)

2.1.1 หลักการและบทบาทของ mutex

Mutex เป็นกลไกการซิงโครไนที่ให้ความมั่นใจในการดำเนินการอย่างปลอดภัยของทรัพยากรที่ถูกแบ่งปันโดยอนุญาตให้ Goroutine เพียงตัวเดียวถือล็อกเพื่อเข้าถึงทรัพยากรที่ถูกแบ่งปันในเวลาใด ๆ เพื่อป้องกันอุณหภูมิแข่งขัน (race conditions) โดย mutex จะบรรลุการซิงโครไนผ่านการเรียกใช้วิธี Lock และ Unlock การเรียกใช้วิธี Lock จะบล็อกจนกว่าล็อกจะถูกปล่อย ณ จุดนี้ Goroutine อื่น ๆ ที่พยายามเรียกใช้จะรอ การเรียกใช้ก็ Unlock จะปล่อยล็อก อนุญาตให้ Goroutine ที่รอดึงได้

var mu sync.Mutex

func criticalSection() {
    // ถือล็อกเพื่อเข้าถึงทรัพยากรอย่างที่เดียว
    mu.Lock()
    // เข้าถึงทรัพยากรที่ถูกแบ่งปันที่นี่
    // ...
    // ปล่อยล็อกเพื่ออนุญาตให้ Goroutines อื่น ๆ ได้ถือ
    mu.Unlock()
}

2.1.2 การใช้งานจริงของ mutex

สมมติว่าเราต้องการรักษาตัวนับทั่วไปและ Goroutines หลาย ๆ ตัวต้องการเพิ่มค่าของมัน การใช้ mutex สามารถรับประกันความถูกต้องของตัวนับได้

var (
    mu      sync.Mutex
    counter int
)

func increment() {
    mu.Lock()         // ล็อกก่อนที่จะปรับเปลี่ยนค่าตัวนับ
    counter++         // เพิ่มค่าตัวนับอย่างปลอดภัย
    mu.Unlock()       // ปล่อยล็อกหลังจากดำเนินการเสร็จ อนุญาตให้ Goroutines อื่น ๆ ใช้ค่าตัวนับ
}

func main() {
    for i := 0; i < 10; i++ {
        go increment()  // เริ่ม Goroutines หลาย ๆ ตัวเพิ่มค่าตัวนับ
    }
    // รอเวลาบางครู่ (ในการปฏิบัติจริง ควรใช้ WaitGroup หรือวิธีอื่น ๆ เพื่อรอให้ Goroutines ทั้งหมดทำงานเสร็จ)
    time.Sleep(1 * time.Second)
    fmt.Println(counter)  // ผลลัพธ์ของค่าตัวนับ
}

2.2 Read-Write Mutex (sync.RWMutex)

2.2.1 แนวคิดของ read-write mutex

RWMutex เป็นประเภทพิเศษของล็อกที่อนุญาตให้ Goroutines หลายตัวอ่านทรัพยากรที่ถูกแบ่งปันพร้อมกันในขณะเดียวกัน ในขณะที่การทำงานเขียนจะใช้เวลาที่เพียงตัวเดียว โดยเปรียบเทียบกับ mutexes การเขียน-อ่านสามารถพัฒนาประสิทธิภาพในสถานการณ์ที่มีการอ่านหลาย ๆ การดำเนินการ เหมือนรายการมีสี่วิธีการ: RLock, RUnlock เพื่อล็อกและปลดล็อกกระบวนการอ่าน และ Lock, Unlock เพื่อล็อกและปลดล็อกกระบวนการเขียน

2.2.2 กรณีการใช้จริงของ Read-Write Mutex

ในแอปพลิเคชันเบสส์ข้อมูลฐานข้อมูล การดำเนินการอ่านอาจมีความถี่มากกว่าการเขียน การใช้ lock ของการอ่าน-เขียนสามารถทราบประสิทธิภาพระบบเพราะจะอนุญาตให้ Goroutines หลาย ๆ ตัวที่อ่านในขณะเดียวกัน

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)     // เริ่ม goroutine เพื่อทำการเขียน
    fmt.Println(readData()) // ใน goroutine หลักดำเนินการอ่าน
    // ใช้ WaitGroup หรือวิธีการซิงค์อื่น ๆ เพื่อให้มั่นใจว่า Goroutines ทั้งหมดเสร็จสิ้น
}

ในตัวอย่างข้างต้น การอ่านหลาย ๆ ตัวสามารถดำเนินการฟังก์ชัน readData ไปพร้อมกัน แต่การเขียนที่ดำเนินด้วย writeData จะบล็อกการสร้างการอ่านใหม่และการเขียนอื่น ๆ กลไกนี้จึงมีประสิทธิภาพในสถานการณ์ที่มีการอ่านมากกว่าการเขียน

2.3 ตัวแปรเงื่อนไข (sync.Cond)

2.3.1 แนวคิดของตัวแปรเงื่อนไข

ในกลุ่มภาษาโกแลงซ์เครื่องมือการซิงโครไนเซชันใช้ตัวแปรเงื่อนไขเพื่อรอหรือแจ้งเหตุการณ์เปลี่ยนแปลงของเงื่อนไขบางอย่างเป็นพื้นที่ซิงโครไนไซเซชัน ตัวแปรเงื่อนไขจะถูกใช้ร่วมกับ mutex (sync.Mutex) เสมอ ซึ่งใช้สำหรับป้องกันความสม่ำเสมอของเงื่อนไขเอง

แนวคิดของตัวแปรเงื่อนไขมาจากโดเมนของระบบปฏิบัติการ ทำให้กลุ่มของกอรูทีนอย่างคนละหลายตัวสามารถรอให้เงื่อนไขบางอย่างถูกตอบสนอง โดยระบุกอรูทีนที่หยุดการทำงานในขณะที่รอให้เงื่อนไขถูกตอบสนอง และกอรูทีนอื่นๆสามารถแจ้งให้กอรูทีนอื่นต้องลบมีการทำงานของตัวเองในขณะที่เปลี่ยนแปลงเงื่อนไขโดยใช้ตัวแปรเงื่อนไข

ในไลบรารีมาตรฐานของภาษาโก ตัวแปรเงื่อนไขจะถูกนำเสนอผ่านประเภท sync.Cond และเมทอดหลักของมันประกอบด้วย:

  • Wait: เรียกใช้เมทอดนี้จะปล่อยล็อคออกและบล็อคจนกว่ากอรูทีนคนอื่นจะเรียก Signal หรือ Broadcast บนตัวแปรเงื่อนไขเดียวกันเพื่อปลุกมันขึ้น หลังจากนั้นมันจะพยายามที่จะเหมือนล็อคอีกครั้ง
  • Signal: ปลุกกอรูทีนอันหนึ่งที่รอคอยตามตัวแปรเงื่อนไขนี้ หากไม่มีกอรูทีนไหนรอ การเรียกใช้เมทอดนี้จะไม่มีผล
  • Broadcast: ปลุกกอรูทีนทั้งหมดที่รอตามตัวแปรเงื่อนไขนี้

ตัวแปรเงื่อนไขไม่ควรถูกคัดลอก ดังนั้นพวกมันมักจะถูกใช้เป็นฟิลด์ของตัวโครนั้นๆในโครงสร้างแบบหนึ่งๆ

2.3.2 กรณีการใช้งานของตัวแปรเงื่อนไข

นี่คือตัวอย่างการใช้งานตัวแปรเงื่อนไขที่แสดงโมเดล producer-consumer ที่เรียบง่าย:

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("Worker %d เริ่มงาน\n", id)
	time.Sleep(time.Second) // 模拟耗时操作
	fmt.Printf("Worker %d เสร็จสิ้น\n", id)
}

func main() {
	var wg sync.WaitGroup

	for i := 1; i <= 5; i++ {
		wg.Add(1) // เพิ่มค่าตัวนับก่อนเรียก goroutine
		go worker(i, &wg)
	}

	wg.Wait() // รอให้ goroutines ทั้งหมดเสร็จสิ้น
	fmt.Println("ทำงานเสร็จสิ้นทั้งหมด")
}

ในตัวอย่างนี้ ฟังก์ชัน worker จำลองการปฏิบัติงานที่เราทำ. ในฟังก์ชัน main เราเริ่ม goroutines ของ worker ห้าตัว. ก่อนที่จะเริ่มทุก goroutine เราเรียก wg.Add(1) เพื่อแจ้งให้ WaitGroup ทราบว่ามีการปฏิบัติงานใหม่. เมื่อทุกฟังก์ชัน worker เสร็จสิ้น, มันจะเรียก defer wg.Done() เพื่อแจ้งให้ WaitGroup ทราบว่างานเสร็จสิ้น. หลังจากเริ่มทุก goroutines, ฟังก์ชัน main จะ block ที่ wg.Wait() จนกว่าทุก workers จะรายงานเสร็จสิ้น.

2.5 การปฏิบัติทางแอตทอมิก (Atomic Operations) (sync/atomic)

2.5.1 แนวคิดของการปฏิบัติทางแอตทอมิก

การปฏิบัติทางแอตทอมิก (Atomic operations) หมายถึงการปฏิบัติการที่ไม่สามารถแบ่งออกได้ในการโปรแกรมที่ทำงานพร้อมกัน ซึ่งหมายความว่ามันไม่ถูกขัดจังหวะโดยการปฏิบัติการอื่นในระหว่างการทำงาน สำหรับการปฏิบัติทางแอตทอมิกสำหรับโกรูทีนท์หลายตัว การใช้การปฏิบัติการแอตทอมิกสามารถรักษาความสอดคล้องของข้อมูลและการซิงโครไนเซชันของสถานะโดยไม่จำเป็นต้องล็อค โดยการปฏิบัติทางแอตทอมิกเอง การปฏิบัติทางแอตทอมิก รับประกันความทรงจำของการทำงานที่ไม่ถูกขัดจังหวะ สำคัญของการปฏิบัติทางแอตทอมิกอยู่ที่การเป็นฐานของการสร้างระบบขั้นบัน นั้น (เช่นล็อคและตัวแปรเงื่อนไข) และบ่อยครั้งยังมีประสิทธิภาพที่ดีกว่ากลไกโลค

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)
}

ในตัวอย่างนี้, เราสร้าง 100 goroutines เพื่อจำลองการเข้าและออกของผู้เยี่ยมชม. โดยใช้ฟังก์ชัน atomic.AddInt32() เราทำให้การเพิ่มและลดของตัวนับเป็นการเป็นอะตอมิก แม้ในสถานการณ์ที่มีการจนจำนวนมาก, ซึ่งทำให้ค่า visitorCount ที่ถูกต้อง.

2.6 กลไกการซิงโครไนซ์ของช่อง

2.6.1 คุณลักษณะการซิงโครไนซ์ของช่อง

ช่อง (Channels) เป็นวิธีสำหรับ goroutines ในภาษา Go ที่ทำการสื่อสารในระดับของภาษา. ช่องให้ความสามารถในการส่งและรับข้อมูล. เมื่อ goroutine พยายามอ่านข้อมูลจากช่องและช่องไม่มีข้อมูล, มันจะ block จนกว่าจะมีข้อมูล. ในทำนองเดียวกัน, ถ้าช่องเต็ม (สำหรับช่องไม่มีการเก็บข้อมูลล่วงหน้านี้ หมายความว่ามันมีข้อมูลแล้ว), goroutine ที่พยายามส่งข้อมูลจะถูกบล็อค จนกว่าจะมีพื้นที่เพื่อเขียน. คุณสมบัตินี้ทำให้ช่องเป็นประโยชน์มากสำหรับการซิงโครไนซ์ระหว่าง goroutines.

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 นั้น มันจะมีการเรียกเพียงครั้งเดียวเท่านั้น จึงมีชื่อเรียกว่าฟังก์ชัน 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 เพื่อทำงานใน Goroutines ที่แยกกัน หากฟังก์ชั่นใดๆ คืนค่าเป็นข้อผิดพลาด g.Wait จะทันทีคืนค่าข้อผิดพลาดนั้น ทำให้เราสามารถจัดการข้อผิดพลาดได้อย่างเหมาะสมเมื่อมีข้อผิดพลาดเกิดขึ้น หากฟังก์ชั่นทั้งหมดทำงานเสร็จสมบูรณ์ g.Wait จะคืนค่า nil ที่บ่งบอกว่างานทุกตัวได้ทำงานเสร็จสมบูรณ์

คุณสมบัติอีกอย่างที่สำคัญของ ErrGroup คือหาก Goroutines ใดๆ มีการ panic ErrGroup จะพยายามกู้คืนการ panic นี้ และคืนค่าเป็นข้อผิดพลาด ซึ่งช่วยป้องกัน Goroutines อื่นๆ ที่ทำงานพร้อมกันไม่ต้องล้มเหลวในการปิดการทำงานอย่างสงบ เสมือนกัน หากคุณต้องการให้งานตอบสนองกับสัญญาณยกเลิกภายนอก คุณสามารถผสมฟังก์ชั่น WithContext ของ errgroup กับแพ็กเกจ context เพื่อให้การตอบสนองต่อบริบาลนั้นสามารถยกเลิกได้

ด้วยวิธีนี้ ErrGroup กลายเป็นกลไกการประสานและการจัดการข้อผิดพลาดที่มีประโยชน์มากในการปฏิบัติการของโปรแกรมที่ใช้การทำงานพร้อมกันใน Go อย่างแท้จริง