diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4c506df..314d122 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: - go-version: [1.18, 1.19, "1.20", 1.21, 1.22, 1.23] + go-version: [1.23, 1.24,1.25] steps: - uses: actions/checkout@v4 diff --git a/README.md b/README.md index aba358b..ef0a88b 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,18 @@  -[gin](https://github.com/gin-gonic/gin)是简单快速的`golang`框架,这篇文章主要是介绍`gin`的路由配置及使用(主要是post方法) +[gin](https://github.com/gin-gonic/gin)是简单快速的`golang`框架,这个项目主要是介绍`gin`的路由配置及使用,包括各种HTTP请求方法(GET、POST)和数据格式处理(JSON、XML、Form、URL Encoded)。 -golang >= 1.18 +**主要特性:** +- 展示 Gin 框架的多种路由配置方式 +- 支持多种数据格式的请求处理(JSON、XML、Form、URL Encoded) +- 文件上传功能(单文件及分片上传) +- 前端与后端交互示例 + +**技术栈:** +- Go >= 1.18 +- Gin Web Framework v1.9.1 +- 支持 Docker 部署 @@ -14,7 +23,7 @@ golang >= 1.18 # development go run main.go -# run development +# run development with live reload # https://github.com/cosmtrek/air Live reload for Go apps air @@ -25,120 +34,278 @@ go build # export GIN_MODE=release ./gin-router-web -# make build ./bin/app +# make build -> ./bin/app make build # server 8080 http://localhost:8080/ + # file chunk upload http://localhost:8080/upload_chunks # docker deploy make serve -## +# dependency management go mod tidy ``` -## API +## 项目理念与设计 + +本项目是一个 **Gin 框架学习和示例项目**,旨在展示: + +### 核心特性 +1. **路由配置** - 展示 Gin 框架的路由分组、中间件、静态资源配置等 +2. **多种数据格式处理** - JSON、XML、Form、URL Encoded 等多种数据格式的处理方式 +3. **文件处理** - 单文件上传、批量上传、分片上传完整解决方案 +4. **Web 应用** - 包含前后端交互的完整示例 + +### 架构思想 +- **分层设计** - API层、路由层、模型层清晰分离 +- **可扩展性** - 新增 API 只需在 `api/` 目录添加处理函数,在 `route.go` 注册路由 +- **代码复用** - 响应格式统一通过 `helper` 模块处理 +- **前后端分离** - 前端页面独立,便于前后端开发解耦 + +## 项目结构 + +``` +gin_router_web/ +├── api/ # API 处理层 +│ ├── handle_func.go # 各类型数据处理函数 +│ ├── upload.go # 文件上传相关 +│ └── web.go # 网页路由处理 +├── router/ +│ └── route.go # 路由配置 +├── models/ # 数据模型 +│ ├── chunk_file.go # 分片文件模型 +│ └── user.go # 用户模型 +├── helper/ +│ └── respose.go # 响应格式化工具 +├── templates/ # HTML 模板 +│ ├── index.html # 首页 +│ └── upload_chunks.html # 分片上传页面 +├── public/ # 静态资源 +│ └── static/ +│ ├── css/ +│ │ └── common.css +│ └── js/ +│ ├── index.js +│ └── plupload.full.min.js +├── main.go # 应用入口 +├── go.mod # Go 模块定义 +├── Dockerfile # Docker 配置 +├── docker-compose.yml # Docker Compose 配置 +├── Makefile # 构建脚本 +└── README.md # 项目文档 +``` + +### 关键模块说明 + +| 模块 | 描述 | +|-----|------| +| **api** | 包含所有的 HTTP 请求处理函数,支持多种数据格式 | +| **router** | 路由配置层,定义所有的 API 端点和Web路由 | +| **models** | 数据模型定义,如用户信息、分片文件等 | +| **helper** | 工具函数,如统一的响应格式化 | +| **templates** | HTML 模板文件,提供前端界面 | +| **public** | 静态资源(CSS、JavaScript等) | + +## API 接口列表 + ```md -[POST] /api/form_post -[POST] /api/json_post -[POST] /api/urlencoded_post -[POST] /api/json_and_form_post -[POST] /api/xml_post -[POST] /api/file_upload -[POST] /api/file_chunk_upload -[GET] /api/query +[GET] / 首页 +[GET] /upload_chunks 分片上传页面 +[GET] /api/query 查询参数示例 + +[POST] /api/form_post 表单数据提交 +[POST] /api/json_post JSON 数据提交 +[POST] /api/urlencoded_post URL 编码数据提交 +[POST] /api/json_and_form_post JSON 或 Form 数据提交(自动识别) +[POST] /api/xml_post XML 数据提交 +[POST] /api/file_upload 文件上传 +[POST] /api/file_chunk_upload 分片文件上传 +``` + +## 快速开始 + +### 环境要求 +- Go >= 1.18 +- (可选) Docker & Docker Compose 用于容器化部署 + +### 本地运行 + +```bash +# 1. 克隆项目 +git clone https://github.com/freeshineit/gin_router_web.git +cd gin_router_web + +# 2. 安装依赖 +go mod tidy + +# 3. 开发模式运行(需要安装 air) +go install github.com/cosmtrek/air@latest +air + +# 或直接运行 +go run main.go + +# 4. 浏览器访问 +http://localhost:8080/ +``` + +### Docker 部署 + +```bash +# 构建并运行 Docker 容器 +make deploy + +# 或手动构建 +docker build -t gin-router-web . +docker run -p 8080:8080 gin-router-web ``` ## 静态资源配置 ```go func setStaticFS(r *gin.Engine) { - // set html template - r.LoadHTMLGlob("views/*") + // 加载 HTML 模板文件 + r.LoadHTMLGlob("./templates/*.html") - // set server static + // 配置 favicon r.StaticFile("favicon.ico", "./public/favicon.ico") + + // 配置静态文件目录(CSS、JS 等) r.StaticFS("/static", http.Dir("public/static")) + + // 配置上传文件目录 r.StaticFS("/upload", http.Dir("upload")) } ``` -`func (engine *Engine) LoadHTMLGlob(pattern string)`函数加载全局模式的 HTML 文件标识,并将结果与 HTML 渲染器相关联。 +**关键方法说明:** -`func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) IRoutes` 设置相对路径的静态资源 +- `LoadHTMLGlob(pattern string)` - 加载全局模式的 HTML 文件标识,并与 HTML 渲染器关联 +- `StaticFile(relativePath, filepath string)` - 配置单个静态文件 +- `StaticFS(relativePath string, fs http.FileSystem)` - 配置静态资源目录 -## api +## 路由配置详解 -> api 路由分组 +### Web 路由 ```go -api := r.Group("/api") -{ - api.POST("/form_post", formPost) - - api.POST("/json_post", jsonPost) - api.POST("/urlencoded_post", urlencodedPost) - api.POST("/json_and_form_post", jsonAndFormPost) - api.POST("/xml_post", xmlPost) - api.POST("/file_upload", fileUpload) - - api.GET("/list", func(c *gin.Context) { - name := c.Query("name") - message := c.Query("message") - nick := c.DefaultQuery("nick", "anonymous") - - c.JSON(http.StatusOK, helper.BuildResponse(gin.H{ - "name": name, - "message": message, - "nick": nick, - })) - }) +func setWebRoute(r *gin.Engine) { + // 首页路由 + r.GET("/", api.WebIndex) + + // 分片上传页面 + r.GET("/upload_chunks", api.WebUploadChunks) } +``` + +### API 路由分组 + +```go +func SetupRoutes() *gin.Engine { + r := gin.Default() + + // 设置静态资源 + setStaticFS(r) + + // 设置Web路由 + setWebRoute(r) + + // API 路由分组 + apiGroup := r.Group("/api") + { + // 表单提交 + apiGroup.POST("/form_post", api.FormPost) + + // JSON 提交 + apiGroup.POST("/json_post", api.JSONPost) + + // URL 编码提交 + apiGroup.POST("/urlencoded_post", api.UrlencodedPost) + // JSON 和 Form 混合提交(自动识别) + apiGroup.POST("/json_and_form_post", api.JSONAndFormPost) + + // XML 提交 + apiGroup.POST("/xml_post", api.XMLPost) + + // 文件上传 + apiGroup.POST("/file_upload", api.FileUpload) + + // 文件分片上传 + apiGroup.POST("/file_chunk_upload", api.FileChunkUpload) + + // 查询参数示例 + apiGroup.GET("/query", func(c *gin.Context) { + name := c.Query("name") + message := c.Query("message") + nick := c.DefaultQuery("nick", "anonymous") + + c.JSON(http.StatusOK, helper.BuildResponse(gin.H{ + "name": name, + "message": message, + "nick": nick, + })) + }) + } + + return r +} ``` -## 消息的类型 +**关键概念:** +- **路由分组** (`r.Group()`) - 对多个相关路由进行分组,便于批量配置中间件或路径前缀 +- **Query 参数** - 通过 URL 查询字符串传递,如 `?name=value&message=test` +- **DefaultQuery** - 获取查询参数,提供默认值 -常用请求`Headers`中`Content-Type`的类型有`text/plain`、`text/html`、`application/json`、`application/x-www-form-urlencoded`、`application/xml`和`multipart/form-data`等. +## HTTP 请求数据格式 -- `text/plain` 纯文本 -- `text/html` HTML 文档 -- `application/json` json 格式数据 -- `application/x-www-form-urlencoded` 使用 HTTP 的 POST 方法提交的表单 -- `application/xml` xml 格式数据 -- `application/form-data`主要是用来上传文件 +在 HTTP 通信中,请求体的数据格式由 `Content-Type` 头部指定。本项目展示如何处理常见的数据格式: -[MIME](https://zh.wikipedia.org/wiki/MIME) +| Content-Type | 用途 | 例子 | +|-------------|------|-----| +| `text/plain` | 纯文本 | 简单文本消息 | +| `text/html` | HTML 文档 | 网页内容 | +| `application/json` | JSON 格式数据 | REST API 的标准格式 | +| `application/x-www-form-urlencoded` | 表单数据 | HTML 表单默认格式 | +| `application/xml` | XML 格式数据 | 配置文件、SOAP 通信 | +| `multipart/form-data` | 多部分数据 | 文件上传、混合数据 | -### form 表单提交 +参考:[MIME 类型](https://zh.wikipedia.org/wiki/MIME) -gin 路由实现 +### 1. Form 表单提交 (`application/x-www-form-urlencoded`) + +**数据模型** ```go -// User user struct +// User 用户信息结构体,支持 JSON、Form 和 XML 三种格式 type User struct { Name string `json:"name" form:"name" xml:"name"` Message string `json:"message" form:"message" xml:"message"` Nick string `json:"nick" form:"nick" xml:"nick"` } +``` -// FormPost 表单提交 -func FormPost(c *gin.Context) { +**后端实现** (api/handle_func.go) +```go +// FormPost 处理表单数据提交 +func FormPost(c *gin.Context) { + // 方式1:逐个读取字段 message := c.PostForm("message") nick := c.DefaultPostForm("nick", "default nick") name := c.DefaultPostForm("name", "default name") + user := User{ Name: name, Nick: nick, Message: message, } - // This way is better - // 下面这种方式 会自动和定义的结构体进行绑定 + // 方式2:自动绑定(推荐) // user := &User{} // c.ShouldBind(user) @@ -146,36 +313,46 @@ func FormPost(c *gin.Context) { } ``` -html 实现 +**前端实现** (templates/index.html) ```html
``` -## post 提交`application/json`类型数据 +**关键知识点:** +- `c.PostForm(key)` - 读取表单字段 +- `c.DefaultPostForm(key, defaultValue)` - 读取表单字段,提供默认值 +- `c.ShouldBind()` - 自动将表单数据绑定到结构体(推荐) -gin 路由实现 +### 2. JSON 数据提交 (`application/json`) + +**后端实现** ```go -// JSONPost json +// JSONPost 处理 JSON 格式数据 func JSONPost(c *gin.Context) { var user User + + // 绑定 JSON 数据到结构体 if err := c.BindJSON(&user); err != nil { - c.AbortWithStatusJSON(http.StatusOK, helper.BuildErrorResponse(http.StatusBadRequest, "invalid parameter")) + c.AbortWithStatusJSON(http.StatusOK, helper.BuildErrorResponse( + http.StatusBadRequest, + "invalid parameter", + )) return } @@ -183,78 +360,102 @@ func JSONPost(c *gin.Context) { } ``` -js 实现 +**前端实现** (JavaScript/Axios) ```js +const data = { + name: "shine", + message: "hello gin", + nick: "shineshao" +}; + axios({ method: "post", url: "/api/json_post", headers: { "Content-Type": "application/json", }, - data, + data, // 自动序列化为 JSON }).then((res) => { console.log(res.data); - $(".json-msg").text(`success ${new Date()}`); + $(".json-msg").text(`success at ${new Date()}`); }); ``` -## post 提交`application/x-www-form-urlencoded`类型数据 +**关键知识点:** +- `c.BindJSON()` - 解析 JSON 请求体并绑定到结构体 +- Axios 会自动设置 `Content-Type: application/json` + +### 3. URL 编码数据提交 (`application/x-www-form-urlencoded`) -gin 实现 +**后端实现** ```go -// UrlencodedPost application/x-www-form-urlencoded +// UrlencodedPost 处理 URL 编码格式数据 func UrlencodedPost(c *gin.Context) { - + // 从查询参数读取 limit limit := c.Query("limit") + + // 从 POST 表单数据读取 name := c.PostForm("name") message := c.PostForm("message") - nick := c.DefaultPostForm("nick", "1231412") + nick := c.DefaultPostForm("nick", "default") + user := User{ Name: name, Nick: nick, Message: message, } - // This way is better - // 下面这种方式 会自动和定义的结构体进行绑定 - // user := &User{} - // c.ShouldBind(user) - log.Printf("request query limit: %s\n", limit) - c.JSON(http.StatusOK, helper.BuildResponse(user)) } ``` -js 实现 +**前端实现** ```js +const data = { + name: "shine", + message: "hello gin", + nick: "shineshao" +}; + +// 注意:查询参数在 URL 中,表单数据在请求体中 axios({ method: "post", - url: "/api/urlencoded_post?name=shineshao", + url: "/api/urlencoded_post?limit=100", headers: { "Content-Type": "application/x-www-form-urlencoded", }, - data: $.param(data), + data: $.param(data), // 使用 $.param() 进行 URL 编码 }).then((res) => { console.log(res.data); - $(".urlencoded-msg").text(`success ${new Date()}`); + $(".urlencoded-msg").text(`success at ${new Date()}`); }); ``` -## post 提交`application/x-www-form-urlencoded`或`application/json`类型数据 +**关键知识点:** +- `c.Query()` - 读取 URL 查询参数 +- `c.PostForm()` - 读取 POST 表单数据 +- 使用 `$.param()` 进行 URL 编码 + +### 4. JSON 或 Form 混合提交 (自动识别) -gin +**后端实现** ```go -//JSONAndFormPost application/json application/x-www-form-urlencoded +// JSONAndFormPost 自动识别 JSON 或 Form 格式 +// 可以同时处理 application/json 和 application/x-www-form-urlencoded func JSONAndFormPost(c *gin.Context) { var user User + // ShouldBind() 根据 Content-Type 自动选择合适的绑定方式 if err := c.ShouldBind(&user); err != nil { - c.AbortWithStatusJSON(http.StatusOK, helper.BuildErrorResponse(http.StatusBadRequest, "invalid parameter")) + c.AbortWithStatusJSON(http.StatusOK, helper.BuildErrorResponse( + http.StatusBadRequest, + "invalid parameter", + )) return } @@ -262,10 +463,15 @@ func JSONAndFormPost(c *gin.Context) { } ``` -js 实现 +**前端实现 - JSON 方式** ```js -// json +const data = { + name: "shine", + message: "hello gin", + nick: "shineshao" +}; + axios({ method: "post", url: "/api/json_and_form_post", @@ -275,10 +481,13 @@ axios({ data, }).then((res) => { console.log(res.data); - $(".jsonandform-msg").text(`success application/json data, ${new Date()}`); + $(".jsonandform-msg").text(`success with JSON at ${new Date()}`); }); +``` + +**前端实现 - Form 方式** -// x-www-form-urlencoded +```js axios({ method: "post", url: "/api/json_and_form_post", @@ -288,25 +497,29 @@ axios({ data: $.param(data), }).then((res) => { console.log(res.data); - $(".jsonandform-msg").text( - `success application/x-www-form-urlencoded data${new Date()}` - ); + $(".jsonandform-msg").text(`success with Form at ${new Date()}`); }); ``` -## post 提交`application/xml`类型数据(`application/xml`) +**关键知识点:** +- `c.ShouldBind()` - 自动识别 Content-Type,使用相应的绑定方式 +- 同一个接口可以处理多种数据格式 + +### 5. XML 数据提交 (`application/xml`) -gin 实现 +**后端实现** ```go -//XMLPost xml +// XMLPost 处理 XML 格式数据 func XMLPost(c *gin.Context) { var user User - // c.ShouldBind(&user) - // c.Bind(&user) + // 绑定 XML 数据到结构体 if err := c.BindXML(&user); err != nil { - c.AbortWithStatusJSON(http.StatusOK, helper.BuildErrorResponse(http.StatusBadRequest, "invalid parameter")) + c.AbortWithStatusJSON(http.StatusOK, helper.BuildErrorResponse( + http.StatusBadRequest, + "invalid parameter", + )) return } @@ -314,65 +527,99 @@ func XMLPost(c *gin.Context) { } ``` -js 实现 +**前端实现** ```js +const data = { + name: "shine", + message: "hello gin", + nick: "shineshao" +}; + +// 手动构建 XML 字符串 +const xmlData = `