A small, idiomatic Go client for the pkg.go.dev API. It gives Go tooling a typed interface to module and package metadata — search, version history, package and symbol listings, importers, and vulnerability data — instead of scraping the website.
This client targets the pkg.go.dev API at https://pkg.go.dev/v1beta/. The API
is explicitly labelled v1beta: a stable v1 is planned but not yet
released, and response shapes may change before then. Pin a version of this
module and expect to update when the upstream API moves to v1.
go get github.com/pouya1364/pkgsiteRequires Go 1.23 or later (the iterator methods use iter.Seq2).
The library uses only the Go standard library. Running go get on it pulls in
no transitive dependencies.
package main
import (
"context"
"fmt"
"log"
"github.com/pouya1364/pkgsite"
)
func main() {
client := pkgsite.NewClient()
pkg, err := client.Package(context.Background(), "golang.org/x/time/rate")
if err != nil {
log.Fatal(err)
}
fmt.Println(pkg.Name, pkg.Synopsis)
}NewClient accepts options: WithHTTPClient to supply your own *http.Client
(for custom timeouts or transport), and WithBaseURL to point at a different
API host (useful in tests).
Each example assumes a client := pkgsite.NewClient() and ctx.
pkg, err := client.Package(ctx, "golang.org/x/time/rate")
// pkg.ModulePath, pkg.Version, pkg.Name, pkg.Synopsis, ...mod, err := client.Module(ctx, "golang.org/x/time")
// mod.Path, mod.Version, mod.IsLatest, mod.RepoURL, ...page, err := client.Versions(ctx, "golang.org/x/time")
for _, v := range page.Items {
fmt.Println(v.Version, "latest:", v.LatestVersion)
}page, err := client.ModulePackages(ctx, "golang.org/x/time")
for _, p := range page.Items {
fmt.Println(p.Path, p.Synopsis)
}page, err := client.Search(ctx, "rate limiter", pkgsite.SearchOptions{
Symbol: "Limiter", // optional: narrow to packages exporting this symbol
})
for _, r := range page.Items {
fmt.Println(r.PackagePath)
}page, err := client.Symbols(ctx, "golang.org/x/time/rate")
for _, s := range page.Items {
fmt.Println(s.Kind, s.Name)
}page, err := client.ImportedBy(ctx, "golang.org/x/time/rate")
for _, importPath := range page.Items { // items are import path strings
fmt.Println(importPath)
}page, err := client.Vulnerabilities(ctx, "github.com/dgrijalva/jwt-go")
for _, v := range page.Items {
fmt.Println(v.ID, v.Details, "fixed in:", v.FixedVersion)
}Paginated endpoints return a Page[T]:
type Page[T any] struct {
Items []T
Total int
NextPageToken string
}
func (p Page[T]) HasMore() bool // true when another page existsYou can page manually with the Token option:
page, _ := client.Search(ctx, "bloom filter")
for page.HasMore() {
page, _ = client.Search(ctx, "bloom filter", pkgsite.SearchOptions{
ListOptions: pkgsite.ListOptions{Token: page.NextPageToken},
})
}Or let the iterator handle paging for you. Every paginated endpoint has an
…Iter variant returning a Go 1.23 iter.Seq2[T, error], so you can range
over all results across all pages:
for result, err := range client.SearchIter(ctx, "bloom filter") {
if err != nil {
log.Fatal(err)
}
fmt.Println(result.PackagePath)
}The iterators are VersionsIter, ModulePackagesIter, SearchIter,
SymbolsIter, and ImportedByIter. Breaking out of the loop stops cleanly
without fetching further pages.
The paginated list options accept a Filter — a Go expression the API
evaluates against each item. The library URL-encodes it for you:
page, err := client.ModulePackages(ctx, "golang.org/x/tools", pkgsite.ListOptions{
Filter: `contains(path,"internal")`,
})Failures come back as typed errors you can inspect with errors.As:
_, err := client.Package(ctx, "encoding/json/v2")
switch {
case err == nil:
// ok
default:
var ambiguous *pkgsite.ErrAmbiguousPath
var rateLimited *pkgsite.ErrRateLimit
var apiErr *pkgsite.APIError
switch {
case errors.As(err, &ambiguous):
// The path matches more than one module. Retry with the Module option
// set to one of ambiguous.Candidates.
_, _ = client.Package(ctx, "encoding/json/v2", pkgsite.PackageOptions{
Module: ambiguous.Candidates[0],
})
case errors.As(err, &rateLimited):
// HTTP 429. rateLimited.RetryAfter is the Unix timestamp when the
// limit resets. The API allows 40 requests/sec per IP.
case errors.As(err, &apiErr):
// Any other non-2xx response. apiErr.Code, apiErr.Message, apiErr.Fixes.
}
}pkgsite.Client is a concrete type. To test your own code without real HTTP
calls, declare an interface in your package listing the methods you use, accept
that interface, and substitute *mock.Client in tests. Both *pkgsite.Client
and *mock.Client satisfy such an interface.
package myapp
import (
"context"
"github.com/pouya1364/pkgsite"
)
// The slice of the API this code depends on.
type PackageGetter interface {
Package(ctx context.Context, path string, opts ...pkgsite.PackageOptions) (*pkgsite.PackageInfo, error)
}
func Synopsis(ctx context.Context, c PackageGetter, path string) (string, error) {
pkg, err := c.Package(ctx, path)
if err != nil {
return "", err
}
return pkg.Synopsis, nil
}package myapp_test
import (
"context"
"testing"
"github.com/pouya1364/pkgsite"
"github.com/pouya1364/pkgsite/mock"
)
func TestSynopsis(t *testing.T) {
c := &mock.Client{
PackageFn: func(ctx context.Context, path string, opts ...pkgsite.PackageOptions) (*pkgsite.PackageInfo, error) {
return &pkgsite.PackageInfo{Synopsis: "Package rate provides a rate limiter."}, nil
},
}
got, err := Synopsis(context.Background(), c, "golang.org/x/time/rate")
if err != nil || got != "Package rate provides a rate limiter." {
t.Fatalf("got %q, %v", got, err)
}
}Each method on mock.Client has a matching …Fn field. A method whose field
is left nil returns the zero value and a nil error.
This repository's own tests use net/http/httptest and make no network calls.
A separate set of integration tests exercises the live API; they are guarded by
the integration build tag and the PKGSITE_INTEGRATION environment variable,
so they never run by accident:
PKGSITE_INTEGRATION=1 go test -tags integration -run TestIntegration ./...