gitlink-cli/doc/reading_notes/02_client.md

561 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# internal/client/client.go 阅读笔记(面向 Go 小白)
---
## 第 1 行:`package client`
**字面意思**:声明这个文件属于 `client`
**运行时作用**:这是项目的 HTTP 客户端模块,负责所有与 GitLink API 的通信。
---
## 第 3-16 行import 导入依赖
```go
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` 结构体(核心!)
```go
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` 结构体
```go
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()` 方法:
```go
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` 函数(构造函数)
```go
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`,内容大致如下:
```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 行)
```go
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/api`
- `path``/users/me`
- 最终 `fullURL` 变成 `https://www.gitlink.org.cn/api/users/me.json`
### ② 请求体处理(第 69-77 行)
```go
// 处理请求体
var bodyReader io.Reader
if body != nil {
// 把 body 序列化成 JSON
data, err := json.Marshal(body)
if err != nil {
return nil, err
}
// 转成 io.ReaderHTTP 请求需要的格式)
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 行)
```go
// 创建 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 行)
```go
// 发送请求
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 行)
```go
// 读取响应体
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 行)
```go
// 检查 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 行)
```go
// 解析 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 行)
```go
// 检查 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 的响应格式:
```json
{
"status": 0, // 0=失败, 1=成功, 200=成功
"message": "...", // 错误信息
"data": {...} // 数据
}
```
### ⑨ 自动解析 JSON 字符串数据(第 144-150 行)
```go
// 自动解析 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需要再次解析。
**小白补充**
比如响应是这样的:
```json
{
"status": 1,
"data": "{\"name\": \"test\"}" // data 是字符串!
}
```
这里需要把 `"{\"name\": \"test\"}"` 再解析成 `{"name": "test"}`
### ⑩ 构建分页元数据(第 152-166 行)
```go
// 构建分页元数据
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 行:便捷方法
```go
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 行:错误信息映射
```go
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` 方法
```go
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. 构建 URLhttps://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. 解析 JSONjson.Unmarshal → map[string]interface{}
8. 检查业务错误:判断 status 字段
9. 返回 Envelopeoutput.SuccessEnvelope(raw, meta)
```