The problem with raw HTTP
You need to build a tool that checks repository visibility, lists open pull requests, or fetches contributor statistics. You could craft raw HTTP requests, manually serialize JSON payloads, and parse status codes. Or you could use the official go-github library. The library gives you strongly typed methods that map directly to GitHub's REST API endpoints. You trade a few lines of boilerplate for type safety, automatic JSON mapping, and built-in pagination helpers.
How the client wraps the API
The GitHub REST API speaks JSON over HTTP. Every endpoint expects specific headers, query parameters, and request bodies. Every response returns a status code, pagination metadata, and a JSON payload. Writing a client from scratch means repeating the same serialization logic across dozens of endpoints. go-github wraps that repetition. It generates Go structs for every GitHub resource and provides methods like client.Repositories.Get() or client.Issues.List(). Think of it as a translation layer. You write idiomatic Go, the library handles the HTTP plumbing, and you get back populated structs instead of raw bytes.
The library also manages API versioning automatically. GitHub requires an Accept header to specify which version of the API you want. The client injects application/vnd.github+json by default, so your code does not break when GitHub rolls out backward-incompatible changes. You rarely need to touch headers unless you are using a beta feature.
Stop fighting HTTP status codes. Let the library translate JSON into structs.
Minimal example
Here is the simplest way to fetch your own public profile without authentication.
package main
import (
"context"
"fmt"
"log"
"github.com/google/go-github/v62/github"
)
func main() {
// Background context provides a default cancellation scope for the request.
ctx := context.Background()
// nil tells the client to make unauthenticated public requests.
client := github.NewClient(nil)
// Get returns the user, a response object for pagination/rate limits, and an error.
user, _, err := client.Users.Get(ctx, "")
if err != nil {
log.Fatal(err)
}
// GetLogin safely returns the string field without a nil pointer check.
fmt.Println(user.GetLogin())
}
What happens under the hood
When you run this, the client builds an HTTP GET request to https://api.github.com/user. The empty string for the username parameter tells the API to look up the authenticated user, but since we passed nil for the HTTP client, it falls back to the public endpoint behavior. The library serializes the request, sends it over the network, and reads the response stream. It unmarshals the JSON into a *github.User struct. The method returns three values: the populated struct, a *github.Response containing headers and pagination state, and an error if anything fails.
Go forces you to handle all three. If you ignore the response object, you lose access to rate limit headers and the NextPage cursor. The compiler rejects the program with assignment mismatch: 3 variables but client.Users.Get returns 3 values if you try to capture fewer. The three-return pattern is intentional. It forces you to acknowledge that network calls can fail, that pagination exists, and that rate limits apply. Convention aside: context.Context always goes as the first parameter in Go functions that perform I/O. The library follows this rule strictly. Pass a context with a timeout or cancellation channel when calling long-running endpoints, and the client will abort the HTTP request gracefully instead of hanging.
Never ignore the response object. Rate limits and pagination live there.
Realistic example with pagination and auth
Public endpoints have strict rate limits and cannot access private data. Real applications need authentication. The standard approach uses a personal access token or an OAuth2 token. You wrap the token in an http.Client and pass it to github.NewClient. List endpoints also require pagination handling. GitHub limits list endpoints to 30 items per page. You control this with the ListOptions struct.
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/google/go-github/v62/github"
"golang.org/x/oauth2"
)
func main() {
// Read token from environment to avoid hardcoding secrets.
token := os.Getenv("GITHUB_TOKEN")
if token == "" {
log.Fatal("GITHUB_TOKEN environment variable is required")
}
// oauth2.NewClient creates an HTTP client that attaches the Bearer token to every request.
httpClient := oauth2.NewClient(context.Background(), oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: token},
))
// Pass the authenticated HTTP client to the GitHub wrapper.
client := github.NewClient(httpClient)
ctx := context.Background()
// ListOptions controls page size and cursor for paginated endpoints.
opts := &github.ListOptions{PerPage: 100}
// Loop until NextPage is zero, which signals the final page.
for {
repos, resp, err := client.Repositories.List(ctx, "", opts)
if err != nil {
log.Fatalf("failed to fetch repos: %v", err)
}
for _, r := range repos {
fmt.Println(r.GetName())
}
// Break when the API returns no more pages.
if resp.NextPage == 0 {
break
}
opts.Page = resp.NextPage
}
}
The ListOptions struct follows a common Go pattern: configuration structs are passed by pointer to avoid copying, and zero values represent sensible defaults. The PerPage field caps at 100 because GitHub enforces a hard limit. Setting it higher does not speed up your code. It just triggers a 400 error. The resp.NextPage field is an integer. Zero means you have reached the end. Non-zero means you should update opts.Page and call the method again. This explicit loop replaces the hidden iterator patterns you might see in other languages. Go prefers visible control flow.
Convention aside: if err != nil { return err } is verbose by design. The community accepts the boilerplate because it makes the unhappy path visible. Do not swallow errors with empty blocks. Log them, wrap them with fmt.Errorf("fetching repos: %w", err), or return them up the call stack. Error wrapping preserves the stack trace and lets callers inspect the root cause.
Trust the explicit loop. Hidden iterators hide bugs.
Pitfalls and compiler/runtime errors
The three-return pattern trips up beginners. You will see result, resp, err := client.X.Get(...) everywhere. Ignoring resp by writing _, _, err := ... works syntactically but hides rate limit data. When GitHub throttles you, the API returns a 403 status. The library converts that into an error, but you can also check resp.Rate.Remaining before the call fails. If you forget to import the OAuth2 package, the compiler rejects the program with undefined: oauth2. If you pass a string where a struct is expected, you get cannot use "value" (untyped string constant) as *github.Repository value in argument.
Pagination is another common trap. Failing to check the next page cursor means your tool silently drops results after the first thirty. Goroutine leaks also happen here if you spawn a background worker to poll the API but forget to cancel its context when the main program exits. Always tie API polling to a cancellable context. The worst goroutine bug is the one that never logs.
Rate limit headers are not optional. GitHub returns X-RateLimit-Remaining and X-RateLimit-Reset on every response. The library parses them into resp.Rate.Remaining and resp.Rate.Reset. When Remaining hits zero, subsequent calls fail immediately. You should implement exponential backoff or respect the Reset timestamp. Ignoring rate limits turns your tool into a noisy neighbor that gets banned.
Convention aside: _ (underscore) discards a value intentionally. result, _ := ... says "I considered the second return value and chose to drop it". Use it sparingly with errors. Dropping errors with _ is a fast track to silent data corruption. The compiler will not stop you, but your users will notice.
Read the rate limit headers. Back off before you get blocked.
Decision matrix
Use go-github when you need type-safe access to the GitHub REST API and want automatic JSON unmarshaling. Use the raw net/http client when you only need a single endpoint and want to avoid pulling in a large dependency tree. Use the GitHub GraphQL client when you need to fetch deeply nested relationships in a single request without over-fetching fields. Use a third-party wrapper only when you need legacy API versions that the official library no longer supports. Use plain sequential code when you don't need concurrency: the simplest thing that works is usually the right thing.