errGroup in GoLang explained

  sonic0002        2023-05-27 23:58:20       7,215        0          English  简体中文  Tiếng Việt 

Hôm nay, chúng ta hãy cùng thảo luận về cách sử dụng và các trường hợp áp dụng của errGroup, một gói thường được sử dụng. Tại sao nó hữu ích? Nhìn chung, khi chúng ta sử dụng goroutine, chúng ta không thể trả về giá trị. Nếu bạn muốn truyền ra kết quả thực thi goroutine, bạn thường phải sử dụng kênh. Gói errGroup phù hợp nếu bạn muốn biết khi nào goroutine bạn mở gặp lỗi trong quá trình thực thi và ngừng hoạt động, và tôi cần biết giá trị lỗi.

Cách sử dụng errGroup

Gói cần được tải xuống và cài đặt trước. 

go get -u golang.org/x/sync

Một ví dụ về cách sử dụng nó sẽ là

package main

import (
    "fmt"
    "log"
    "net/http"

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

func main() {
    eg := errgroup.Group{}
    eg.Go(func() error {
        return getPage("https://blog.kennycoder.io")
    })
    eg.Go(func() error {
        return getPage("https://google.com")
    })
    if err := eg.Wait(); err != nil {
        log.Fatalf("get error: %v", err)
    }
}

func getPage(url string) error {
    resp, err := http.Get(url)
    if err != nil {
        return err
    }
    if resp.StatusCode != http.StatusOK {
        return fmt.Errorf("fail to get page: %s, wrong statusCode: %d", url, resp.StatusCode)
    }
    log.Printf("success get page %s", url)
    return nil
}
  • Đầu tiên, tạo một cấu trúc nhóm. Bản thân cấu trúc này không cần thiết lập bất kỳ trường nào, vì vậy chỉ cần tạo một cấu trúc trống là đủ.
  • Cấu trúc nhóm chỉ cung cấp hai hàm, GoWait, thực sự khá giống với waitGroup.
  • Hàm Go nhận một hàm làm tham số và trả về một lỗi. Hàm này thực sự là hàm mà bạn muốn thực thi trong goroutine. Trong ví dụ trên, getPage được đặt trong Go. Việc triển khai getPage rất đơn giản. Nó gọi http.Get(url) và trả về lỗi nếu statusCode không phải là 200. Nếu không, nó sẽ ghi nhật ký để cho biết rằng việc thực thi đã thành công.
  • Cuối cùng, gọi Wait có nghĩa là nó sẽ bắt đầu chặn, tương tự như phương thức Wait của waitGroup. Nó sẽ chờ tất cả các goroutine mà bạn đã mở hoàn thành thực thi trước khi thoát khỏi Wait. Một điểm khác biệt là Wait sẽ trả về một lỗi, xuất phát từ lỗi được trả về bởi một trong các goroutine của bạn.

Trong ví dụ trên, Wait() tự nó không trả về lỗi vì tất cả các trang web đều có thể truy cập bình thường. Nếu bạn thay đổi một trong các URL thành URL không tồn tại, bạn sẽ thấy hiệu ứng.

2021/10/03 11:52:23 success get page https://google.com
2021/10/03 11:52:23 get error: Get "https://kenny.example.com": dial tcp: lookup kenny.example.com: no such host
exit status 1

Nếu cả hai goroutine của bạn đều cố gắng truy cập vào một URL không tồn tại:

2021/10/03 11:53:52 get error: Get "https://kenny.example.com": dial tcp: lookup kenny.example.com: no such host
exit status 1

Bạn sẽ thấy rằng chỉ có một lỗi được in ra ở cuối vì errGroup chỉ lưu trữ lỗi từ một goroutine. Lỗi của goroutine nào được lưu trữ phụ thuộc vào goroutine nào gặp lỗi trước và sẽ được lưu trữ. Các lỗi từ các goroutine tiếp theo sẽ không được lưu trữ.

Bạn có thể cảm thấy rằng những gì bạn muốn là biết kết quả của tất cả các lỗi, và việc chỉ cung cấp cho tôi lỗi từ một goroutine là không hữu ích. Trong ví dụ trên, nó thực sự không lý tưởng vì bạn sẽ không biết URL nào không thể truy cập được. Vì vậy, cá nhân tôi nghĩ rằng trường hợp mà errGroup phù hợp là khi các tác vụ bạn muốn thực thi là giống nhau hoặc cùng loại. Ví dụ: nếu bạn có nhiều goroutine cần truy cập cùng một dịch vụ để lấy thông tin khác nhau. Do đó, khi một trong các goroutine thất bại, lý do có thể xảy ra nhất là sự cố mạng, và các goroutine khác truy cập cùng một dịch vụ cũng sẽ không thể truy cập thành công. Ngoài ra, nếu những gì bạn muốn là khi một goroutine thất bại, ngay cả khi các goroutine khác đã hoàn thành thành công, thì nó cũng không hữu ích, thì errGroup rất phù hợp.

Tuy nhiên, chỉ sử dụng cấu trúc Group trống là không hữu ích.

Khi errGroup đang chạy, ngay cả khi một trong các goroutine của bạn gặp lỗi, nó sẽ không hủy bỏ các goroutine khác. Điều này có nghĩa là các goroutine khác không thể bị hủy kịp thời, và không thể biết liệu các goroutine đã thoát đúng cách hay chưa.

errGroup đã xem xét tình huống này, vì vậy nó chọn sử dụng phương pháp ngữ cảnh để hủy bỏ các goroutine khác.

Một ví dụ

func main() {
  eg, ctx := errgroup.WithContext(context.Background())
  eg.Go(func() error {
    for i := 0; i < 10; i++ {
      select {
      case <-ctx.Done():
        log.Printf("goroutine should cancel")
        return nil
      default:
        if err := getPage("https://blog.kennycoder.io"); err != nil {
          return err
        }
        time.Sleep(1 * time.Second)
      }
    }
    return nil
  })
  eg.Go(func() error {
    for i := 0; i < 10; i++ {
      select {
      case <-ctx.Done():
        log.Printf("goroutine should cancel")
        return nil
      default:
        if err := getPage("https://google.com"); err != nil {
          return err
        }
        time.Sleep(1 * time.Second)
      }
    }
    return nil
  })
  if err := eg.Wait(); err != nil {
    log.Fatalf("get error: %v", err)
  }
}
  • errGroup cung cấp WithContext để đặt ngữ cảnh cha của bạn vào, và trả về một cấu trúc nhóm và ngữ cảnh. Ngữ cảnh này thực sự là một ngữ cảnh hủy bỏ.
  • Sau khi lấy được ngữ cảnh hủy bỏ, bạn có thể đặt nó vào hàm Go và sử dụng select <-ctx.Done() để biết có nên hủy bỏ hay không và do đó kết thúc goroutine. Trong trường hợp này, tôi truy cập URL mười lần. Nếu có lỗi một lần, tôi trả về err, và nếu tôi nhận được ctx.Done(), tôi trả về nil để kết thúc goroutine. Tôi truy cập một trong các URL không chính xác để xem hiệu ứng:
2021/10/03 12:11:40 success get page https://google.com
2021/10/03 12:11:41 goroutine should cancel
2021/10/03 12:11:41 get error: Get "https:kenny.example.com": http: no Host in request URL
exit status 1

Như bạn thấy, goroutine thứ hai đã truy cập thành công URL một lần, nhưng nó phải in ra nhật ký và kết thúc goroutine thứ hai vì nó đã nhận được ctx.Done trước khi truy cập thành công URL lần thứ hai. Cuối cùng, lỗi Wait đã in ra lỗi từ goroutine đầu tiên.

Cách tiếp cận này đảm bảo rằng khi một goroutine gặp lỗi, nó cũng thông báo cho các goroutine khác kết thúc công việc của chúng và đảm bảo rằng các goroutine có thể thoát bình thường.

errGroup hoạt động như thế nào bên trong

Hãy xem cấu trúc của nó.

type Group struct {
  cancel func()

  wg sync.WaitGroup

  errOnce sync.Once
  err     error
}

Đây là cấu trúc của cấu trúc Group, cho thấy rằng có một cancel func() được sử dụng cho WithContext đã đề cập trước đó. Sự xuất hiện của WaitGroup cho thấy rằng errGroup cũng được triển khai thông qua WaitGroup, và Once là để chỉ chấp nhận một lỗi. err là giá trị lỗi được trả về bởi Wait ở cuối.

Dưới đây là hàm Go

func (g *Group) Go(f func() error) {
  g.wg.Add(1)

  go func() {
    defer g.wg.Done()

    if err := f(); err != nil {
      g.errOnce.Do(func() {
        g.err = err
        if g.cancel != nil {
          g.cancel()
        }
      })
    }
  }()
}

Thông qua wg.Add(1), một goroutine được mở bên trong để thực thi hàm được truyền vào, và giá trị trả về được kiểm tra lỗi. Nếu có lỗi, errOnce.Do được sử dụng để lưu trữ lỗi. Bởi vì tính năng của Once là bất kể bạn truyền vào hàm nào, Once sẽ chỉ thực thi một lần, ngay cả khi nhiều goroutine trả về lỗi. Do đó, g.err = err sẽ chỉ được đặt một lần, và giá trị của nó sẽ không thay đổi một khi nó được đặt thành công.

Cuối cùng, nó cũng sẽ kiểm tra xem cancel có bằng nil hay không. Nếu nó không bằng nil, điều đó có nghĩa là gì? Điều đó có nghĩa là errGroup được tạo bằng WithContext, vì vậy cancel sẽ không bằng nil. Tại thời điểm này, g.cancel() sẽ được gọi để hủy bỏ các goroutine khác cho bạn, và ctx.Done() sẽ có giá trị.

Và vì nó được triển khai thông qua WaitGroup, g.wg.Done() cần được gọi sau khi mỗi goroutine hoàn thành công việc của nó để giảm nó.

Dưới đây là hàm Wait:

func (g *Group) Wait() error {
  g.wg.Wait()
  if g.cancel != nil {
    g.cancel()
  }
  return g.err
}

Tương tự, g.wg.Wait() được gọi, điều này gây ra hiệu ứng chặn. Sau khi tất cả các goroutine đã hoàn thành, nó sẽ kiểm tra xem cancel có bằng nil hay không và hủy bỏ các goroutine khác theo cùng một cách. Lý do để làm điều này là hướng tới việc hủy bỏ ngữ cảnh này hơn. Sau cùng, ngữ cảnh cancel này tồn tại cho errGroup này, và nó nên được đặt lại sau khi tất cả các tác vụ đã hoàn thành.

func WithContext(ctx context.Context) (*Group, context.Context) {
  ctx, cancel := context.WithCancel(ctx)
  return &Group{cancel: cancel}, ctx
}

Như bạn thấy, một ngữ cảnh hủy bỏ thực sự được thiết lập, và ctx được trả về cho phía client để sử dụng, và hàm cancel được lưu trữ.

Tóm lại, thiết kế của errGroup rất đơn giản, và khéo léo sử dụng WaitGroupContext để đạt được khả năng chờ tất cả các goroutine và lấy lỗi, và sử dụng Context để cho phép phía client thiết kế việc triển khai thoát goroutine.

CONCURRENCY  GOLANG  ERRGROUP  WAITHROUP 

       

  RELATED


  0 COMMENT


No comment for this article.



  RANDOM FUN

Oh, No. Something unexpected happens