上一期主要再解釋怎麼設計一個運用 context 管理的 goroutine request ,今天比較簡單,分成兩個部分,一個是說明 github 的 api ,另一個部分是上次的 httpDo 後來在研究 concurrency 的問題時,發現有一個 bug ,在這邊修正一下。

  • Read source file to find git repositories.
  • Using GitHub api to fetch repository information, such like description, starts, issues.
  • Make simple goroutine worker pool to fetch mass repositories.
  • Store above data to DB.
  • Set up cron job to update information.
  • provide api or other to show those data

Using GitHub api to fetch repository information, such like description, starts, issues.

GitHub API 的文件在這邊,內容說明得很清楚,主要是使用下面這隻 API

GET /repos/{owner}/{repo}

比較值得注意的地方是,因為我們的 repositories 有兩千多個,根據 GitHub API 的限制,沒有 token 的 request 一小時只能請求 60 個,所以請記得去申請 personal token 來使用,有 token 的話,一小時有 5000 個請求額度,應該是很夠用了,不過記得測試 code 的時候一次跑一些就好,不然一下子用掉 2000+ ,5000 很快就見底了。

想要知道目前的 ip 還能用幾個,可以打這支 API 看看,有帶 token 跟沒帶是不同的,可以試看看

https://api.github.com/rate_limit

GET repo 的工作相對簡單,建立 request 並填上需要的參數,再呼叫 HttpDo 後,讀取 response 處理後回傳即可:

	req, err := http.NewRequest("GET", GITHUB_API_URL+"repos/"+userRepo, nil)
	req.Header.Set("Accept", "application/vnd.github.v3+json")
	req.Header.Set("Authorization", "YOUR_TOKEN")
	if err != nil {
		return Repository{}, err
	}
	repo := Repository{}
	err = service.HttpDo(ctx, req, func(resp *http.Response, err error) error {
		if err != nil {
			return err
		}
		defer resp.Body.Close()
		body, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			return err
		}

		err = json.Unmarshal(body, &repo)
		if err != nil {
			return err
		}
		return nil
	})
	return repo, err

看到這邊我突然想起之前有看到 Dave Cheney 寫過一篇文章提到原生套件 encoding/json 的 decoding 太慢的問題,之後也許有機會可做看看實驗。

最後想提一個 concurrency blocking bug

func HttpDo(ctx context.Context, req *http.Request, f func(*http.Response, error) error) error {
	// to more safety, give 1 buffer
	c := make(chan error, 1)
	defer close(c)
	req = req.WithContext(ctx)
	// run http request on goroutine, and pass receive values to f
	go func() {
		c <- f(http.DefaultClient.Do(req))
	}()

	select {
	case <-ctx.Done():
		// context is end, but need waiting f() to return
		// otherwise running f() will send to close channel
		<-c
		return ctx.Err()
	case err := <-c:
		// normal case, receive from f()
		return err
	}
}

c := make(chan error, 1) 之前有提到是不需要 buffer 的,因為我們有在 ctx timeout 的 case 中讀取

這麼做的原因是因為如果當 c <- f(http.DefaultClient.Do(req)) & <-ctx.Done() 同時滿足條件的時候,select-case 有隨機性,因此我們不知道哪一個 case 會先被執行,但如果是 <-ctx.Done() 的話,直接 return 會造成 c 這個 channel 的關閉,因此 f() 無法傳遞信號給 c ,產生了 block,因此之前的解法是在 case <-ctx.Done(): 中讀取 ←c,以避免 block 的問題。

ctx 的取消,可能是因為 timeout 或是其 cancel func 被呼叫,理論上我們希望的是立即返回錯誤,繼續執行後續的程式,我們會在裡面等 c 的返回後(只要還在等待 response 會有錯誤返回,因為 request 裏面也有 ctx)再來返回 error 。如果沒有等待 c 的返回,就一定要替 c 加上 buffer ,否則就會有很高的機率產生 panic block,為了避免有無法預期的 blocking bug,除了等待 c 的返回之外我覺得最重要的還是要替 c 加上一個 buffer ,這也提醒了我,未來在 select-case 的處理上,當無法 100% 確認可以處理 case 的 channel 的生命週期時,應該還是要加上 buffer 會比較安全一些。

Reference