登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

pkg.go.dev API 如何按导入路径反查模块版本列表

来源:17golang原创

时间:2026-10-09 02:20:06 394浏览 收藏

如果工具手里只有 golang.org/x/time/rate 这样的包导入路径,不能直接把它塞进模块版本接口。正确做法是两段查询:先调用 /v1/package/{path} 取得 modulePath,再调用 /v1/versions/{modulePath} 分页读取版本列表。

官方 API 文档:https://pkg.go.dev/v1/api

2026 年 5 月的 API 发布博客仍以 /v1beta 介绍首批接口,而当前官方文档已经提供 /v1。新代码应以当前文档为准,并把版本前缀集中配置,避免散落在业务逻辑里。

为什么值得改用 API,而不是抓取网页

pkg.go.dev API 是无状态、只读 GET 的 JSON 接口。它把网页展示背后的包、模块、版本、符号和导入关系公开为结构化数据,适合依赖看板、IDE 集成、升级提示和内部组件目录。相比解析 HTML,字段契约更明确,也更容易缓存和处理分页。

收益最明显的角色是工具作者:输入一个 import path,就能得到所属模块、当前版本、历史标签、提交时间、撤回状态等元数据。不过 API 的设计强调“精确优先于便利”,遇到一个包路径可由多个模块提供时,它不会替调用方偷偷选择最长模块路径,而是返回候选让调用方消歧。

核心关系:导入路径不是模块路径

Go 包导入路径描述一个包目录,模块路径则来自模块根目录的 go.mod。例如 golang.org/x/time/rate 是包路径,而它所属的模块是 golang.org/x/time。versions 路由接收后者,不接收前者。

pkg.go.dev API 中导入路径、模块路径和版本集合字段的静态关系图
图1:package 接口负责确认模块归属,versions 接口针对 modulePath 返回版本集合。

先用 curl 看最小查询。package 响应里的 modulePath 就是第二个接口所需的路径。

# 按包导入路径查询元数据,并只提取模块路径
curl -fsSL "https://pkg.go.dev/v1/package/golang.org/x/time/rate" \
  | jq -r '.modulePath'

# 使用模块路径查询前三个版本,观察分页令牌
curl -fsSL "https://pkg.go.dev/v1/versions/golang.org/x/time?limit=3" \
  | jq '{items, nextPageToken}'

versions 接口默认只返回已打标签版本,并按降序排列;跨主版本也会包含在集合中,不兼容版本排在后面。若确实需要伪版本,显式添加 pseudo=true。这应是有意识的产品选择,因为伪版本数量更大,也不一定适合展示给普通升级用户。

先封装统一的 GET 与错误对象

下面的 Go 示例只使用标准库。它限制响应体大小、设置客户端超时,并在非 2xx 时保留官方返回的 message、candidates 和 fixes,便于上层决定如何消歧。

package pkgapi

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
	"time"
)

const apiPrefix = "/v1"

// Client 复用连接,并为整个请求设置超时。
type Client struct {
	http *http.Client
}

// NewClient 创建一个适合后台查询的客户端。
func NewClient() *Client {
	return &Client{http: &http.Client{Timeout: 10 * time.Second}}
}

// APIError 保存 pkg.go.dev 返回的结构化错误信息。
type APIError struct {
	Code       int      `json:"code"`
	Message    string   `json:"message"`
	Candidates []string `json:"candidates"`
	Fixes      []string `json:"fixes"`
}

func (e *APIError) Error() string {
	return fmt.Sprintf("pkg.go.dev API %d: %s", e.Code, e.Message)
}

// endpoint 让 net/url 负责路径转义,同时保留导入路径中的斜线。
func endpoint(kind, p string) string {
	u := url.URL{
		Scheme: "https",
		Host:   "pkg.go.dev",
		Path:   apiPrefix + "/" + kind + "/" + strings.TrimPrefix(p, "/"),
	}
	return u.String()
}

