Files
go-common/docs/http.md

766 lines
20 KiB
Markdown
Raw Permalink 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.

# HTTP Restful工具文档
## 概述
HTTP Restful工具提供了标准化的HTTP请求和响应处理功能提供公共方法供外部调用保持低耦合。
## 功能特性
- **低耦合设计**提供公共方法不封装Handler结构
- **标准化的响应结构**`{code, message, timestamp, data}`
- **分离HTTP状态码和业务状态码**
- **支持分页响应**
- **提供便捷的请求参数解析方法**
- **支持JSON请求体解析**
- **Factory黑盒模式**:推荐使用 `factory.Success()` 等方法
## 响应结构
### 标准响应结构
```json
{
"code": 0,
"message": "success",
"timestamp": 1704067200,
"data": {}
}
```
**结构体类型(暴露在 factory 中):**
```go
// 在 factory 包中可以直接使用
type Response struct {
Code int `json:"code"` // 业务状态码0表示成功
Message string `json:"message"` // 响应消息
Timestamp int64 `json:"timestamp"` // 时间戳
Data interface{} `json:"data"` // 响应数据
}
```
### 分页响应结构
```json
{
"code": 0,
"message": "success",
"timestamp": 1704067200,
"data": {
"list": [],
"total": 100,
"page": 1,
"pageSize": 10
}
}
```
**结构体类型(暴露在 factory 中):**
```go
// 在 factory 包中可以直接使用
type PageData struct {
List interface{} `json:"list"` // 数据列表
Total int64 `json:"total"` // 总记录数
Page int `json:"page"` // 当前页码
PageSize int `json:"pageSize"` // 每页大小
}
type PageResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Timestamp int64 `json:"timestamp"`
Data *PageData `json:"data"`
}
```
### 使用暴露的结构体
外部项目可以直接使用 `factory.Response``factory.PageData` 等类型:
```go
import "git.toowon.com/jimmy/go-common/factory"
// 创建标准响应对象
response := factory.Response{
Code: 0,
Message: "success",
Data: userData,
}
// 创建分页数据对象
pageData := &factory.PageData{
List: users,
Total: 100,
Page: 1,
PageSize: 20,
}
// 传递给 Success 方法
fac.Success(w, pageData)
```
## 使用方法
### 方式一使用Factory黑盒方法推荐
这是最简单的方式,直接使用 `factory.Success()` 等方法:
```go
import (
"net/http"
"git.toowon.com/jimmy/go-common/factory"
commonhttp "git.toowon.com/jimmy/go-common/http"
"git.toowon.com/jimmy/go-common/tools"
)
func GetUser(w http.ResponseWriter, r *http.Request) {
fac, _ := factory.NewFactoryFromFile("config.json")
// 获取查询参数(使用类型转换方法)
id := tools.ConvertInt64(r.URL.Query().Get("id"), 0)
// 返回成功响应使用factory方法
fac.Success(w, data)
}
func CreateUser(w http.ResponseWriter, r *http.Request) {
fac, _ := factory.NewFactoryFromFile("config.json")
// 解析JSON使用公共方法
var req struct {
Name string `json:"name"`
}
if err := commonhttp.ParseJSON(r, &req); err != nil {
commonhttp.WriteJSON(w, http.StatusBadRequest, 400, "请求参数解析失败", nil)
return
}
// 返回成功响应使用factory方法
fac.Success(w, data, "创建成功")
}
func GetUserList(w http.ResponseWriter, r *http.Request) {
fac, _ := factory.NewFactoryFromFile("config.json")
// 获取分页参数使用factory方法推荐
pagination := fac.ParsePaginationRequest(r)
page := pagination.GetPage()
pageSize := pagination.GetPageSize()
// 获取查询参数直接使用HTTP原生方法
keyword := r.URL.Query().Get("keyword")
// 查询数据
list, total := getDataList(keyword, page, pageSize)
// 返回分页响应使用factory方法
fac.SuccessPage(w, list, total, page, pageSize)
}
// 注册路由
http.HandleFunc("/user", GetUser)
http.HandleFunc("/users", GetUserList)
```
### 方式二:直接使用公共方法
如果不想使用Factory可以直接使用 `http` 包的公共方法:
```go
import (
"net/http"
commonhttp "git.toowon.com/jimmy/go-common/http"
"git.toowon.com/jimmy/go-common/tools"
)
func GetUser(w http.ResponseWriter, r *http.Request) {
// 获取查询参数
id := tools.ConvertInt64(r.URL.Query().Get("id"), 0)
// 返回成功响应
commonhttp.Success(w, data)
}
func CreateUser(w http.ResponseWriter, r *http.Request) {
// 解析JSON
var req struct {
Name string `json:"name"`
}
if err := commonhttp.ParseJSON(r, &req); err != nil {
commonhttp.WriteJSON(w, http.StatusBadRequest, 400, "请求参数解析失败", nil)
return
}
// 返回成功响应
commonhttp.Success(w, data, "创建成功")
}
```
### 成功响应
```go
// 使用Factory推荐
fac.Success(w, data) // 只有数据,使用默认消息 "success"
fac.Success(w, data, "操作成功") // 数据+消息
// 或直接使用公共方法
commonhttp.Success(w, data) // 只有数据
commonhttp.Success(w, data, "操作成功") // 数据+消息
```
### 错误响应
```go
// 使用Factory推荐
fac.Error(w, 1001, "用户不存在") // 业务错误HTTP 200业务code非0
fac.SystemError(w, "服务器内部错误") // 系统错误HTTP 500
// 或直接使用公共方法
commonhttp.Error(w, 1001, "用户不存在")
commonhttp.SystemError(w, "服务器内部错误")
commonhttp.WriteJSON(w, http.StatusBadRequest, 400, "请求参数错误", nil) // 自定义HTTP状态码仅公共方法
```
### 分页响应
```go
// 使用Factory推荐
fac.SuccessPage(w, list, total, page, pageSize)
fac.SuccessPage(w, list, total, page, pageSize, "查询成功")
// 或直接使用公共方法
commonhttp.SuccessPage(w, list, total, page, pageSize)
commonhttp.SuccessPage(w, list, total, page, pageSize, "查询成功")
```
### 解析请求
#### 解析JSON请求体
```go
// 使用公共方法
var req struct {
Name string `json:"name"`
Email string `json:"email"`
}
if err := commonhttp.ParseJSON(r, &req); err != nil {
commonhttp.WriteJSON(w, http.StatusBadRequest, 400, "请求参数解析失败", nil)
return
}
```
#### 获取查询参数
```go
import "git.toowon.com/jimmy/go-common/tools"
// 字符串直接获取
name := r.URL.Query().Get("name")
// 使用类型转换方法
id := tools.ConvertInt(r.URL.Query().Get("id"), 0)
userId := tools.ConvertInt64(r.URL.Query().Get("userId"), 0)
isActive := tools.ConvertBool(r.URL.Query().Get("isActive"), false)
price := tools.ConvertFloat64(r.URL.Query().Get("price"), 0.0)
```
#### 获取表单参数
```go
import "git.toowon.com/jimmy/go-common/tools"
// 字符串直接获取
name := r.FormValue("name")
// 使用类型转换方法
age := tools.ConvertInt(r.FormValue("age"), 0)
userId := tools.ConvertInt64(r.FormValue("userId"), 0)
isActive := tools.ConvertBool(r.FormValue("isActive"), false)
```
#### 获取请求头
```go
// 直接使用HTTP原生方法
token := r.Header.Get("Authorization")
contentType := r.Header.Get("Content-Type")
if contentType == "" {
contentType = "application/json" // 设置默认值
}
```
#### 获取分页参数
**方式1使用 PaginationRequest 结构(推荐)**
```go
// 定义请求结构(包含分页字段)
type ListUserRequest struct {
Keyword string `json:"keyword"`
commonhttp.PaginationRequest // 嵌入分页请求结构
}
// 从JSON请求体解析分页字段会自动解析
var req ListUserRequest
if err := commonhttp.ParseJSON(r, &req); err != nil {
commonhttp.WriteJSON(w, http.StatusBadRequest, 400, "请求参数解析失败", nil)
return
}
// 使用分页方法
page := req.GetPage() // 获取页码默认1
pageSize := req.GetPageSize() // 获取每页数量默认20最大100
offset := req.GetOffset() // 计算偏移量
```
**方式2从查询参数/form解析分页**
```go
// 使用公共方法
pagination := commonhttp.ParsePaginationRequest(r)
page := pagination.GetPage()
pageSize := pagination.GetPageSize()
offset := pagination.GetOffset()
```
#### 获取时区
```go
// 使用公共方法
// 如果使用了middleware.Timezone中间件可以从context中获取时区信息
// 如果未设置,返回默认时区 AsiaShanghai
timezone := commonhttp.GetTimezone(r)
```
## 完整示例
### 使用Factory推荐
```go
package main
import (
"log"
"net/http"
"git.toowon.com/jimmy/go-common/factory"
commonhttp "git.toowon.com/jimmy/go-common/http"
"git.toowon.com/jimmy/go-common/tools"
)
// 用户结构
type User struct {
ID int64 `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
// 用户列表接口
func GetUserList(w http.ResponseWriter, r *http.Request) {
fac, _ := factory.NewFactoryFromFile("config.json")
// 获取分页参数(使用公共方法)
pagination := commonhttp.ParsePaginationRequest(r)
page := pagination.GetPage()
pageSize := pagination.GetPageSize()
// 获取查询参数直接使用HTTP原生方法
keyword := r.URL.Query().Get("keyword")
// 查询数据
users, total := queryUsers(keyword, page, pageSize)
// 返回分页响应使用factory方法
fac.SuccessPage(w, users, total, page, pageSize)
}
// 创建用户接口
func CreateUser(w http.ResponseWriter, r *http.Request) {
fac, _ := factory.NewFactoryFromFile("config.json")
// 解析请求体(使用公共方法)
var req struct {
Name string `json:"name"`
Email string `json:"email"`
}
if err := commonhttp.ParseJSON(r, &req); err != nil {
fac.WriteJSON(w, http.StatusBadRequest, 400, "请求参数解析失败", nil)
return
}
// 参数验证
if req.Name == "" {
fac.Error(w, 1001, "用户名不能为空")
return
}
// 创建用户
user, err := createUser(req.Name, req.Email)
if err != nil {
fac.SystemError(w, "创建用户失败")
return
}
// 返回成功响应使用factory方法
fac.Success(w, user, "创建成功")
}
// 获取用户详情接口
func GetUser(w http.ResponseWriter, r *http.Request) {
fac, _ := factory.NewFactoryFromFile("config.json")
// 获取查询参数(使用类型转换方法)
id := tools.ConvertInt64(r.URL.Query().Get("id"), 0)
if id == 0 {
commonhttp.WriteJSON(w, http.StatusBadRequest, 400, "用户ID不能为空", nil)
return
}
// 查询用户
user, err := getUserByID(id)
if err != nil {
fac.SystemError(w, "查询用户失败")
return
}
if user == nil {
fac.Error(w, 1002, "用户不存在")
return
}
// 返回成功响应使用factory方法
fac.Success(w, user)
}
func main() {
http.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
GetUserList(w, r)
case http.MethodPost:
CreateUser(w, r)
default:
commonhttp.WriteJSON(w, http.StatusMethodNotAllowed, 405, "方法不支持", nil)
}
})
http.HandleFunc("/user", GetUser)
log.Println("Server started on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
```
## API 参考
### Factory HTTP响应结构体暴露给外部项目使用
#### Response
标准响应结构体,外部项目可以直接使用 `factory.Response`
**字段:**
- `Code`: 业务状态码0表示成功
- `Message`: 响应消息
- `Timestamp`: 时间戳Unix时间戳
- `Data`: 响应数据
**示例:**
```go
response := factory.Response{
Code: 0,
Message: "success",
Data: userData,
}
```
#### PageData
分页数据结构体,外部项目可以直接使用 `factory.PageData`
**字段:**
- `List`: 数据列表
- `Total`: 总记录数
- `Page`: 当前页码
- `PageSize`: 每页大小
**示例:**
```go
pageData := &factory.PageData{
List: users,
Total: 100,
Page: 1,
PageSize: 20,
}
fac.Success(w, pageData)
```
#### PageResponse
分页响应结构体,外部项目可以直接使用 `factory.PageResponse`
**字段:**
- `Code`: 业务状态码
- `Message`: 响应消息
- `Timestamp`: 时间戳
- `Data`: 分页数据(*PageData
### Factory HTTP响应方法推荐使用
#### (f *Factory) Success(w http.ResponseWriter, data interface{}, message ...string)
成功响应HTTP 200业务code 0。
**参数:**
- `data`: 响应数据可以为nil
- `message`: 响应消息(可选),如果为空则使用默认消息 "success"
**示例:**
```go
fac.Success(w, data) // 只有数据,使用默认消息 "success"
fac.Success(w, data, "操作成功") // 数据+消息
```
#### (f *Factory) Error(w http.ResponseWriter, code int, message string)
业务错误响应HTTP 200业务code非0。
**示例:**
```go
fac.Error(w, 1001, "用户不存在")
```
#### (f *Factory) SystemError(w http.ResponseWriter, message string)
系统错误响应HTTP 500业务code 500。
**示例:**
```go
fac.SystemError(w, "服务器内部错误")
```
#### (f *Factory) WriteJSON(w http.ResponseWriter, httpCode, code int, message string, data interface{})
写入JSON响应自定义
**参数:**
- `httpCode`: HTTP状态码
- `code`: 业务状态码
- `message`: 响应消息
- `data`: 响应数据
**说明:**
- 此方法不在 Factory 中,直接使用 `commonhttp.WriteJSON()`
- 用于需要自定义HTTP状态码的场景如 400, 401, 403, 404 等)
**示例:**
```go
commonhttp.WriteJSON(w, http.StatusBadRequest, 400, "请求参数错误", nil)
commonhttp.WriteJSON(w, http.StatusUnauthorized, 401, "未登录", nil)
```
#### (f *Factory) SuccessPage(w http.ResponseWriter, list interface{}, total int64, page, pageSize int, message ...string)
分页成功响应。
**参数:**
- `list`: 数据列表
- `total`: 总记录数
- `page`: 当前页码
- `pageSize`: 每页大小
- `message`: 响应消息(可选,如果为空则使用默认消息 "success"
**示例:**
```go
fac.SuccessPage(w, users, total, page, pageSize)
fac.SuccessPage(w, users, total, page, pageSize, "查询成功")
```
### HTTP公共方法直接使用
#### WriteJSON(w http.ResponseWriter, httpCode, code int, message string, data interface{})
写入JSON响应自定义HTTP状态码和业务状态码
**说明:**
- 此方法不在 Factory 中,直接使用 `commonhttp.WriteJSON()`
- 用于需要自定义HTTP状态码的场景如 400, 401, 403, 404 等)
**示例:**
```go
commonhttp.WriteJSON(w, http.StatusBadRequest, 400, "请求参数错误", nil)
commonhttp.WriteJSON(w, http.StatusUnauthorized, 401, "未登录", nil)
```
#### ParseJSON(r *http.Request, v interface{}) error
解析JSON请求体。
**示例:**
```go
var req CreateUserRequest
if err := commonhttp.ParseJSON(r, &req); err != nil {
// 处理错误
}
```
#### 获取查询参数和表单参数
**推荐方式:使用类型转换工具**
```go
import "git.toowon.com/jimmy/go-common/tools"
// 字符串直接使用HTTP原生方法
name := r.URL.Query().Get("name")
if name == "" {
name = "default" // 设置默认值
}
// 类型转换使用tools包
id := tools.ConvertInt(r.URL.Query().Get("id"), 0)
userId := tools.ConvertInt64(r.URL.Query().Get("userId"), 0)
isActive := tools.ConvertBool(r.URL.Query().Get("isActive"), false)
price := tools.ConvertFloat64(r.URL.Query().Get("price"), 0.0)
// 表单参数类似
age := tools.ConvertInt(r.FormValue("age"), 0)
```
**类型转换方法说明:**
- `tools.ConvertInt(value string, defaultValue int) int` - 转换为int
- `tools.ConvertInt64(value string, defaultValue int64) int64` - 转换为int64
- `tools.ConvertUint64(value string, defaultValue uint64) uint64` - 转换为uint64
- `tools.ConvertUint32(value string, defaultValue uint32) uint32` - 转换为uint32
- `tools.ConvertBool(value string, defaultValue bool) bool` - 转换为bool
- `tools.ConvertFloat64(value string, defaultValue float64) float64` - 转换为float64
**获取请求头:**
```go
// 直接使用HTTP原生方法
token := r.Header.Get("Authorization")
contentType := r.Header.Get("Content-Type")
```
#### ParsePaginationRequest(r *http.Request) *PaginationRequest
从请求中解析分页参数。
**说明:**
- 支持从查询参数和form表单中解析
- 优先级:查询参数 > form表单
- 如果请求体是JSON格式且包含分页字段建议先使用`ParseJSON`解析完整请求体到包含`PaginationRequest`的结构体中
#### GetTimezone(r *http.Request) string
从请求的context中获取时区。
**说明:**
- 如果使用了middleware.Timezone中间件可以从context中获取时区信息
- 如果未设置,返回默认时区 AsiaShanghai
#### Success(w http.ResponseWriter, data interface{}, message ...string)
成功响应(公共方法)。
**参数:**
- `data`: 响应数据可以为nil
- `message`: 响应消息(可选),如果为空则使用默认消息 "success"
**示例:**
```go
commonhttp.Success(w, data) // 只有数据
commonhttp.Success(w, data, "操作成功") // 数据+消息
```
#### Error(w http.ResponseWriter, code int, message string)
错误响应(公共方法)。
#### SystemError(w http.ResponseWriter, message string)
系统错误响应(公共方法)。
#### WriteJSON(w http.ResponseWriter, httpCode, code int, message string, data interface{})
写入JSON响应公共方法不在Factory中
**说明:**
- 用于需要自定义HTTP状态码的场景
- 直接使用 `commonhttp.WriteJSON()`,不在 Factory 中
#### SuccessPage(w http.ResponseWriter, list interface{}, total int64, page, pageSize int, message ...string)
分页成功响应(公共方法)。
### 分页请求结构
#### PaginationRequest
分页请求结构支持从JSON和form中解析分页参数。
**字段:**
- `Page`: 页码默认1
- `PageSize`: 每页数量
**方法:**
- `GetPage() int`: 获取页码如果未设置则返回默认值1
- `GetPageSize() int`: 获取每页数量如果未设置则返回默认值20最大限制100
- `GetOffset() int`: 计算数据库查询的偏移量
#### ParsePaginationRequest(r *http.Request) *PaginationRequest
从请求中解析分页参数内部函数Handler内部使用
## 状态码说明
### HTTP状态码
- `200`: 正常响应(包括业务错误)
- `400`: 请求参数错误
- `401`: 未授权
- `403`: 禁止访问
- `404`: 资源不存在
- `500`: 系统内部错误
### 业务状态码
- `0`: 成功
- `非0`: 业务错误(具体错误码由业务定义)
## 注意事项
1. **HTTP状态码与业务状态码分离**
- 业务错误如用户不存在、参数验证失败等返回HTTP 200业务code非0
- 只有系统异常如数据库连接失败、程序panic等才返回HTTP 500
2. **分页参数限制**
- page最小值为1
- pageSize最小值为1最大值为100
3. **响应格式统一**
- 所有响应都遵循标准结构
- timestamp为Unix时间戳
4. **错误处理**
- 使用`Error`方法返回业务错误HTTP 200业务code非0
- 使用`SystemError`返回系统错误HTTP 500
- 其他HTTP错误状态码400, 401, 403, 404等使用`WriteJSON`方法直接指定
5. **推荐使用Factory**
- 使用 `factory.Success()` 等方法,代码更简洁
- 直接使用 `http` 包的公共方法,保持低耦合
- 不需要Handler结构减少不必要的封装
## 示例
完整示例请参考:
- `examples/http_handler_example.go` - 使用Factory和公共方法
- `examples/http_pagination_example.go` - 分页示例
- `examples/factory_blackbox_example.go` - Factory黑盒模式示例