16 KiB
internal/client/client.go 阅读笔记(面向 Go 小白)
第 1 行:package client
字面意思:声明这个文件属于 client 包
运行时作用:这是项目的 HTTP 客户端模块,负责所有与 GitLink API 的通信。
第 3-16 行:import 导入依赖
import (
"bytes" // 字节缓冲(用于构造请求体)
"encoding/json" // JSON 序列化/反序列化
"fmt" // 格式化输出
"io" // 输入输出接口
"net/http" // HTTP 协议
"net/url" // URL 处理
"strings" // 字符串操作
"github.com/gitlink-org/gitlink-cli/internal/auth" // 认证模块(带 Token 的 HTTP 客户端)
"github.com/gitlink-org/gitlink-cli/internal/config" // 配置管理
clierrors "github.com/gitlink-org/gitlink-cli/internal/errors" // 错误定义
"github.com/gitlink-org/gitlink-cli/internal/output" // 输出格式化
)
小白补充:
| 包名 | 用途 | 在本文件中的作用 |
|---|---|---|
bytes |
字节操作 | 把 JSON 数据转成 HTTP 请求体 |
io |
输入输出 | 读取 HTTP 响应体 |
net/http |
HTTP 协议 | 创建和发送 HTTP 请求 |
第 18-23 行:Client 结构体(核心!)
type Client struct {
HTTP *http.Client
BaseURL string
Debug bool
SkipJSONSuffix bool
}
字面意思:定义 HTTP 客户端的结构
运行时作用:这是项目封装的 HTTP 客户端,所有 API 调用都通过它来完成。
小白补充:
① 每个字段的含义:
| 字段 | 类型 | 含义 |
|---|---|---|
HTTP |
*http.Client |
Go 标准库的 HTTP 客户端(核心) |
BaseURL |
string |
API 基础地址(如 https://www.gitlink.org.cn/api) |
Debug |
bool |
是否开启调试模式(打印请求/响应) |
SkipJSONSuffix |
bool |
是否跳过自动添加 .json 后缀(Wiki Gateway 需要) |
② *http.Client 是什么?
http.Client 是 Go 标准库提供的 HTTP 客户端,它包含:
- 连接池管理
- 超时设置
- Cookie 管理
- 传输层配置(如 TLS、代理)
我们项目在 internal/auth/transport.go 中对它进行了扩展,自动添加认证 Token。
第 25-35 行:APIError 结构体
type APIError struct {
StatusCode int
Code interface{}
Message string
Kind clierrors.ErrorKind
Suggestion string
}
func (e *APIError) Error() string {
return fmt.Sprintf("[%v] %s", e.Code, e.Message)
}
字面意思:定义 API 错误的结构
运行时作用:封装 API 返回的错误信息,包含错误码、消息和解决建议。
小白补充:
① Error() 方法:
func (e *APIError) Error() string {
return fmt.Sprintf("[%v] %s", e.Code, e.Message)
}
- 这是实现了 Go 的
error接口 - 任何实现了
Error() string方法的类型都可以作为error返回 - 这样
APIError就可以像普通错误一样使用:return apiErr
② 为什么需要自定义错误类型?
普通的 error 只能包含一条消息,而我们需要:
StatusCode:HTTP 状态码(404/403/500 等)Code:API 返回的业务错误码Kind:错误分类(认证错误/输入错误/服务器错误等)Suggestion:给用户的解决建议
第 37-46 行:New 函数(构造函数)
func New() (*Client, error) {
// 1. 加载配置
cfg, err := config.Load()
if err != nil {
return nil, err
}
// 2. 创建并返回 Client
return &Client{
HTTP: auth.NewHTTPClient(), // 带认证的 HTTP 客户端
BaseURL: cfg.BaseURL, // 从配置获取 API 地址
}, nil
}
字面意思:创建一个新的 Client 实例
运行时作用:这是 Client 的构造函数,自动加载配置并创建带认证的 HTTP 客户端。
小白补充:
① auth.NewHTTPClient() 做了什么?
这个函数在 internal/auth/transport.go 中,它创建了一个 HTTP 客户端,并且:
- 自动从配置文件读取 Token
- 在每个请求的
Authorization头中添加Bearer {token} - 处理 Token 过期等情况
② 配置文件的内容:
配置文件位于 ~/.config/gitlink-cli/config.yaml,内容大致如下:
base_url: https://www.gitlink.org.cn/api
gateway_base_url: https://gateway.gitlink.org.cn/api
token: your-token-here
第 48-168 行:Do 方法(核心!)
这是整个文件中最重要的函数,负责发送 HTTP 请求并解析响应。
① 路径处理(第 48-67 行)
func (c *Client) Do(method, path string, body interface{}, query url.Values) (*output.Envelope, error) {
// 自动添加 .json 后缀(GitLink API 约定)
if c.shouldAppendJSONSuffix(path) {
if idx := strings.Index(path, "?"); idx != -1 {
// 路径已经包含查询参数,在 ? 前面加 .json
basePath := path[:idx]
queryStr := path[idx:]
path = basePath + ".json" + queryStr
} else {
// 路径没有查询参数,直接加 .json
path += ".json"
}
}
// 构建完整 URL
fullURL := c.BaseURL + path
if query != nil && len(query) > 0 {
sep := "?"
if strings.Contains(fullURL, "?") {
sep = "&" // URL 已经有 ?,用 & 连接
}
fullURL += sep + query.Encode()
}
// ...
}
字面意思:处理请求路径,构建完整 URL
运行时作用:GitLink API 约定所有路径都需要 .json 后缀,这里自动添加。
小白补充:
c.BaseURL是https://www.gitlink.org.cn/apipath是/users/me- 最终
fullURL变成https://www.gitlink.org.cn/api/users/me.json
② 请求体处理(第 69-77 行)
// 处理请求体
var bodyReader io.Reader
if body != nil {
// 把 body 序列化成 JSON
data, err := json.Marshal(body)
if err != nil {
return nil, err
}
// 转成 io.Reader(HTTP 请求需要的格式)
bodyReader = bytes.NewReader(data)
}
字面意思:把请求体转成 HTTP 可以发送的格式
运行时作用:如果有请求体(如 POST/PUT 请求),把 Go 的 map 转成 JSON 字符串,再转成字节流。
小白补充:
json.Marshal(body):把 Go 结构体/map 转成 JSON 字节数组bytes.NewReader(data):把字节数组包装成io.Reader(HTTP 请求体需要这个接口)
③ 创建 HTTP 请求(第 79-87 行)
// 创建 HTTP 请求
req, err := http.NewRequest(method, fullURL, bodyReader)
if err != nil {
return nil, err
}
// 调试模式:打印请求信息
if c.Debug {
fmt.Printf("→ %s %s\n", method, fullURL)
}
字面意思:创建一个 HTTP 请求对象
运行时作用:http.NewRequest 创建请求对象,包含方法、URL 和请求体。
④ 发送请求(第 89-92 行)
// 发送请求
resp, err := c.HTTP.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close() // 确保响应体被关闭
字面意思:发送 HTTP 请求并获取响应
运行时作用:c.HTTP.Do(req) 发送请求,返回响应对象。
小白补充:
defer resp.Body.Close():非常重要! 确保响应体被关闭,避免资源泄漏defer是 Go 的关键字,它会在函数返回前执行后面的语句- 如果不关闭
resp.Body,HTTP 连接池会被占满,导致后续请求失败
⑤ 读取响应体(第 94-101 行)
// 读取响应体
respData, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
// 调试模式:打印响应信息
if c.Debug {
fmt.Printf("← %d %s\n", resp.StatusCode, string(respData[:min(len(respData), 200)]))
}
字面意思:把响应体读取成字节数组
运行时作用:io.ReadAll(resp.Body) 读取整个响应体内容。
小白补充:
resp.StatusCode是 HTTP 状态码(200=成功,404=未找到,500=服务器错误)
⑥ HTTP 状态码检查(第 103-113 行)
// 检查 HTTP 状态码
if resp.StatusCode >= 400 {
info := lookupStatusInfo(resp.StatusCode)
return nil, &APIError{
StatusCode: resp.StatusCode,
Code: resp.StatusCode,
Message: fmt.Sprintf("HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(respData))),
Kind: info.kind,
Suggestion: info.suggestion,
}
}
字面意思:如果状态码 >= 400,返回错误
运行时作用:HTTP 4xx/5xx 都是错误,这里封装成 APIError 返回。
小白补充:
lookupStatusInfo(resp.StatusCode)根据状态码查找对应的错误分类和建议
⑦ JSON 解析(第 115-120 行)
// 解析 JSON 响应
var raw map[string]interface{}
if err := json.Unmarshal(respData, &raw); err != nil {
// 不是 JSON,直接返回原始内容
return output.SuccessEnvelope(string(respData), nil), nil
}
字面意思:把响应体解析成 Go 的 map
运行时作用:json.Unmarshal 把 JSON 字符串转成 Go 的 map[string]interface{}。
小白补充:
json.Unmarshal的第二个参数需要传递指针(&raw)interface{}是 Go 的"万能类型",可以存储任何值- 如果响应不是 JSON(比如返回的是 HTML 错误页面),就直接返回字符串
⑧ GitLink 业务错误检查(第 122-142 行)
// 检查 GitLink 业务错误(响应体中的 status 字段)
if status, ok := raw["status"]; ok {
var statusCode float64
switch v := status.(type) {
case float64:
statusCode = v
case int:
statusCode = float64(v)
}
// status 不为 0、1、200 都是错误
if statusCode != 0 && statusCode != 200 && statusCode != 1 {
msg, _ := raw["message"].(string)
info := lookupStatusInfo(int(statusCode))
return output.ErrorEnvelope(int(statusCode), msg, info.suggestion), &APIError{
StatusCode: int(statusCode),
Code: int(statusCode),
Message: msg,
Kind: info.kind,
Suggestion: info.suggestion,
}
}
}
字面意思:检查 GitLink API 返回的业务错误码
运行时作用:GitLink API 有时 HTTP 状态码是 200,但响应体中的 status 字段表示业务失败(如参数校验失败)。
小白补充:
GitLink API 的响应格式:
{
"status": 0, // 0=失败, 1=成功, 200=成功
"message": "...", // 错误信息
"data": {...} // 数据
}
⑨ 自动解析 JSON 字符串数据(第 144-150 行)
// 自动解析 JSON 字符串数据(GitLink API 的一个特性)
if dataStr, ok := raw["data"].(string); ok {
var parsedData interface{}
if err := json.Unmarshal([]byte(dataStr), &parsedData); err == nil {
raw["data"] = json.RawMessage(dataStr)
}
}
字面意思:处理 data 字段是 JSON 字符串的情况
运行时作用:GitLink 某些 API 返回的 data 字段是字符串形式的 JSON,需要再次解析。
小白补充:
比如响应是这样的:
{
"status": 1,
"data": "{\"name\": \"test\"}" // data 是字符串!
}
这里需要把 "{\"name\": \"test\"}" 再解析成 {"name": "test"}。
⑩ 构建分页元数据(第 152-166 行)
// 构建分页元数据
var meta *output.Meta
if tc, ok := raw["total_count"]; ok {
meta = &output.Meta{}
if v, ok := tc.(float64); ok {
meta.TotalCount = int(v)
}
if v, ok := raw["page"].(float64); ok {
meta.Page = int(v)
}
if v, ok := raw["limit"].(float64); ok {
meta.Limit = int(v)
}
}
// 返回成功的 Envelope
return output.SuccessEnvelope(raw, meta), nil
字面意思:从响应中提取分页信息
运行时作用:如果 API 返回了分页信息(total_count/page/limit),提取出来作为 Meta。
第 170-184 行:便捷方法
func (c *Client) Get(path string, query url.Values) (*output.Envelope, error) {
return c.Do("GET", path, nil, query)
}
func (c *Client) Post(path string, body interface{}) (*output.Envelope, error) {
return c.Do("POST", path, body, nil)
}
func (c *Client) Put(path string, body interface{}) (*output.Envelope, error) {
return c.Do("PUT", path, body, nil)
}
func (c *Client) Delete(path string, query url.Values) (*output.Envelope, error) {
return c.Do("DELETE", path, nil, query)
}
字面意思:封装常见的 HTTP 方法
运行时作用:提供更简洁的调用方式,比如 client.Get("/users/me", nil) 而不是 client.Do("GET", "/users/me", nil, nil)。
第 186-225 行:错误信息映射
type statusInfo struct {
kind clierrors.ErrorKind
message string
suggestion string
}
var statusMessages = map[int]statusInfo{
-2: {clierrors.KindAuth, "未登录或 Token 已过期",
"运行 gitlink-cli auth login 重新登录"},
-1: {clierrors.KindInput, "参数校验失败",
"检查必填参数是否缺失"},
401: {clierrors.KindAuth, "认证失败",
"运行 gitlink-cli auth login 登录"},
403: {clierrors.KindForbidden, "权限不足",
"请确认账号有此仓库的访问权限"},
404: {clierrors.KindNotFound, "资源不存在",
"检查 owner/repo/id 是否正确"},
// ... 更多状态码
}
func lookupStatusInfo(code int) statusInfo {
if info, ok := statusMessages[code]; ok {
return info
}
return statusInfo{
kind: clierrors.KindUnknown,
message: fmt.Sprintf("API 返回错误码 %d", code),
}
}
字面意思:根据错误码查找对应的错误信息
运行时作用:把枯燥的错误码转换成人类可读的错误信息和解决建议。
第 227-246 行:shouldAppendJSONSuffix 方法
func (c *Client) shouldAppendJSONSuffix(path string) bool {
// 1. 如果设置了 SkipJSONSuffix,不添加
if c.SkipJSONSuffix {
return false
}
// 2. 如果已经有 .json 后缀,不添加
if strings.HasSuffix(path, ".json") {
return false
}
// 3. 如果是 raw 内容路径,不添加
parts := strings.Split(strings.Trim(path, "/"), "/")
for i, part := range parts {
if part == "raw" && i >= 2 && i+2 < len(parts) {
return false
}
}
// 4. 其他情况,添加 .json 后缀
return true
}
字面意思:判断是否应该添加 .json 后缀
运行时作用:控制是否自动添加 .json 后缀。
小白补充:
为什么需要这个方法?
- Wiki Gateway API 不需要
.json后缀(设置SkipJSONSuffix: true) - 某些路径(如
/owner/repo/raw/...)返回的是原始文件内容,不是 JSON
完整调用流程
ctx.CallAPI("GET", "/users/me", nil)
↓
Client.Do("GET", "/users/me", nil, nil)
↓
1. 路径处理:/users/me → /users/me.json
2. 构建 URL:https://www.gitlink.org.cn/api/users/me.json
3. 创建 HTTP 请求:http.NewRequest("GET", url, nil)
4. 发送请求:c.HTTP.Do(req)
↓ (auth.NewHTTPClient() 自动添加 Authorization 头)
5. 读取响应体:io.ReadAll(resp.Body)
6. 检查状态码:如果 >= 400,返回 APIError
7. 解析 JSON:json.Unmarshal → map[string]interface{}
8. 检查业务错误:判断 status 字段
9. 返回 Envelope:output.SuccessEnvelope(raw, meta)