// getJSON 执行一次 GET;响应体上限防止异常响应占满内存。
func (c *Client) getJSON(ctx context.Context, rawURL string, out any) error {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL, nil)
	if err != nil {
		return err
	}

	resp, err := c.http.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	body := io.LimitReader(resp.Body, 4= 300 {
		var apiErr APIError
		if err := json.NewDecoder(body).Decode(&apiErr); err != nil {
			return fmt.Errorf("pkg.go.dev 返回 HTTP %d", resp.StatusCode)
		}
		if apiErr.Code == 0 {
			apiErr.Code = resp.StatusCode
		}
		return &apiErr
	}

	if err := json.NewDecoder(body).Decode(out); err != nil {
		return fmt.Errorf("解析 pkg.go.dev 响应: %w", err)
	}
	return nil
}

第一段查询:从 import path 得到 modulePath

package 路由允许可选的 module 参数。普通路径先不传;如果服务返回多个 candidates,再让用户、配置文件或已有 go.mod 明确选择候选,不要在库代码里静默猜测。

package pkgapi

import (
	"context"
	"net/url"
)

// PackageMeta 只声明本任务需要的字段,未知字段会被 json 包忽略。
type PackageMeta struct {
	ModulePath string `json:"modulePath"`
	Version    string `json:"version"`
	Path       string `json:"path"`
	Name       string `json:"name"`
}

// ResolveModule 把包导入路径解析为明确的模块路径。
// moduleHint 为空时让 API 检测歧义;非空时用于指定候选模块。
func (c *Client) ResolveModule(
	ctx context.Context,
	importPath string,
	moduleHint string,
) (PackageMeta, error) {
	u, err := url.Parse(endpoint("package", importPath))
	if err != nil {
		return PackageMeta{}, err
	}
	if moduleHint != "" {
		q := u.Query()
		q.Set("module", moduleHint)
		u.RawQuery = q.Encode()
	}

	var meta PackageMeta
	err = c.getJSON(ctx, u.String(), &meta)
	return meta, err
}

如果错误对象中的 Candidates 非空,调用方应展示候选并重试。例如包路径 example.com/a/b/c 可能来自模块 example.com/a,也可能来自子模块 example.com/a/b。pkg.go.dev 网页可能采用最长模块路径,而 API 刻意要求明确的 module 参数,两者行为不要混淆。

第二段查询:分页取得模块版本

版本响应的核心字段是 items 和 nextPageToken。只要令牌非空,就代表还有下一页,即使当前页因 filter 没有 items,也不能提前停止。官方还说明,只有所有结果都落在一页时 total 才是确切值,否则可能为 -1,因此不要把 total 当作分页终止条件。

package pkgapi

import (
	"context"
	"net/url"
	"strconv"
)

// ModuleVersion 保留升级工具最常用的版本状态字段。
type ModuleVersion struct {
	ModulePath        string `json:"modulePath"`
	Version           string `json:"version"`
	CommitTime        string `json:"commitTime"`
	LatestVersion     string `json:"latestVersion"`
	HasGoMod          bool   `json:"hasGoMod"`
	Deprecated        bool   `json:"deprecated"`
	DeprecationReason string `json:"deprecationReason"`
	Retracted         bool   `json:"retracted"`
	RetractionReason  string `json:"retractionReason"`
}

type versionsPage struct {
	Items         []ModuleVersion `json:"items"`
	Total         int             `json:"total"`
	NextPageToken string          `json:"nextPageToken"`
}

// ListVersions 遍历全部页面;filter 使用 API 支持的 Go 表达式子集。
func (c *Client) ListVersions(
	ctx context.Context,
	modulePath string,
	filter string,
	includePseudo bool,
) ([]ModuleVersion, error) {
	base, err := url.Parse(endpoint("versions", modulePath))
	if err != nil {
		return nil, err
	}

	var all []ModuleVersion
	token := ""
	for {
		// 每页查询条件保持不变,只更新服务端返回的 token。
		q := base.Query()
		q.Set("limit", strconv.Itoa(100))
		if filter != "" {
			q.Set("filter", filter)
		}
		if includePseudo {
			q.Set("pseudo", "true")
		}
		if token != "" {
			q.Set("token", token)
		}
		base.RawQuery = q.Encode()

		var page versionsPage
		if err := c.getJSON(ctx, base.String(), &page); err != nil {
			return nil, err
		}
		all = append(all, page.Items...)
		if page.NextPageToken == "" {
			break
		}
		token = page.NextPageToken
	}
	return all, nil
}

