go-githubはGoogle公式のGitHub REST APIクライアントで、GitHub Actionsやgh CLIなど多くのツールが内部で使っています。星(Star)機能を扱うこのファイルを選んだ理由は、「HTTPリクエストを組み立てて投げて、レスポンスをGoの構造体に詰め替える」という、APIクライアントを書くときに何百回も繰り返すことになる定型パターンが、無駄なく1関数に収まっているからです。

コード

// ListStargazers lists people who have starred the specified repo.
//
// GitHub API docs: https://docs.github.com/rest/activity/starring?apiVersion=2022-11-28#list-stargazers
//
//meta:operation GET /repos/{owner}/{repo}/stargazers
func (s *ActivityService) ListStargazers(ctx context.Context, owner, repo string, opts *ListOptions) ([]*Stargazer, *Response, error) {
	u := fmt.Sprintf("repos/%v/%v/stargazers", owner, repo)
	u, err := addOptions(u, opts)
	if err != nil {
		return nil, nil, err
	}

	req, err := s.client.NewRequest(ctx, "GET", u, nil)
	if err != nil {
		return nil, nil, err
	}

	req.Header.Set("Accept", mediaTypeStarring)

	var stargazers []*Stargazer
	resp, err := s.client.Do(req, &stargazers)
	if err != nil {
		return nil, resp, err
	}

	return stargazers, resp, nil
}

// …略…

// IsStarred checks if a repository is starred by authenticated user.
//
// GitHub API docs: https://docs.github.com/rest/activity/starring?apiVersion=2022-11-28#check-if-a-repository-is-starred-by-the-authenticated-user
//
//meta:operation GET /user/starred/{owner}/{repo}
func (s *ActivityService) IsStarred(ctx context.Context, owner, repo string) (bool, *Response, error) {
	u := fmt.Sprintf("user/starred/%v/%v", owner, repo)
	req, err := s.client.NewRequest(ctx, "GET", u, nil)
	if err != nil {
		return false, nil, err
	}

	resp, err := s.client.Do(req, nil)
	starred, err := parseBoolResponse(err)
	return starred, resp, err
}

引用はリポジトリの実物と機械で照合しています。「// …略…」は省略した行です。

上から順に読む

