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 @@ ![build](https://github.com/freeshineit/gin_rotuer_web/workflows/build/badge.svg) -[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 = ` + ${data.name} + ${data.message} + ${data.nick} +`; + axios({ method: "post", url: "/api/xml_post", headers: { "Content-Type": "application/xml", }, - data: `${data.name}${data.message}${data.nick}`, + data: xmlData, +}).then((res) => { + console.log(res.data); + $(".xml-msg").text(`success at ${new Date()}`); }); ``` -## post 提交`multipart/form-data`类型数据(`multipart/form-data`) +**关键知识点:** +- `c.BindXML()` - 解析 XML 请求体并绑定到结构体 +- 需要在结构体标签中指定 `xml:"fieldname"` 标签 +- XML 通常用于旧系统集成、配置文件等场景 -gin 实现文件上传 (api/upload.go) +### 6. 文件上传 (`multipart/form-data`) + +**数据模型** ```go -func fileUpload(c *gin.Context) { +// ChunkFile 分片文件信息 +type ChunkFile struct { + Name string `json:"name" form:"name"` // 文件名 + Chunk int `json:"chunk" form:"chunk"` // 当前分片号 + Chunks int `json:"chunks" form:"chunks"` // 总分片数 +} +``` - filesUrl := make([]string, 0) +**后端实现** (api/upload.go) +```go +// FileUpload 处理文件上传 +func FileUpload(c *gin.Context) { + filesUrl := make([]string, 0) + + // 获取多部分表单数据 form, err := c.MultipartForm() - if err != nil { - log.Println("postMultipleFile error: %s") + log.Println("MultipartForm error: %s", err) + return } + // 获取所有 "file" 字段的文件 files := form.File["file"] - _, err = os.Stat("upload") - - if err != nil { + // 创建 upload 目录(如果不存在) + if _, err := os.Stat("upload"); err != nil { os.Mkdir("upload", os.ModePerm) } + // 保存所有上传的文件 for _, file := range files { - log.Println(file.Filename) - - // Upload the file to specific dst. - if err = c.SaveUploadedFile(file, "upload/"+file.Filename); err != nil { - log.Println("SaveUploadedFile error: %s") + log.Println("Uploading file:", file.Filename) + // 保存文件到指定位置 + if err := c.SaveUploadedFile(file, "upload/"+file.Filename); err != nil { + log.Println("SaveUploadedFile error: %s", err) return } filesUrl = append(filesUrl, "upload/"+file.Filename) } - c.JSON(http.StatusOK, models.BuildResponse(gin.H{ - "urls": filesURL, + c.JSON(http.StatusOK, helper.BuildResponse(gin.H{ + "urls": filesUrl, })) } ``` -html 实现 +**前端实现 - HTML** ```html
+ ``` -js 实现 +**前端实现 - JavaScript** ```js // 单个文件上传 -// var fd = new FormData() -// var file = document.getElementById('file') +// const fd = new FormData() +// const file = document.getElementById('file') // fd.append('file', file.files[0]) +// 多个文件上传 axios({ method: "post", url: "/api/file_upload", headers: { - "Content-Type": "application/form-data", + "Content-Type": "multipart/form-data", }, - // data:fd // 单个文件上传 data: new FormData($("#multipleForm")[0]), }).then((res) => { - console.log(res.data); - const urls = res.data.data.urls || []; - - let imgHtml = ""; + console.log(res.data); + const urls = res.data.data.urls || []; - for (let i = 0; i < urls.length; i++) { - imgHtml += `
/${urls[i]}
`; - } + let imgHtml = ""; + for (let i = 0; i < urls.length; i++) { + imgHtml += `
+ +
${urls[i]}
+
`; + } - $(".file_upload-msg").html( - `
${new Date()}
- ${imgHtml} -
-
` - ); + $(".file_upload-msg").html(`
${new Date()}
${imgHtml}`); }); ``` -[官方文件上传 demo](https://github.com/gin-gonic/examples/tree/master/upload-file) +**关键知识点:** +- `c.MultipartForm()` - 获取多部分表单数据 +- `c.SaveUploadedFile()` - 保存上传的文件 +- `FormData` - JavaScript 用于构建表单数据(包括文件) +- 使用 `multiple` 属性支持多文件选择 + +**参考资源:** [Gin 官方文件上传示例](https://github.com/gin-gonic/examples/tree/master/upload-file) -## 文件分片上传原理 +## 文件分片上传 -客户端会根据文件大小和用户要分片的大小来计算文件分片个数。客户端会一片一片的去请求接口把文件的所有片段上传带服务器端。 +### 工作原理 -服务端接受客户端上传的文件片段进行缓存或创建文件并读入该片段,直至最后一片上传成功。 +文件分片上传是一种大文件上传优化技术,通过将大文件拆分成多个小片段,分别上传到服务器: -> [http://localhost:8080/upload_chunks](http://localhost:8080/upload_chunks) +1. **客户端** - 根据文件大小和设定的分片大小,计算需要的分片数量 +2. **分片上传** - 逐片发送分片数据到服务器 +3. **服务端** - 接收并缓存各片段,当所有片段都收到后,完成文件上传 -## 服务器端 +**优势:** +- 支持断点续传 +- 网络不稳定时可单独重试失败的分片 +- 提高大文件上传的成功率和用户体验 -服务器端使用的是 go 语言的 gin 框架。 +### 服务器实现 + +**数据模型** (models/chunk_file.go) ```go type ChunkFile struct { - Name string `json:"name" form:"name"` - Chunk int `json:"chunk" form:"chunk"` - Chunks int `json:"chunks" form:"chunks"` + Name string `json:"name" form:"name"` // 文件名 + Chunk int `json:"chunk" form:"chunk"` // 当前分片编号(0开始) + Chunks int `json:"chunks" form:"chunks"` // 总分片数 } +``` -func PathExists(path string) (bool, error) { - _, err := os.Stat(path) - if err == nil { - return true, nil - } - if os.IsNotExist(err) { - return false, nil - } - return false, err -} -// 文件分片上传handler -func fileChunkUpload(c *gin.Context) { +**处理函数** (api/upload.go) +```go +// FileChunkUpload 处理文件分片上传 +func FileChunkUpload(c *gin.Context) { var chunkFile ChunkFile r := c.Request + // 绑定分片信息 c.Bind(&chunkFile) - var Buf = make([]byte, 0) - // in your case file would be fileupload - file, _, _ := r.FormFile("file") + // 读取上传的文件数据 + file, _, err := r.FormFile("file") + if err != nil { + log.Println("FormFile error:", err) + return + } - log.Println("this is ", chunkFile.File) - Buf, _ = ioutil.ReadAll(file) + // 将文件内容读入缓冲区 + buf, _ := ioutil.ReadAll(file) - filePath := "upload/"+ chunkFile.Name + // 文件保存路径 + filePath := "upload/" + chunkFile.Name + // 打开文件,以追加模式写入(如果不存在则创建) fd, _ := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) - fd.Write(Buf) + fd.Write(buf) fd.Close() - if chunkFile.Chunk + 1 == chunkFile.Chunks { + // 判断是否是最后一片 + if chunkFile.Chunk+1 == chunkFile.Chunks { + // 所有分片已上传完成 c.JSON(http.StatusOK, gin.H{ "state": "SUCCESS", - "url": "/"+filePath, + "url": "/" + filePath, }) } else { - contentType := strings.Split(c.GetHeader("Content-Type"), "boundary=") - c.String(http.StatusOK, contentType[1]) + // 还有更多分片待上传 + c.String(http.StatusOK, "UPLOADING") } } ``` -[服务端接口完整代码](https://github.com/freeshineit/gin_rotuer_web/blob/master/api) +### 客户端实现 -## 客户端(web) +项目使用 [plupload](https://www.plupload.com/) 插件实现分片上传。该插件自动处理文件分片、并发上传等细节。 -客户端使用的是[plupload](https://www.plupload.com/)文件上传插件,好处是它提供了分片上传,创建对象时配置`chunk_size`属性就可以实现了(插件底层会根据文件大小和`chunk_size`来计算分片的个数)。 +**初始化配置** (templates/upload_chunks.html) ```js var uploader = new plupload.Uploader({ - runtimes: "html5,flash,silverlight,html4", - browse_button: "pickfiles", // you can pass an id... - container: document.getElementById("container"), // ... or DOM Element itself - url: "/api/file_chunk_upload", - flash_swf_url: "/static/js/Moxie.swf", - silverlight_xap_url: "/static/js/Moxie.xap", - chunk_size: "100kb", - filters: { - max_file_size: "10mb", - mime_types: [ - { title: "Image files", extensions: "jpg,gif,png,jpeg" }, - { title: "Zip files", extensions: "zip" }, - ], - }, - - init: { - PostInit: function () { - document.getElementById("filelist").innerHTML = ""; - - document.getElementById("uploadfiles").onclick = function () { - uploader.start(); - return false; - }; - }, - - FilesAdded: function (up, files) { - plupload.each(files, function (file) { - document.getElementById("filelist").innerHTML += - '
' + - file.name + - " (" + - plupload.formatSize(file.size) + - ")
"; - }); - }, - - UploadProgress: function (up, file) { - document.getElementById(file.id).getElementsByTagName("b")[0].innerHTML = - "" + file.percent + "%"; - }, - - Error: function (up, err) { - document - .getElementById("console") - .appendChild( - document.createTextNode("\nError #" + err.code + ": " + err.message) - ); - }, - }, + runtimes: "html5,flash,silverlight,html4", + browse_button: "pickfiles", + container: document.getElementById("container"), + url: "/api/file_chunk_upload", + flash_swf_url: "/static/js/Moxie.swf", + silverlight_xap_url: "/static/js/Moxie.xap", + + // 分片大小:100KB + chunk_size: "100kb", + + // 文件过滤 + filters: { + max_file_size: "10mb", + mime_types: [ + { title: "Image files", extensions: "jpg,gif,png,jpeg" }, + { title: "Zip files", extensions: "zip" }, + ], + }, + + init: { + PostInit: function () { + document.getElementById("filelist").innerHTML = ""; + document.getElementById("uploadfiles").onclick = function () { + uploader.start(); + return false; + }; + }, + + FilesAdded: function (up, files) { + // 文件添加事件 + plupload.each(files, function (file) { + document.getElementById("filelist").innerHTML += + '
' + + file.name + + " (" + + plupload.formatSize(file.size) + + ")
"; + }); + }, + + UploadProgress: function (up, file) { + // 上传进度事件 + document.getElementById(file.id).getElementsByTagName("b")[0].innerHTML = + "" + file.percent + "%"; + }, + + Error: function (up, err) { + // 错误事件 + document + .getElementById("console") + .appendChild( + document.createTextNode( + "\nError #" + err.code + ": " + err.message + ) + ); + }, + + ChunkUploaded: function (up, file, info) { + // 单个分片上传完成事件 + console.log("Chunk uploaded:", file.name, info); + }, + }, }); -uploader.bind("ChunkUploaded", function (up, file, info) { - // do some chunk related stuff - console.log(info); +uploader.init(); +``` + +**关键配置参数说明:** + +| 参数 | 说明 | +|-----|------| +| `url` | 分片上传的服务器接口地址 | +| `chunk_size` | 分片大小(KB 或 MB) | +| `max_file_size` | 允许的最大文件大小 | +| `runtimes` | 支持的运行环境 | +| `FilesAdded` | 文件选择后的回调 | +| `UploadProgress` | 上传进度回调 | +| `ChunkUploaded` | 分片上传完成回调 | + +**演示页面:** [http://localhost:8080/upload_chunks](http://localhost:8080/upload_chunks) + +**参考资源:** +- [服务端接口完整代码](https://github.com/freeshineit/gin_rotuer_web/blob/master/api) +- [客户端文件上传完整代码](https://github.com/freeshineit/gin_rotuer_web/blob/master/templates/upload_chunks.html) +- [完整项目演示](https://github.com/freeshineit/gin_rotuer_web) + +## 最佳实践 + +### 1. 数据绑定 + +```go +// ✅ 推荐:使用 ShouldBind,错误处理更灵活 +var user User +if err := c.ShouldBind(&user); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return +} + +// ❌ 不推荐:使用 Bind,当绑定失败时会自动响应 400 +c.Bind(&user) +``` + +### 2. 响应格式统一 + +```go +// 使用 helper 函数统一响应格式 +c.JSON(http.StatusOK, helper.BuildResponse(data)) +c.JSON(http.StatusBadRequest, helper.BuildErrorResponse(400, "error message")) +``` + +### 3. 文件上传安全性 + +```go +// ✅ 验证文件类型 +allowedTypes := map[string]bool{ + "image/jpeg": true, + "image/png": true, + "image/gif": true, +} + +fileHeader, _ := c.FormFile("file") +if !allowedTypes[fileHeader.Header.Get("Content-Type")] { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid file type"}) + return +} + +// ✅ 限制文件大小 +if fileHeader.Size > 10*1024*1024 { // 10MB + c.JSON(http.StatusBadRequest, gin.H{"error": "file too large"}) + return +} +``` + +### 4. 错误处理 + +```go +// 使用 defer 统一处理错误恢复 +defer func() { + if r := recover(); r != nil { + log.Printf("Panic recovered: %v", r) + c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"}) + } +}() +``` + +### 5. 日志记录 + +```go +// 记录关键操作 +log.Printf("User: %s, Action: upload, Size: %d bytes, Time: %s", + username, + fileHeader.Size, + time.Now().Format("2006-01-02 15:04:05"), +) +``` + +## 常见问题 + +### Q1:上传文件时出现 "invalid request" 错误? +**A:** 检查 `Content-Type` 头是否正确设置为 `multipart/form-data`。使用 Axios 的 FormData 时应该不设置 Content-Type,让浏览器自动处理。 + +```js +// ✅ 正确 +const formData = new FormData(); +formData.append('file', file); +axios.post('/api/file_upload', formData); // 不设置 headers + +// ❌ 错误 +axios.post('/api/file_upload', formData, { + headers: { 'Content-Type': 'application/json' } }); +``` -uploader.init(); +### Q2:如何处理 CORS 跨域问题? +**A:** 使用 Gin 的 CORS 中间件: + +```bash +go get github.com/gin-contrib/cors +``` + +```go +import "github.com/gin-contrib/cors" + +r := gin.Default() +r.Use(cors.Default()) +``` + +### Q3:大文件上传超时如何解决? +**A:** 增加服务器的请求超时时间: + +```go +srv := &http.Server{ + Addr: ":8080", + Handler: r, + ReadTimeout: 15 * time.Second, + WriteTimeout: 15 * time.Second, + IdleTimeout: 60 * time.Second, +} +``` + +### Q4:如何保证文件上传的安全性? +**A:** 采取以下措施: +- 验证文件类型(通过 MIME 类型和文件扩展名) +- 限制文件大小 +- 使用白名单过滤文件名中的特殊字符 +- 存储文件到不可直接访问的目录 +- 使用 HTTPS 传输 + +```go +// 文件名清理示例 +fileName := filepath.Base(fileHeader.Filename) +fileName = strings.Map(func(r rune) rune { + if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '.' || r == '_' || r == '-' { + return r + } + return -1 +}, fileName) ``` -[客户端文件上传完整代码](https://github.com/freeshineit/gin_rotuer_web/blob/master/templates/upload_chunks.html) +## 学习资源 + +- **Gin 官方文档**:https://gin-gonic.com/ +- **Go 标准库文档**:https://golang.org/pkg/ +- **HTTP 协议规范**:https://tools.ietf.org/html/rfc7231 +- **Plupload 文档**:https://www.plupload.com/ + +## 扩展建议 + +1. **数据库集成** - 添加 GORM 或其他 ORM,持久化用户数据 +2. **身份验证** - 实现 JWT 或 Session 认证机制 +3. **日志系统** - 集成 Logrus 或 Zap 提供结构化日志 +4. **限流控制** - 实现请求限流,防止滥用 +5. **单元测试** - 为 API 编写完整的单元测试 +6. **错误监控** - 集成 Sentry 或其他错误监控服务 +7. **性能优化** - 添加缓存、CDN 支持 +8. **文档自动生成** - 使用 Swag 自动生成 OpenAPI 文档 + +## 许可证 -[demo](https://github.com/freeshineit/gin_rotuer_web) +本项目采用 MIT 许可证。详见 LICENSE 文件。 diff --git a/go.mod b/go.mod index 54576bb..b730bc6 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module gin-router-web -go 1.19 +go 1.24.0 require github.com/gin-gonic/gin v1.9.1 @@ -23,10 +23,10 @@ require ( github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.11 // indirect golang.org/x/arch v0.3.0 // indirect - golang.org/x/crypto v0.21.0 // indirect - golang.org/x/net v0.23.0 // indirect - golang.org/x/sys v0.18.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/crypto v0.47.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/text v0.33.0 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 990b3f6..8bbba8a 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,11 @@ +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +github.com/air-verse/air v1.64.0 h1:lKEvIQzaOM5vAVKOdQxq1WrXLfLmzz+e+hPrhmgF6yo= +github.com/air-verse/air v1.64.0/go.mod h1:Dnn4m4DlC9IQiNd3ir57SOdpvGJ3gnC1+OlIGMi2fJY= +github.com/bep/godartsass/v2 v2.5.0 h1:tKRvwVdyjCIr48qgtLa4gHEdtRkPF8H1OeEhJAEv7xg= +github.com/bep/godartsass/v2 v2.5.0/go.mod h1:rjsi1YSXAl/UbsGL85RLDEjRKdIKUlMQHr6ChUNYOFU= +github.com/bep/golibsass v1.2.0 h1:nyZUkKP/0psr8nT6GR2cnmt99xS93Ji82ZD9AgOK6VI= +github.com/bep/golibsass v1.2.0/go.mod h1:DL87K8Un/+pWUS75ggYv41bliGiolxzDKWJAq3eJ1MA= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= @@ -7,6 +15,11 @@ github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583j github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= @@ -14,34 +27,56 @@ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/gohugoio/hugo v0.149.1 h1:uWOc8Ve4h4e48FyYhBquRoHCJviyxA5yGrFJLT48yio= +github.com/gohugoio/hugo v0.149.1/go.mod h1:HS6BP6e8FGxungP4CHC3zeLDvhBLnTJIjHJZWTZjs7o= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= +github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= +github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE= +github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -53,6 +88,10 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/tdewolff/parse/v2 v2.8.3 h1:5VbvtJ83cfb289A1HzRA9sf02iT8YyUwN84ezjkdY1I= +github.com/tdewolff/parse/v2 v2.8.3/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo= +github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= @@ -60,21 +99,25 @@ github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZ golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=