若只关心 v2,可传入 hasPrefix(version, "v2.")。不要手工拼接已经转义的 filter,示例通过 url.Values.Encode 自动处理引号、空格和符号。

把两段查询连起来

package main

import (
	"context"
	"fmt"
	"log"

	"example.com/project/pkgapi"
)

func main() {
	ctx := context.Background()
	client := pkgapi.NewClient()

	// 第一次调用不指定模块,让 API 暴露可能的路径歧义。
	meta, err := client.ResolveModule(ctx, "golang.org/x/time/rate", "")
	if err != nil {
		log.Fatal(err)
	}

	// 默认只取标签版本;如业务需要伪版本再把最后一个参数改为 true。
	versions, err := client.ListVersions(ctx, meta.ModulePath, "", false)
	if err != nil {
		log.Fatal(err)
	}

	for _, v := range versions {
		// 撤回版本仍保留在结果中,展示层应明确标记而不是直接丢失证据。
		fmt.Printf("%s\tretracted=%v\t%s\n", v.Version, v.Retracted, v.CommitTime)
	}
}

示例中的 example.com/project/pkgapi 是占位导入路径,放进真实项目时改成本地模块路径。生产环境还应给 context 设置更短的截止时间,并在上层做缓存、请求合并和可观测性记录。

四个容易踩坑的边界

pkg.go.dev API 中路径歧义、版本筛选、分页状态和限流的静态关系图
图2:模块候选、版本口径、分页状态和限流需要由调用方分别处理。

1. 一个导入路径可能对应多个模块候选

不要自行套用“最长模块路径”后继续。记录 candidates,让调用方以 go.mod、仓库策略或人工选择提供 module 参数。这样拆分子模块后,工具不会悄悄切换数据源。

2. v2 以后模块路径本身会变化

Go 的语义导入版本规则要求 v2 及以上主版本带 /vN 后缀。example.com/lib 与 example.com/lib/v2 是不同模块路径。versions 路由会描述指定模块及相关主版本集合,但升级工具仍需按 import path 判断是否涉及代码导入路径迁移。

3. 撤回、弃用与伪版本不能混为一谈

retracted 表示模块作者撤回某个版本,deprecated 描述模块级弃用信号,伪版本则是由提交生成的合法版本形式。列表展示、自动推荐和审计导出应分别保留这些状态。一般升级建议默认排除撤回版本,但审计记录不应删除它们。

4. 分页与限流是协议的一部分

官方当前按 IP 网段限制为每秒 45 次查询,超出返回 HTTP 429。批量扫描依赖库时,应缓存 import path 到 modulePath 的映射、按 modulePath 去重、限制并发,并对 429 使用带抖动的退避;不要立即并发重试。分页请求除新增 token 外应保持原条件不变。

采用这套查询链时应观察什么

正式接入后,我建议至少记录五个指标:package 解析成功率、歧义候选比例、modulePath 去重率、平均分页数、429 比例。它们能分别说明输入质量、模块拆分复杂度、缓存收益、版本历史长度和请求调度是否健康。

如果只是临时脚本,两次 curl 已足够;如果是 IDE、机器人或组件平台,则应使用上面的两段式客户端,并把 API 前缀、超时、缓存 TTL 和伪版本策略做成配置。核心原则不变:先让 package 路由确认模块归属,再让 versions 路由负责版本枚举。这比猜测模块根路径可靠,也能正确承接 pkg.go.dev API 的歧义与分页语义。

官方资料

  • pkg.go.dev API 文档:https://pkg.go.dev/v1/api
  • Go 官方 API 发布博客:https://go.dev/blog/pkgsite-api
  • go.mod 文件参考:https://go.dev/doc/modules/gomod-ref
  • Go Modules v2 语义导入版本:https://go.dev/blog/v2-go-modules
声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>