前回、GitHub統計ツール「ghstat」を作りながらGitHub APIの基本を学びました。今回は、その続編としてもっと深くGitHub APIを掘り下げます。
実際に遭遇した問題、その解決策、そして「これ知ってたら最初から楽だったのに...」というTipsを惜しみなくシェアします。
GitHub API v3 vs GraphQL API
GitHub APIには2つのバージョンがあります:
REST API (v3) - 今回使ったやつ
// シンプル!分かりやすい!
repos, resp, err := client.Repositories.List(ctx, "", nil)
メリット:
- 直感的で分かりやすい
- ライブラリが充実
- ドキュメントが豊富
デメリット:
- データの取得に複数リクエストが必要
- Over-fetching(不要なデータも取得)
- レート制限が厳しい(5000リクエスト/時)
GraphQL API (v4) - 新しいやつ
query {
viewer {
repositories(first: 100) {
nodes {
name
stargazerCount
languages(first: 10) {
edges {
node {
name
}
}
}
}
}
}
}
メリット:
- 1回のリクエストで必要なデータすべて取得
- レート制限が緩い(ポイント制)
- Under-fetching/Over-fetching がない
デメリット:
- 学習コストが高い
- クエリの設計が難しい
結論:
- REST API - シンプルなツールならこれで十分
- GraphQL API - 大規模アプリや複雑なデータ取得には最適
ghstatは最初REST APIで作りましたが、パフォーマンスが問題になったらGraphQLに移行する予定です。
レート制限との正しい付き合い方
これが一番重要。GitHub APIには厳しいレート制限があります。
レート制限の種類
| 認証方法 | 制限 |
|---|---|
| 認証なし | 60リクエスト/時 |
| Personal Access Token | 5,000リクエスト/時 |
| GitHub App | 15,000リクエスト/時 |
| GraphQL API | 5,000ポイント/時 |
リアルタイムで制限を確認
func (c *Client) GetRateLimit() (*RateLimitInfo, error) {
rate, _, err := c.client.RateLimits(c.ctx)
if err != nil {
return nil, err
}
return &RateLimitInfo{
Limit: rate.Core.Limit,
Remaining: rate.Core.Remaining,
Reset: rate.Core.Reset.Time,
}, nil
}
// 使用例
func (c *Client) safeAPICall() error {
info, _ := c.GetRateLimit()
if info.Remaining < 100 {
waitTime := time.Until(info.Reset)
log.Printf("⚠️ API limit low (%d remaining), waiting %v",
info.Remaining, waitTime)
time.Sleep(waitTime)
}
// APIコール実行
return c.actualAPICall()
}
レスポンスヘッダーからも取得できる
APIレスポンスには必ずレート制限情報が含まれています:
repos, resp, err := client.Repositories.List(ctx, "", nil)
// レスポンスヘッダーから取得
remaining := resp.Header.Get("X-RateLimit-Remaining")
reset := resp.Header.Get("X-RateLimit-Reset")
log.Printf("Remaining: %s, Reset: %s", remaining, reset)
実践Tips:
- 早めにチェック - 残り100リクエストで警告
- リセット時刻を待つ - 無駄な失敗を避ける
- キャッシュを活用 - 同じデータは再取得しない
ページネーション完全ガイド
GitHubは大量データをページングで返します。正しく扱わないと、データが取れません。
基本パターン
func (c *Client) ListAllRepositories() ([]*github.Repository, error) {
opt := &github.RepositoryListOptions{
ListOptions: github.ListOptions{
PerPage: 100, // 最大100件/ページ
},
Sort: "updated",
Direction: "desc",
}
var allRepos []*github.Repository
for {
repos, resp, err := c.client.Repositories.List(c.ctx, "", opt)
if err != nil {
return nil, fmt.Errorf("failed to list repos: %w", err)
}
allRepos = append(allRepos, repos...)
// 次ページがなければ終了
if resp.NextPage == 0 {
break
}
opt.Page = resp.NextPage
}
return allRepos, nil
}
プログレスバー付きページネーション
ユーザーに進捗を見せると親切:
func (c *Client) ListAllRepositoriesWithProgress() ([]*github.Repository, error) {
// まず総数を取得
user, _, _ := c.client.Users.Get(c.ctx, "")
totalRepos := user.GetPublicRepos() + user.GetTotalPrivateRepos()
// プログレスバー作成
bar := progressbar.NewOptions(totalRepos,
progressbar.OptionSetDescription("Fetching repositories..."),
)
opt := &github.RepositoryListOptions{
ListOptions: github.ListOptions{PerPage: 100},
}
var allRepos []*github.Repository
for {
repos, resp, err := c.client.Repositories.List(c.ctx, "", opt)
if err != nil {
return nil, err
}
allRepos = append(allRepos, repos...)
bar.Add(len(repos)) // 進捗更新
if resp.NextPage == 0 {
break
}
opt.Page = resp.NextPage
}
return allRepos, nil
}
並列ページネーション(上級者向け)
複数リポジトリのコミット取得を並列化:
func (c *Client) GetCommitsParallel(repos []*Repository) map[string][]*Commit {
results := make(map[string][]*Commit)
var mu sync.Mutex
var wg sync.WaitGroup
// セマフォでAPI制限を守る
sem := make(chan struct{}, 10) // 同時10リクエストまで
for _, repo := range repos {
wg.Add(1)
go func(r *Repository) {
defer wg.Done()
sem <- struct{}{} // セマフォ取得
defer func() { <-sem }() // セマフォ解放
commits, err := c.getRepoCommits(r.Name)
if err != nil {
return
}
mu.Lock()
results[r.Name] = commits
mu.Unlock()
}(repo)
}
wg.Wait()
return results
}
注意点:
- セマフォで同時リクエスト数を制限
- Mutexで結果マップを保護
- エラーは無視(一部失敗しても続行)
エラーハンドリングの極意
GitHub APIのエラーは種類が多い。適切に処理しないと、ユーザーが困ります。
エラーの種類と対処法
func handleGitHubError(err error) error {
if err == nil {
return nil
}
// GitHub APIエラーの型アサーション
if ghErr, ok := err.(*github.ErrorResponse); ok {
switch ghErr.Response.StatusCode {
case 401:
return fmt.Errorf("❌ 認証エラー: トークンが無効です")
case 403:
if ghErr.Message == "API rate limit exceeded" {
return fmt.Errorf("❌ API制限超過: %v にリセットされます",
ghErr.Rate.Reset.Time.Format("15:04:05"))
}
return fmt.Errorf("❌ アクセス拒否: %s", ghErr.Message)
case 404:
return fmt.Errorf("❌ リソースが見つかりません")
case 422:
return fmt.Errorf("❌ バリデーションエラー: %s", ghErr.Message)
default:
return fmt.Errorf("❌ APIエラー (%d): %s",
ghErr.Response.StatusCode, ghErr.Message)
}
}
// ネットワークエラー
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
return fmt.Errorf("❌ タイムアウト: ネットワークが遅いか、不安定です")
}
// その他のエラー
return fmt.Errorf("❌ 予期しないエラー: %w", err)
}
// 使用例
repos, _, err := client.Repositories.List(ctx, "", nil)
if err != nil {
return handleGitHubError(err)
}
リトライロジック
一時的なエラーは自動リトライ:
func (c *Client) callWithRetry(fn func() error) error {
maxRetries := 3
backoff := time.Second
for i := 0; i < maxRetries; i++ {
err := fn()
if err == nil {
return nil
}
// リトライ可能なエラーかチェック
if !isRetryable(err) {
return err
}
if i < maxRetries-1 {
log.Printf("⚠️ リトライ %d/%d (待機: %v)",
i+1, maxRetries, backoff)
time.Sleep(backoff)
backoff *= 2 // Exponential backoff
}
}
return fmt.Errorf("最大リトライ回数を超えました")
}
func isRetryable(err error) bool {
if ghErr, ok := err.(*github.ErrorResponse); ok {
// 5xx エラーはリトライ可能
return ghErr.Response.StatusCode >= 500
}
// ネットワークエラーもリトライ可能
if netErr, ok := err.(net.Error); ok {
return netErr.Timeout() || netErr.Temporary()
}
return false
}
認証トークンのベストプラクティス
トークンの扱いは超重要。間違えると大惨事です。
1. トークンは絶対にハードコードしない
// ❌ 絶対ダメ!
const GITHUB_TOKEN = "ghp_xxxxxxxxxxxxxxxxxxxx"
// ✅ 環境変数から
token := os.Getenv("GITHUB_TOKEN")
// ✅ または設定ファイルから(.gitignore必須)
cfg, _ := config.Load()
token := cfg.GitHubToken
2. トークンのスコープは最小限に
Personal Access Tokenを作るとき、必要な権限だけ付与:
ghstatに必要なスコープ:
✅ repo (プライベートリポジトリアクセス用)
✅ read:user (ユーザー情報取得用)
❌ admin:org (不要!)
❌ delete_repo (危険!)
3. トークンの有効期限を設定
# トークン作成時
Expiration: 90 days # 推奨
4. 複数トークンの切り替え
type Client struct {
client *github.Client
ctx context.Context
}
// トークンを切り替えて新しいクライアント作成
func NewClientWithToken(token string) (*Client, error) {
if token == "" {
return nil, errors.New("token required")
}
ctx := context.Background()
ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
tc := oauth2.NewClient(ctx, ts)
return &Client{
client: github.NewClient(tc),
ctx: ctx,
}, nil
}
// 使用例:複数アカウントの統計を取得
func compareAccounts(token1, token2 string) {
client1, _ := NewClientWithToken(token1)
client2, _ := NewClientWithToken(token2)
stats1 := client1.GetStats()
stats2 := client2.GetStats()
// 比較処理...
}
キャッシュ戦略
API呼び出しを減らす一番効果的な方法はキャッシュです。
1. メモリキャッシュ(シンプル)
type CachedClient struct {
client *Client
cache map[string]interface{}
mutex sync.RWMutex
ttl time.Duration
}
func NewCachedClient(client *Client) *CachedClient {
return &CachedClient{
client: client,
cache: make(map[string]interface{}),
ttl: 5 * time.Minute, // 5分間キャッシュ
}
}
func (c *CachedClient) GetRepositories() ([]*Repository, error) {
key := "repos"
// キャッシュチェック
c.mutex.RLock()
if cached, ok := c.cache[key]; ok {
c.mutex.RUnlock()
return cached.([]*Repository), nil
}
c.mutex.RUnlock()
// キャッシュミス:APIコール
repos, err := c.client.ListRepositories()
if err != nil {
return nil, err
}
// キャッシュに保存
c.mutex.Lock()
c.cache[key] = repos
c.mutex.Unlock()
// TTL後に削除
time.AfterFunc(c.ttl, func() {
c.mutex.Lock()
delete(c.cache, key)
c.mutex.Unlock()
})
return repos, nil
}
2. ファイルキャッシュ(永続化)
type FileCache struct {
dir string
}
func NewFileCache() *FileCache {
home, _ := os.UserHomeDir()
dir := filepath.Join(home, ".cache", "ghstat")
os.MkdirAll(dir, 0755)
return &FileCache{dir: dir}
}
func (c *FileCache) Get(key string, v interface{}) error {
path := filepath.Join(c.dir, key+".json")
// ファイル存在チェック
info, err := os.Stat(path)
if err != nil {
return err
}
// 24時間以上古いキャッシュは無効
if time.Since(info.ModTime()) > 24*time.Hour {
return errors.New("cache expired")
}
data, err := os.ReadFile(path)
if err != nil {
return err
}
return json.Unmarshal(data, v)
}
func (c *FileCache) Set(key string, v interface{}) error {
path := filepath.Join(c.dir, key+".json")
data, err := json.MarshalIndent(v, "", " ")
if err != nil {
return err
}
return os.WriteFile(path, data, 0644)
}
// 使用例
cache := NewFileCache()
// キャッシュから取得
var repos []*Repository
if err := cache.Get("repos", &repos); err != nil {
// キャッシュミス:APIから取得
repos, _ = client.ListRepositories()
cache.Set("repos", repos)
}
3. 条件付きリクエスト(ETag)
GitHubはETagをサポートしています:
type ConditionalClient struct {
client *Client
etags map[string]string
}
func (c *ConditionalClient) GetReposWithETag() ([]*Repository, error) {
req, _ := c.client.client.NewRequest("GET", "user/repos", nil)
// 前回のETagを設定
if etag, ok := c.etags["repos"]; ok {
req.Header.Set("If-None-Match", etag)
}
var repos []*Repository
resp, err := c.client.client.Do(c.client.ctx, req, &repos)
if err != nil {
return nil, err
}
// 304 Not Modified = キャッシュが有効
if resp.StatusCode == http.StatusNotModified {
return nil, errors.New("use cached data")
}
// 新しいETagを保存
c.etags["repos"] = resp.Header.Get("ETag")
return repos, nil
}
実践的なパフォーマンス最適化
ghstatで実際にやったパフォーマンス改善:
Before(遅い)
func GetCommitStats(repos []*Repository) *Stats {
stats := &Stats{}
// 直列処理:100リポジトリで5分
for _, repo := range repos {
commits := getRepoCommits(repo)
stats.Add(commits)
}
return stats
}
問題点:
- 直列処理(1つずつ)
- ページネーションが非効率
- キャッシュなし
After(速い)
func GetCommitStats(repos []*Repository) *Stats {
stats := &Stats{}
var mu sync.Mutex
// ワーカープール
workers := 20
jobs := make(chan *Repository, len(repos))
var wg sync.WaitGroup
// ワーカー起動
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for repo := range jobs {
// キャッシュチェック
if cached := cache.Get(repo.Name); cached != nil {
mu.Lock()
stats.Add(cached)
mu.Unlock()
continue
}
commits := getRepoCommits(repo)
cache.Set(repo.Name, commits)
mu.Lock()
stats.Add(commits)
mu.Unlock()
}
}()
}
// ジョブ投入
for _, repo := range repos {
jobs <- repo
}
close(jobs)
wg.Wait()
return stats
}
改善点:
- 並列処理(20ワーカー)
- キャッシュ活用
- 効率的なページネーション
結果: 5分 → 15秒(20倍高速化!)
セキュリティチェックリスト
本番環境に出す前に確認:
✅ トークン管理
- トークンはハードコードしていない
- 環境変数または設定ファイルで管理
-
設定ファイルは
.gitignoreに追加 - 必要最小限のスコープのみ付与
- トークンに有効期限を設定
✅ エラーハンドリング
- トークンがエラーメッセージに含まれない
- APIエラーを適切に処理
- ネットワークエラーに対応
- リトライロジックを実装
✅ レート制限
- レート制限をチェック
- 制限間近で警告表示
- リセット時刻を待つ機能
- キャッシュで呼び出し削減
✅ データ保護
- ローカルキャッシュの権限設定(0600 or 0644)
- センシティブなデータは暗号化
- ログにトークンを出力しない
デバッグテクニック
API呼び出しをデバッグするのは大変。便利なテクニック:
1. リクエスト/レスポンスのログ
type DebugTransport struct {
Transport http.RoundTripper
}
func (t *DebugTransport) RoundTrip(req *http.Request) (*http.Response, error) {
// リクエストをログ
fmt.Printf("→ %s %s\n", req.Method, req.URL.Path)
// ヘッダーをログ
for k, v := range req.Header {
if k == "Authorization" {
fmt.Printf(" %s: ***REDACTED***\n", k)
} else {
fmt.Printf(" %s: %v\n", k, v)
}
}
// 実際のリクエスト実行
resp, err := t.Transport.RoundTrip(req)
// レスポンスをログ
if resp != nil {
fmt.Printf("← %d %s\n", resp.StatusCode, resp.Status)
fmt.Printf(" X-RateLimit-Remaining: %s\n",
resp.Header.Get("X-RateLimit-Remaining"))
}
return resp, err
}
// 使用例
func NewDebugClient() *github.Client {
httpClient := &http.Client{
Transport: &DebugTransport{
Transport: http.DefaultTransport,
},
}
return github.NewClient(httpClient)
}
2. APIコール数のカウント
type CountingClient struct {
client *Client
callCount map[string]int
mutex sync.Mutex
}
func (c *CountingClient) GetRepositories() ([]*Repository, error) {
c.mutex.Lock()
c.callCount["ListRepos"]++
c.mutex.Unlock()
return c.client.ListRepositories()
}
func (c *CountingClient) PrintStats() {
fmt.Println("API Call Statistics:")
for endpoint, count := range c.callCount {
fmt.Printf(" %s: %d calls\n", endpoint, count)
}
}
3. モックテスト
type MockGitHubClient struct {
repos []*Repository
err error
}
func (m *MockGitHubClient) ListRepositories() ([]*Repository, error) {
return m.repos, m.err
}
// テスト
func TestGetStats(t *testing.T) {
mock := &MockGitHubClient{
repos: []*Repository{
{Name: "test-repo", Stars: 100},
},
}
stats := GetStats(mock)
if stats.TotalRepos != 1 {
t.Errorf("Expected 1 repo, got %d", stats.TotalRepos)
}
}
まとめ
GitHub APIを使いこなすための実践テクニックを紹介しました:
基本:
- ✅ REST API vs GraphQL API の使い分け
- ✅ レート制限の正しい扱い方
- ✅ ページネーションの実装
応用:
- ✅ エラーハンドリングとリトライ
- ✅ 並列処理によるパフォーマンス向上
- ✅ キャッシュ戦略
セキュリティ:
- ✅ トークン管理のベストプラクティス
- ✅ セキュリティチェックリスト
デバッグ:
- ✅ リクエスト/レスポンスのログ
- ✅ モックテスト
これらのテクニックを使えば、堅牢で高速なGitHub連携ツールが作れます。
次は、これらを活かしてもっと複雑な統計機能やチーム向け機能を追加する予定です。
それでは、Happy Coding!