func (s *ActivityService) ListStargazers(ctx context.Context, owner, repo string, opts *ListOptions) ([]*Stargazer, *Response, error) {

まず構文から。`(s *ActivityService)` は「レシーバ」と呼ばれる部分で、この関数を `ActivityService` 型のポインタに紐づくメソッドとして定義しています。つまり `client.Activity.ListStargazers(...)` のように呼び出せるということです。引数の `ctx context.Context` はGoのAPIクライアントで慣例的に第一引数に置かれるもので、タイムアウトやキャンセルを外から伝えるための箱です。`owner, repo string` のようにカンマで型を共有できるのもGoの書き方です。`opts *ListOptions` の `*` はポインタ型で、「nilも渡せる(=オプション未指定でもよい)任意引数」を表現する定番パターンです。戻り値は `([]*Stargazer, *Response, error)` の3つ組で、「結果」「HTTP応答のメタ情報」「エラー」を必ずセットで返すのがこのライブラリの設計方針です。

u := fmt.Sprintf("repos/%v/%v/stargazers", owner, repo)

`:=` は「短縮変数宣言」で、型を書かずに右辺の値から型を推論して新しい変数 `u` を作ります。`fmt.Sprintf` はC言語のprintf系に似た文字列整形関数で、`%v` はどんな型の値でもとりあえず標準的な見た目で埋め込める万能フォーマット指定子です。ここでは owner と repo を埋め込んでAPIのパス文字列(例: `repos/google/go-github/stargazers`)を組み立てています。

u, err := addOptions(u, opts)
	if err != nil {
		return nil, nil, err
	}

`addOptions` はこのファイルの外にあるヘルパー関数で、`opts` 構造体の中身(ページ番号など)を `?page=2` のようなクエリ文字列に変換してURLに付け加えます。ここで注目してほしいのは `if err != nil { return ... }` という形です。Goには例外機構がなく、エラーは戻り値の一部として返ってきます。呼び出し側は毎回「エラーが返ってきていないか」を自分でチェックする必要があり、これがGoで一番よく見る書き方です。エラーが出た場合は残りの戻り値(ここでは結果とResponse)に `nil` を詰めて即座に関数を抜けます。

req, err := s.client.NewRequest(ctx, "GET", u, nil)
	if err != nil {
		return nil, nil, err
	}

`s.client.NewRequest` は「HTTPメソッド・URL・リクエストボディ」からGoの `*http.Request` をラップしたオブジェクトを作る内部ヘルパーです。第一引数に `ctx` を渡すことで、あとでこのリクエストがキャンセルされたときに途中で処理を打ち切れるようになります。最後の引数が `nil` なのは、GETリクエストには送信するボディがないためです。ここでも同じ「作る→エラーチェック→ダメなら抜ける」というパターンが繰り返されているのがわかると思います。

req.Header.Set("Accept", mediaTypeStarring)

`req.Header` はHTTPリクエストのヘッダーを保持するマップのようなもので、`.Set(キー, 値)` で値を設定(既存があれば上書き)します。GitHub APIでは、機能によっては特別な `Accept` ヘッダー(メディアタイプ)を送らないとレスポンスの形式が変わったり404になったりすることがあり、`mediaTypeStarring` はそのための専用の文字列定数です。「このAPI呼び出しには特別な作法が必要」ということをコードの中で明示している一行です。

var stargazers []*Stargazer
	resp, err := s.client.Do(req, &stargazers)
	if err != nil {
		return nil, resp, err
	}

ここだけ `:=` ではなく `var stargazers []*Stargazer` を使っています。理由は、この後の行でこの変数の「アドレス」が必要になるからです。`&stargazers` の `&` は「このスライスが実際に格納されているメモリ番地」を意味し、`s.client.Do` はレスポンスのJSONボディを、渡されたアドレス先の変数へ直接デコードして書き込みます。つまり `stargazers` は最初は空(nilスライス)ですが、`Do` の呼び出しが成功すると中身が書き換わっているわけです。またエラー時の `return nil, resp, err` に注目してください。ここでは `resp` を `nil` にせずそのまま返しています。エラーが発生してもHTTPレスポンス自体(レート制限の残数など)は呼び出し側が見たいことがあるためです。

return stargazers, resp, nil

すべて成功したときの最終的な戻り値です。エラーがないことを明示するために第三戻り値に `nil` を入れています。Goでは「成功したらnilを返す」がエラー値の基本ルールで、呼び出し側はまず `err != nil` かどうかだけを見れば処理を分岐できます。

// …略…

実際のファイルでは、このあとに `ActivityListStarredOptions` という設定用の構造体と `ListStarred`(ユーザーがスターしたリポジトリ一覧を取る関数)が挟まっていますが、今回は解説の主題である「2つのエラーハンドリングパターンの対比」に絞るため中略しています。省略した部分もほぼ同じ構造(URL組み立て→リクエスト作成→送信)の繰り返しです。

func (s *ActivityService) IsStarred(ctx context.Context, owner, repo string) (bool, *Response, error) {

こちらは戻り値の形が `(bool, *Response, error)` になっている点に注目してください。「一覧を返す」関数は先頭の戻り値がスライスでしたが、「はい/いいえを判定する」関数では `bool` になります。同じサービスの中でも「何を返す関数か」によって戻り値の型を素直に変える、というシンプルな設計方針が見えます。

resp, err := s.client.Do(req, nil)
	starred, err := parseBoolResponse(err)
	return starred, resp, err

`s.client.Do(req, nil)` の第二引数が `nil` なのは、このAPIがJSONボディを返さないからです(GitHubの「スター済みか確認する」APIは、スター済みなら204 No Content、そうでなければ404 Not Foundという、ステータスコードだけで答えを表現する設計になっています)。面白いのは次の行 `starred, err := parseBoolResponse(err)` です。ここでは新しい変数 `starred` と、直前で使ったのと同じ名前の `err` を左辺に置いていますが、Goでは `:=` の左辺に「少なくとも1つ新しい変数」が含まれていれば、既存の変数(ここでは `err`)への再代入も一緒に書けるというルールがあります。`parseBoolResponse` は「404ならfalseでエラーなし、それ以外のエラーはそのまま返す」といった変換をする別ファイルのヘルパーで、HTTPのステータスコードという情報を `bool` という素直な型に変換する橋渡し役を担っています。

← 2026.09.14 の号を読む