【WEB3.0零基础转行笔记】Go编程篇-第11讲:Gin框架

目录

第一章:Gin基础

[1.1 Gin 介绍](#1.1 Gin 介绍)

[1.1.1 Gin基础](#1.1.1 Gin基础)

[1.1.1 Gin在Web3.0开发中的使用场景](#1.1.1 Gin在Web3.0开发中的使用场景)

[1.2 Gin 环境搭建](#1.2 Gin 环境搭建)

[1.3 Go 程序的热加载](#1.3 Go 程序的热加载)

[1.4 Gin框架中的路由](#1.4 Gin框架中的路由)

[1.4.1 路由概述](#1.4.1 路由概述)

[1.4.2 简单的路由配置](#1.4.2 简单的路由配置)

[1.4.3 Postman](#1.4.3 Postman)

[1.4.3 c.String() c.JSON() c.JSONP() c.XML() c.HTML()](#1.4.3 c.String() c.JSON() c.JSONP() c.XML() c.HTML())

[1.4.5 Gin HTML 模板渲染](#1.4.5 Gin HTML 模板渲染)

[1.4.6 静态文件服务](#1.4.6 静态文件服务)

[1.5 路由详解](#1.5 路由详解)

[1.5.1 GET POST 以及获取 Get Post 传值](#1.5.1 GET POST 以及获取 Get Post 传值)

[1.5.2 简单的路由组](#1.5.2 简单的路由组)

[1.5.3 Gin 路由 文件 分组](#1.5.3 Gin 路由 文件 分组)

[第二章:Gin 中自定义控制器](#第二章:Gin 中自定义控制器)

[2.1 Gin 中自定义控制器](#2.1 Gin 中自定义控制器)

[2.2 控制器分组](#2.2 控制器分组)

[2.3 控制器的继承](#2.3 控制器的继承)

[第三章:Gin 中间件](#第三章:Gin 中间件)

[3.1 路由中间件](#3.1 路由中间件)

[3.2 ctx.Next()调用该请求的剩余处理程序](#3.2 ctx.Next()调用该请求的剩余处理程序)

[3.3 一个路由配置多个中间件的执行顺序](#3.3 一个路由配置多个中间件的执行顺序)

[3.4 c.Abort()--(了解)](#3.4 c.Abort()--(了解))

[3.5 全局中间件](#3.5 全局中间件)

[3.6 在路由分组中配置中间件](#3.6 在路由分组中配置中间件)

[3.7 中间件和对应控制器之间共享数据](#3.7 中间件和对应控制器之间共享数据)

[3.8 中间件注意事项](#3.8 中间件注意事项)

[第四章 Gin 中自定义 Model](#第四章 Gin 中自定义 Model)

[4.1 关于 Model](#4.1 关于 Model)

[4.1.1 Model 里面封装公共的方法](#4.1.1 Model 里面封装公共的方法)

[4.1.2 控制器中调用 Model](#4.1.2 控制器中调用 Model)

[4.1.3 main函数](#4.1.3 main函数)

[4.2 Golang MD5 加密](#4.2 Golang MD5 加密)

[第五章 Gin 文件上传](#第五章 Gin 文件上传)

[5.1 单文件上传](#5.1 单文件上传)

[5.2 多文件上传--不同名字的多个文件](#5.2 多文件上传--不同名字的多个文件)

[5.3 多文件上传--相同名字的多个文件](#5.3 多文件上传--相同名字的多个文件)

[5.4 文件上传--按照日期存储](#5.4 文件上传--按照日期存储)

[第六章:Gin 中的 Cookie 和 Session](#第六章:Gin 中的 Cookie 和 Session)

[6.1 Cookie 介绍](#6.1 Cookie 介绍)

[6.1.1 为什么需要 Cookie?](#6.1.1 为什么需要 Cookie?)

[6.1.2 Cookie 的核心作用:为无状态协议增加记忆能力](#6.1.2 Cookie 的核心作用:为无状态协议增加记忆能力)

[6.1.3 关于 Cookie 的存储特性](#6.1.3 关于 Cookie 的存储特性)

[6.1.4 Cookie 能实现的功能](#6.1.4 Cookie 能实现的功能)

[6.1.5 设置和获取 Cookie](#6.1.5 设置和获取 Cookie)

[6.1.6 多个二级域名共享 cookie](#6.1.6 多个二级域名共享 cookie)

[6.2 Gin 中的 Session](#6.2 Gin 中的 Session)

[6.2.1 Session 简单介绍](#6.2.1 Session 简单介绍)

[6.2.2 Session 的工作流程](#6.2.2 Session 的工作流程)

[6.2.3 Gin 中使用 Session](#6.2.3 Gin 中使用 Session)

[6.2.4 基于 Cookie 存储 Session](#6.2.4 基于 Cookie 存储 Session)

[6.2.5 基于 Redis 存储 Session](#6.2.5 基于 Redis 存储 Session)

[第七章:Gin 中使用 GORM 操作 PostgreSQL 数据库](#第七章:Gin 中使用 GORM 操作 PostgreSQL 数据库)

[7.1 GORM 详情](#7.1 GORM 详情)

[7.1.1 GORM基础](#7.1.1 GORM基础)

[7.1.2 Gorm 特性](#7.1.2 Gorm 特性)

[7.1.3 Gin 中使用 GORM](#7.1.3 Gin 中使用 GORM)

[7.1.4 GORM CURD](#7.1.4 GORM CURD)

[7.1.5 Gin GORM 查询语句详解](#7.1.5 Gin GORM 查询语句详解)

[7.1.6 Gin GORM 查看执行的 sql](#7.1.6 Gin GORM 查看执行的 sql)

[7.2 原生 SQL 和 SQL 生成器](#7.2 原生 SQL 和 SQL 生成器)

[7.2.1 原生 sql 删除 表中的一条数据](#7.2.1 原生 sql 删除 表中的一条数据)

[7.2.2 使用原生 sql 修改 user 表中的一条数据](#7.2.2 使用原生 sql 修改 user 表中的一条数据)

[7.2.3 查询特定条件的数据(查询 uid=2 的数据)](#7.2.3 查询特定条件的数据(查询 uid=2 的数据))

[7.2.4 查询 User 表中所有的数据](#7.2.4 查询 User 表中所有的数据)

[7.2.5 统计 user 表的数量](#7.2.5 统计 user 表的数量)

[7.3 Gin中使用 GORM 实现表关联查询](#7.3 Gin中使用 GORM 实现表关联查询)

[7.3.1 一对一](#7.3.1 一对一)

[7.3.2 一对多](#7.3.2 一对多)

[7.3.3 多对多](#7.3.3 多对多)

[7.4 GORM 中使用事务](#7.4 GORM 中使用事务)

[7.4.1 禁用默认事务](#7.4.1 禁用默认事务)

[7.4.2 事务](#7.4.2 事务)

[第八章:Gin 中使用 go-ini 加载.ini 配置文件](#第八章:Gin 中使用 go-ini 加载.ini 配置文件)

[18.1、go-ini 介绍](#18.1、go-ini 介绍)

[18.2、go-ini 使用](#18.2、go-ini 使用)

[18.3、从.ini 中读取 mysql 配置](#18.3、从.ini 中读取 mysql 配置)


第一章:Gin基础

1.1 Gin 介绍

1.1.1 Gin基础

Gin 是一个 Go (Golang) 编写的轻量级 http web 框架,运行速度非常快,如果你是性能和高效的追求者,我们推荐你使用 Gin 框架。Gin 最擅长的就是 API 接口的高并发,如果项目的规模不大,业务相对简单,这个时候我们也推荐您使用 Gin。当某个接口的性能遭到较大挑战的时候,这个还是可以考虑使用 Gin 重写接口。 Gin也是一个流行的 golang Web 框架,Github Strat 量已经超过了 50k。

Gin 的官网:https://gin-gonic.com/zh-cn/

Gin Github 地址:https://github.com/gin-gonic/gin

1.1.1 Gin在Web3.0开发中的使用场景

在Web3.0开发中,Gin框架主要担当高性能 API网关业务逻辑编排层 的角色,它位于前端应用区块链网络之间,负责处理所有与链交互的复杂性和通用逻辑。其核心使用场景可以从三个层面来详细阐述:

场景一:作为 RESTful API 服务网关 ,封装链上交互

这是Gin在Web3.0中最核心的应用。由于区块链节点通信基于JSON-RPC等协议,对于前端应用不够友好,Gin可以将这些复杂的链上操作封装成标准的RESTful API

  • 智能合约 服务构建 :通过集成 go-ethereum 库中的 ethclient,Gin 可以轻松地与以太坊等区块链节点通信。同时,结合 abigen 工具,可以将Solidity智能合约的ABI转换为类型安全的Go代码。在Gin的路由处理函数中直接调用这些生成的合约方法,实现对合约状态的查询(如eth_call)或交易构建。

    Go 复制代码
    // 示例:在Gin handler中查询ERC-20代币余额
    r.GET("/balance/:address", func(c *gin.Context) {
        addr := common.HexToAddress(c.Param("address"))
        balance, err := contract.BalanceOf(&bind.CallOpts{}, addr)
        // ... 处理响应
    })
  • 数字货币 支付集成:Gin 可以接收支付请求,在后端构造并签名交易,然后广播到区块链网络。这包括了复杂的交易逻辑,如UTXO模型的处理、EIP-1559费用市场的适配以及Gas策略的优化。

🛡️场景二:利用中间件处理Web3.0通用 横切关注点

Gin的中间件机制在Web3.0中得到了全新的应用,用于处理所有与具体业务无关的通用逻辑。

  • 去中心化身份( DID )验证 :不再使用传统的session,中间件可以从请求头中解析出钱包地址和签名,通过ecrecover等密码学方法恢复出签名者公钥,验证其对某个链上地址的控制权(ownership),验证通过后将地址注入到gin.Context中,供后续业务使用。

  • 链上数据 预加载 与Gas优化:针对用户资产仪表盘等高频查询场景,中间件可以批量、并发地从区块链节点预加载多个数据(如ETH余额、不同代币的余额),并进行短暂缓存。这极大地减少了RPC调用次数,提升了接口响应速度,对于按次计费的节点服务而言,也优化了潜在的Gas成本。

  • 跨链 请求路由 :对于支持多链的应用(如跨链桥),中间件可以根据请求参数(如 chain=ethchain=bsc)动态选择并注入对应区块链的客户端实例(RPC连接池),使得上层业务代码无需感知底层链的差异,实现优雅的跨链逻辑解耦。

  • 合规与风控拦截:在涉及现实世界资产(RWA)的应用中,中间件可以拦截关键操作(如提现),调用链外的合规风控服务,验证目标地址是否在制裁名单(如OFAC名单)中,或交易是否符合反洗钱(AML)规则,从而在前端请求到达业务逻辑前就进行拦截。

🌐 场景三:作为Web3.0应用的后端核心,实现完整业务闭环

一个完整的Web3.0应用往往不只有链上部分,还需要链下服务的配合,Gin在此处扮演了粘合剂和业务中枢的角色。

  • 区块链食品溯源系统:在典型的区块链溯源应用中,Gin 可以作为API Gateway,一方面处理来自Vue.js前端的用户认证(如JWT)、请求路由和限流,另一方面与底层的联盟链(如Hyperledger Fabric)交互,调用链码(智能合约)来记录和查询产品从生产、物流到零售的全生命周期数据。Gin 服务端将复杂的区块链网络细节屏蔽,为前端提供清晰、简洁的RESTful API。

  • 物联网 +区块链的数据可信注入:以苏格兰Roehill Springs金酒酒厂的案例为参考,物联网传感器采集的水源数据可以通过后端服务(理论上可以使用Gin构建)处理后,直接写入区块链,确保数据从源头就是不可篡改和真实可信的。消费者通过扫描酒瓶上的二维码,即可通过这个后端服务提供的API查询到存储在区块链上的完整产品溯源信息。

总而言之,Gin在Web3.0中的价值在于,它作为一个高效、灵活且成熟的Web框架,完美地填补了前端 用户界面 与后端去中心化基础设施之间的空白,让开发者可以更专注于业务逻辑本身,而将复杂的链交互、身份验证、数据预加载等通用复杂性封装在清晰、可复用的中间件和API层中。

1.2 Gin 环境搭建

要安装 Gin 软件包,需要先安装 Go 并设置 Go 工作区。

通过 go get 命令来下载和安装 Gin 框架,但遇到了错误提示 go.mod file not found。这是因为 Go 1.17 版本之后,go get 命令不再支持在模块外使用。

你可以通过以下步骤来安装 Gin:

1、打开终端或命令提示符。

2、确保你在一个 Go 模块目录中,或者创建一个新的模块目录并初始化模块:

bash 复制代码
mkdir -p myproject cd myproject 
go mod init myproject

3、使用 go get 命令来下载 Gin:

bash 复制代码
go get -u github.com/gin-gonic/gin

或者使用 go install 命令:

bash 复制代码
go install github.com/gin-gonic/gin@latest

这将下载 Gin 框架并将其添加到你的 Go 模块依赖中。之后,你就可以在你的 Go 项目中导入并使用 Gin 了。

确保你的 Go 环境已经设置正确,并且你的 GOPATH 环境变量包含了 Go 的可执行文件路径。如果仍然遇到问题,请检查你的 Go 版本是否是最新的,或者尝试更新 Go 到最新版本。

go install 命令会将包安装到你的 Go 环境的全局缓存目录中,通常是 $GOPATH/pkg/mod$GOPATH/pkg/mod,这使得安装的包可以在任何地方被导入和使用,而不仅仅是在当前的项目中。

这意味着,当你使用 go install 命令安装一个包时,它的效果是全局的。无论你在哪个项目中,只要正确地导入了包的路径,就可以使用该包。这对于库(library)来说非常有用,因为它们可以在多个项目之间共享。

例如,如果你安装了 github.com/gin-gonic/gin,那么在任何 Go 项目中,你都可以这样导入 Gin:

bash 复制代码
import "github.com/gin-gonic/gin"

然后使用 Gin 提供的各种功能。

如果你想要在特定项目中使用 go get 来下载依赖,而不是全局安装,你应该在项目的根目录下运行 go get 命令,该目录应该包含 go.mod 文件。这样下载的依赖将只属于该项目,不会影响其他项目。

bash 复制代码
cd myproject
go mod tidy  # 初始化或更新模块
go get github.com/gin-gonic/gin

这样,Gin 将作为模块依赖下载到 myproject/go.modmyproject/go.sum 文件中,仅在 myproject 项目中可用。

1.下载并安装 gin:

bash 复制代码
$ go get -u github.com/gin-gonic/gin

2.将 gin 引入到代码中:

Go 复制代码
import "github.com/gin-gonic/gin"

3.(可选)如果使用诸如 http.StatusOK 之类的常量,则需要引入 net/http 包:

Go 复制代码
import "net/http"

4、新建 Main.go 配置路由

Go 复制代码
package main

import "github.com/gin-gonic/gin"

func main() {
    // 创建一个默认的路由引擎
    r := gin.Default()
    // 配置路由
    r.GET("/", func(c *gin.Context) {
        c.JSON(200, gin.H{ // c.JSON:返回 JSON 格式的数据
            "message": "Hello world!"})
    })
    // 启动 HTTP 服务,默认在 0.0.0.0:8080 启动服务
    r.Run()
}

r := gin.Default() 是使用 Gin 框架创建了一个默认的路由引擎实例。

什么是路由引擎?

路由引擎是 Gin 框架的核心组件,它本质上是一个HTTP 请求的 多路复用器 (或者说路由器)。它的主要职责是:根据接收到的 HTTP 请求的 URL 路径HTTP 方法(如 GET、POST),找到预先注册好的对应处理函数,并将请求交由该函数处理,最终将响应返回给客户端。

它的作用是什么?

  • 请求分发 :将不同的请求精准地导向对应的业务逻辑。例如,GET /balance 可能去查询账户余额,POST /transaction 则处理一笔新交易。

  • 中间件集成:Gin 的路由引擎可以挂载中间件(如日志记录、错误恢复、身份验证),在请求到达处理函数之前或之后执行通用逻辑。

  • 组织 API :可以方便地创建路由组(如 /api/v1),使代码结构清晰,便于维护。

在区块链后端的场景中,路由引擎的作用就是将前端或外部调用的 HTTP 请求(如查询区块、发送交易)转换成与区块链节点交互的具体操作,并返回结果。

简单来说,gin.Default() 返回的这个引擎就是整个 Web 服务的"交通指挥中心",负责把所有来访请求准确、高效地分配给对应的"工作人员"(处理函数)。

5、运行你的项目

Go 复制代码
go run main.go

6、运行成功后在如下的链接中便可看到运行的结果

Go 复制代码
http://localhost:8080/

7、要改变默认启动的端口

Go 复制代码
// 声明这个文件是一个可独立运行的程序(而不是一个可以被其他程序使用的库)。
// 所有Go可执行程序都必须包含一个 package main 以及一个 main 函数。
package main

// import 语句用来引入其他包,以便使用它们提供的功能。
// "github.com/gin-gonic/gin" 是 Gin 框架的包,它帮助我们快速构建Web服务(处理HTTP请求和响应)。
import "github.com/gin-gonic/gin"

// main 函数是程序的入口点,当程序启动时,会自动执行这个函数里的代码。
func main() {
        // 创建一个"默认的路由引擎"实例,命名为 r。
        // "路由引擎"可以理解为Web服务的"交通指挥中心",它负责接收所有来自外部的HTTP请求,
        // 然后根据请求的地址(比如 "/" 或 "/hello")和请求方法(比如 GET、POST),
        // 将请求交给对应的处理函数去处理。
        // gin.Default() 创建的路由引擎已经自动附带了一些常用的中间件(比如日志记录、错误恢复),
        // 方便我们快速开始。
        r := gin.Default()

        // 配置路由:告诉路由引擎,当收到一个 GET 请求,且请求的路径是 "/" 时,
        // 应该执行后面定义的函数。
        // GET 是HTTP协议中最常用的请求方法之一,通常用于获取数据。
        // 这里我们提供了一个匿名函数(也就是没有名字的函数)来处理这个请求。
        r.GET("/", func(c *gin.Context) {
                // c 是 *gin.Context 类型的参数,它代表了当前的请求上下文,
                // 通过它可以获取请求的信息(比如请求头、参数),也可以设置响应的内容。
                // c.JSON 是一个方法,用来返回一个 JSON 格式的数据给客户端。
                // 第一个参数 200 是HTTP状态码,表示请求成功。
                // 第二个参数 gin.H{"message": "Hello world!"} 是要返回的数据。
                // gin.H 是 Gin 框架提供的一个快捷方式,本质上是一个 map(键值对集合),
                // 用来方便地构建 JSON 对象。这里它构建了一个包含 "message" 键,
                // 值为 "Hello world!" 的 JSON 对象。
                c.JSON(200, gin.H{
                        "message": "Hello world!",
                })
        })

        // 启动HTTP服务,让程序开始监听来自网络的请求。
        // 默认情况下,它会在本机的所有IP地址(0.0.0.0)上监听端口 8080。
        // 也就是说,当程序运行后,你可以在浏览器里访问 http://localhost:8080/,
        // 就会看到我们刚才配置的 JSON 响应。
        // 如果端口被占用或发生其他错误,r.Run() 会返回错误信息并终止程序。
        r.Run()
}

补充解释(针对零基础读者):

  • HTTP 请求:就像你访问网站时,浏览器向服务器发送的"我要看某某页面"的消息。常见的请求方法有 GET(获取信息)和 POST(提交信息)。

  • JSON :一种轻量级的数据交换格式,类似于 {"message":"Hello world!"},易于人阅读和机器解析。

  • 路由:相当于一个"路牌",指引不同的请求去往不同的处理函数。

  • 匿名函数 :这里 func(c *gin.Context){...} 是一个没有名字的函数,直接写在 r.GET 的括号里,表示这个函数专属于这个路由。

现在,当你运行这段代码时,你的电脑就变成了一个简单的Web服务器,可以对外提供API服务了。

如果 go get 失败请参考: http://bbs.itying.com/topic/5ed08edee7c0790f8475e276

1.3 Go 程序的热加载

所谓热加载就是当我们对代码进行修改时,程序能够自动重新加载并执行,这在我们开发中是非常便利的,可以快速进行代码测试,省去了每次手动重新编译。beego 中我们可以使用官方给我们提供的 bee 工具来热加载项目,但是 Gin 中并没有官方提供的热加载工具,这个时候我们要实现热加载就可以借助第三方的工具。

Beego 是一款开源的、国产的、一站式的 Go 语言 Web 框架,你可以把它想象成一个功能齐全的 "全家桶" 工具箱,专门用来快速构建 Web 应用、API 接口和后端服务。

它有几个非常鲜明的特点:

  • 功能全面,开箱即用:与 Gin 这样轻量级的框架不同,beego 不仅包含了路由模块,还内置了操作数据库的 ORM、会话管理、日志记录、缓存、配置解析等八大独立模块。这意味着你拿到手就能开始搭建复杂的应用,不用再四处寻找和拼凑第三方库。

  • 采用经典的 MVC 架构:它强制将应用分为模型(Models,处理数据)、视图(Views,渲染页面)和控制器(Controllers,处理逻辑)三层。这种清晰的项目结构让代码易于维护,特别适合中大型项目的团队协作。

  • 自带强大的开发工具 :beego 有一个与之配套的命令行工具 bee。它可以帮你一键创建项目、在开发时自动监测代码变化并热重启,甚至能自动化生成 API 文档,极大地提升了开发效率。

  • 应用场景广泛:它既能用来编写传统的 Web 网站,也很适合作为后端,快速开发 RESTful API 为前端(如手机 App 或 Vue 应用)提供数据服务。

总而言之,如果你需要一个结构规范、功能丰富、能应对复杂业务逻辑 的框架,beego 会是一个很顺手的选择。这就像是,你之前了解的 Gin 可能更像是一个提供基础工具的轻量级 "工具箱",而 beego 则是一个包含了设计图纸和全套工具的 "移动板房",能帮你更快地搭建起完整的应用架构。

工具 1(推荐)https://github.com/gravityblast/fresh

bash 复制代码
// 在 PowerShell 中安装 fresh
go install github.com/pilu/fresh@latest
复制代码
bash 复制代码
// 在VSCode终端中执行如下脚本 PS C:\Users\Windows\myproject> fresh

在更改脚本时,它会立刻发生变化,不需要重新启动。

**工具 2:**https://github.com/codegangsta/gin

bash 复制代码
go get -u github.com/codegangsta/gin 
D:\gin_demo>gin run main.go

1.4 Gin框架中的路由

1.4.1 路由概述

路由(Routing)是由一个 URI (或者叫路径)和一个特定的 HTTP 方法(GET、POST 等)组成的 ,涉及到应用如何响应客户端对某个网站节点的访问。RESTful API 是目前比较成熟的一套互联网应用程序的 API 设计理论,所以我们设计我们的路由的时候建议参考 RESTful API 指南。

在 RESTful 架构中,每个网址代表一种资源,不同的请求方式表示执行不同的操作:

HTTP 方法 含义
GET (SELECT) 从服务器取出资源(一项或多项)
POST (CREATE) 在服务器新建一个资源
PUT (UPDATE) 在服务器更新资源(客户端提供改变后的完整资源)
DELETE (DELETE) 从服务器删除资源

1.4.2 简单的路由配置

简单的路由配置(可以通过 postman 测试)

1.GET请求

GET 请求访问网址,就是浏览器先把域名解析成 IP、建立 TCP 连接,然后向服务器发送 "获取指定资源" 的请求,服务器找到对应内容后返回数据,浏览器接收并渲染展示,最后关闭连接,全程只查不取、不修改数据。

Go 复制代码
package main

import "github.com/gin-gonic/gin" // 导入 Gin 框架核心包,用于构建 Web 服务

func main() {
    // 创建一个默认的路由引擎
    // gin.Default() 会返回一个 *gin.Engine 实例,并自动附加 Logger 和 Recovery 两个中间件
    // Logger 用于打印请求日志,Recovery 用于捕获 panic 并返回 500 错误,避免服务崩溃
    r := gin.Default()

    // 注册一个处理 GET 请求的路由
    // r.GET() 方法有两个参数:
    // 1. 路由路径 "/":表示网站的根路径
    // 2. 处理函数 func(c *gin.Context):当有 GET 请求访问根路径时,会执行这个匿名函数
    //    *gin.Context 是请求上下文对象,封装了请求和响应的所有方法
    r.GET("/", func(c *gin.Context) {
        // c.String() 用于返回纯文本格式的 HTTP 响应
        // 参数1: HTTP 状态码 (200 表示 OK) -> http.StatusOk
        // 参数2: 响应内容的字符串
        c.String(200, "Get")
    })

    // 启动 HTTP 服务
    // r.Run() 会调用 http.ListenAndServe() 启动服务
    // 参数 ":8080" 表示监听本机所有 IP 地址的 8080 端口
    // 如果不传参数,默认监听 :8080
    r.Run(":8080")
}

2.POST请求

POST 请求访问网址,是浏览器先将域名解析为服务器 IP、建立 TCP 连接,随后向服务器发送包含请求头和藏在请求体里的提交数据(如表单、JSON)的 POST 请求,服务器接收并处理这些数据(如存储、校验、计算)后,生成包含处理结果的响应报文返回,浏览器接收并展示结果,最后关闭连接,全程核心是向服务器提交 / 修改数据,而非仅获取资源。

Go 复制代码
package main

import "github.com/gin-gonic/gin"

func main() {
    // 创建一个默认的路由引擎
    r := gin.Default()
    
    r.POST("/", func(c *gin.Context) {
        c.String(200, "POST")
    })

    // 启动 HTTP 服务,默认在 0.0.0.0:9090 启动服务
    r.Run(":9090")
}

3、PUT请求

PUT 请求访问网址 ,是浏览器先将域名解析为服务器 IP、建立 TCP 连接,随后向服务器发送包含完整资源数据的 PUT 请求(数据存于请求体),明确要求服务器按指定 URL 完整替换对应资源(若资源不存在则新建),服务器接收并处理数据(如覆盖 / 创建资源)后返回处理结果,浏览器接收结果,最后关闭连接,全程核心是对指定资源做 "全量更新 / 创建",是幂等操作(多次请求结果一致)。

Go 复制代码
package main

import "github.com/gin-gonic/gin"

func main() {
    // 创建一个默认的路由引擎
    r := gin.Default()

    r.PUT("/", func(c *gin.Context) {
        c.String(200, "PUT")
    })

    // 启动 HTTP 服务,默认在 0.0.0.0:9090 启动服务
    r.Run(":9090")
}

当用 DELETE 访问一个网址的时候,执行的操作:

Go 复制代码
package main

import "github.com/gin-gonic/gin"

func main() {
    // 创建一个默认的路由引擎
    r := gin.Default()

    r.DELETE("/", func(c *gin.Context) {
        c.String(200, "DELETE")
    })

    // 启动 HTTP 服务,默认在 0.0.0.0:9090 启动服务
    r.Run(":9090")
}

路由里面获取 Get 传值

域名/news?aid=20

Go 复制代码
package main

// 导入 Gin 框架包,Gin 是一个用 Go 语言编写的 HTTP Web 框架,
// 它提供了高性能、简洁的 API,用于快速构建 Web 应用程序和微服务。
import "github.com/gin-gonic/gin"

// main 函数是程序的入口点,当程序启动时会自动执行该函数。
func main() {
    // gin.Default() 创建一个 Gin 引擎实例,并附加了两个默认的中间件:
    // 1. Logger 中间件:用于记录 HTTP 请求的日志,包括请求方法、路径、状态码、耗时等。
    // 2. Recovery 中间件:用于捕获任何在请求处理过程中发生的 panic,并返回 500 错误,避免程序崩溃。
    // 返回的 *gin.Engine 对象用于注册路由、定义中间件和启动 HTTP 服务。
    r := gin.Default()

    // r.GET() 方法用于注册一个处理 HTTP GET 请求的路由。
    // 第一个参数是路由的相对路径(例如 "/new"),当客户端以 GET 方式请求该路径时,会执行第二个参数传入的处理函数。
    // 第二个参数是一个 gin.HandlerFunc 类型的函数,它接收一个 *gin.Context 指针作为参数。
    // gin.Context 封装了 HTTP 请求和响应的所有信息,是处理请求的核心对象。
    r.GET("/new", func(c *gin.Context) {
        // c.Query() 方法从 URL 的查询字符串中获取指定参数的值。
        // 查询字符串是 URL 中 "?" 后面的部分,格式为 key=value,多个参数用 "&" 连接。
        // 例如:请求 "/new?aid=123" 时,c.Query("aid") 将返回字符串 "123"。
        // 如果参数不存在,则返回空字符串 ""。
        aid := c.Query("aid")

        // c.String() 方法用于向客户端发送一个字符串类型的 HTTP 响应。
        // 第一个参数是 HTTP 状态码,例如 200 表示 OK。
        // 第二个参数是一个格式化字符串,可以包含占位符(如 %s),
        // 后续参数将按照格式化规则填充到占位符中。
        // 这里将状态码 200 和格式化后的字符串 "aid=123" 返回给客户端。
        c.String(200, "aid=%s", aid)
    })

    // r.Run() 方法用于启动 HTTP 服务并监听传入的连接。
    // 它接受一个可选参数作为服务地址,格式为 "host:port"。
    // 如果只指定端口(如 ":9090"),则默认监听所有网络接口(0.0.0.0)。
    // 调用该方法会阻塞当前 goroutine,直到服务因错误而停止(例如端口被占用)。
    // 如果服务启动成功,将一直运行,直到程序被强制终止。
    r.Run(":9090")
}

动态路由

域名/user/20

Go 复制代码
package main

import "github.com/gin-gonic/gin"

func main() {
    // 创建一个默认的路由引擎
    r := gin.Default()

    r.GET("/user/:uid", func(c *gin.Context) {
        uid := c.Param("uid")
        c.String(200, "userID=%s", uid)
    })

    // 启动 HTTP 服务,默认在 0.0.0.0:9090 启动服务
    r.Run(":9090")
}

汇总后的代码:

Go 复制代码
package main

import (
    "net/http"

    "github.com/gin-gonic/gin"
)

func main() {
    // 创建一个默认的路由引擎
    r := gin.Default()

    // 配置路由
    r.GET("/get", func(c *gin.Context) {
        c.String(http.StatusOK, "这是一个Get请求,主要用于从服务器取出资源(一项或多项)")
    })

    // 配置路由
    r.POST("/post", func(c *gin.Context) {
        c.String(http.StatusOK, "这是一个POST请求,主要用于在服务器新建一个资源")
    })

    // 配置路由
    r.PUT("/put", func(c *gin.Context) {
        c.String(http.StatusOK, "这是一个PUT请求,主要用于在服务器更新资源(客户端提供改变后的完整资源)")
    })

    // 配置路由
    r.DELETE("/delete", func(c *gin.Context) {
        c.String(http.StatusOK, "这是一个DELETE请求,主要用于在从服务器删除资源")
    })
    // 启动 HTTP 服务,默认在 0.0.0.0:9090 启动服务
    r.Run(":8080")
}

1.4.3 Postman

Postman 是一款广泛使用的 API 开发和测试工具,它通过直观的图形化界面,让开发者能够轻松地构建、发送 HTTP 请求(如 GET、POST 等)并查看服务器返回的响应数据。除了基础的请求调试功能,Postman 还提供了集合管理、环境变量、自动化测试脚本、API 文档生成以及团队协作等强大特性,极大地提升了后端开发、接口测试和前后端联调的效率,成为现代 Web 和移动应用开发流程中不可或缺的一部分。

在下载页面选择

  1. 下载Postman桌面版:https://www.postman.com/downloads/

  2. 安装后打开应用

  3. 在登录/注册界面选择 "Create Account"

  4. 选择免费计划继续

使用方法:

1.4.3 c.String() c.JSON() c.JSONP() c.XML() c.HTML()

c.String()c.JSON()c.JSONP()c.XML()c.HTML() 是 Gin 框架中 *gin.Context 提供的用于向客户端发送 HTTP 响应的方法 ,它们根据不同的数据格式自动设置对应的 Content-Type 头:c.String() 返回纯文本(text/plain),适用于简单的字符串消息;c.JSON() 返回 JSON 格式(application/json),将传入的 Go 对象(如结构体、切片或 gin.H)序列化为 JSON;c.JSONP() 用于跨域请求,如果请求参数中包含回调函数名,则返回被该函数包裹的 JSON(application/javascript),否则返回普通 JSON;c.XML() 返回 XML 格式(application/xml),常用于传统 Web 服务或需要 XML 交互的 API;c.HTML() 则渲染 HTML 模板(text/html),需要先加载模板文件并将数据注入模板后生成最终的 HTML 页面返回。

1、c.String() 返回一个字符串

c.String() 是 Gin 框架中 *gin.Context 的一个方法,用于向客户端返回纯文本格式的 HTTP 响应 ,它会自动设置响应头为 text/plain; charset=utf-8,支持类似 fmt.Sprintf 的格式化字符串功能,开发者可以传入状态码格式字符串可变参数来构建响应内容,适用于返回简单的文本消息、错误提示或调试信息等无需复杂数据结构的场景。

Go 复制代码
package main

// 导入所需的包
import (
    "net/http" // 提供 HTTP 状态码常量,如 http.StatusOK (200)
    "github.com/gin-gonic/gin" // Gin Web 框架,用于快速构建 HTTP 服务
)

// main 函数是程序的入口点
func main() {
    // 创建一个默认配置的 Gin 引擎实例
    // gin.Default() 会附加两个默认中间件:Logger(日志记录)和 Recovery(panic 恢复)
    r := gin.Default()

    // 配置路由:处理 GET 请求,路径为 "/news"
    // r.GET() 方法注册一个路由和处理函数,当客户端以 GET 方式访问 /news 时,会执行后面的匿名函数
    r.GET("/news", func(c *gin.Context) {
        // c.Query() 从 URL 的查询字符串中获取参数 "aid" 的值
        // 例如请求 "/news?aid=123" 时,aid 的值为 "123"
        // 如果参数不存在,则返回空字符串 ""
        aid := c.Query("aid")

        // c.String() 向客户端发送一个纯文本(text/plain)的 HTTP 响应
        // 第一个参数是 HTTP 状态码,这里使用 net/http 包中的常量 http.StatusOK(即 200)
        // 第二个参数是格式化字符串,支持类似 fmt.Printf 的占位符(如 %s)
        // 后续参数用于替换占位符,这里将 aid 的值填入 %s
        // 最终响应的内容为 "aid=xxx"
        c.String(http.StatusOK, "aid=%s", aid)
    })

    // 启动 HTTP 服务,监听在 8080 端口
    // r.Run() 默认监听所有网络接口(0.0.0.0),可以指定端口,如 ":8080"
    // 该方法会阻塞,直到服务因错误而停止(例如端口被占用或程序被中断)
    r.Run(":8080")
}

当URL中123时,会作为入参传进入。

2、c.JSON() 返回一个 JSON 数据

Go 复制代码
package main

// 导入程序所需的包
import (
    "net/http" // 提供 HTTP 状态码常量(如 http.StatusOK)和 HTTP 相关功能
    "github.com/gin-gonic/gin" // Gin 是一个高性能的 Go Web 框架,用于快速构建 RESTful API 和 Web 服务
)

// main 函数是 Go 程序的入口点,当程序启动时自动执行
func main() {
    // gin.Default() 创建一个默认配置的 Gin 引擎实例
    // 该实例预置了两个有用的中间件:
    //   1. Logger 中间件:自动记录每个 HTTP 请求的详细信息(方法、路径、状态码、耗时等)
    //   2. Recovery 中间件:捕获处理函数中发生的任何 panic,并返回 500 错误,防止服务崩溃
    // 返回的 r 是一个 *gin.Engine 类型,代表整个 Web 服务,用于注册路由、绑定中间件和启动服务
    r := gin.Default()

    // r.GET() 方法用于注册一个处理 HTTP GET 请求的路由
    // 第一个参数是路由的路径(例如 "/someJSON"),第二个参数是一个处理函数(类型为 gin.HandlerFunc)
    // 当客户端以 GET 方式请求该路径时,Gin 会调用这个处理函数,并将请求上下文(*gin.Context)作为参数传入
    r.GET("/someJSON", func(c *gin.Context) {
        // c.JSON() 是 *gin.Context 提供的方法,用于向客户端返回 JSON 格式的响应
        // 它接收两个参数:
        //   - 第一个参数:HTTP 状态码,这里使用 net/http 包的常量 http.StatusOK(值为 200),表示请求成功
        //   - 第二个参数:要序列化为 JSON 的数据,可以是任意 Go 类型(如结构体、切片、map 等)
        // gin.H 是 map[string]interface{} 的预定义别名,用于快速构建 JSON 对象
        // 这里构建了一个包含 "message" 字段的 JSON 对象,内容为 "Welcome to Web3.0 World!",突出 Web3.0 主题
        c.JSON(http.StatusOK, gin.H{
            "message": "Welcome to Web3.0 World!",
        })
        // c.JSON() 会自动设置响应头 Content-Type 为 application/json; charset=utf-8
    })

    // 注册第二个 GET 路由 "/moreJSON",用于返回更详细的 Web3.0 项目信息
    r.GET("/moreJSON", func(c *gin.Context) {
        // 定义一个匿名结构体(即没有名字的结构体类型),用于封装与 Web3.0 相关的代币信息
        // 结构体的字段必须是导出的(首字母大写),否则 encoding/json 包在序列化时会忽略它们
        var tokenInfo struct {
            // Name 字段:表示 Web3.0 项目的名称,例如 "Ethereum"
            // 后面跟了一个 JSON 标签 `json:user`
            // 注意:这里的 JSON 标签写法有误,正确的格式应为 `json:"user"`(键名和值都需要用双引号括起来)
            // 当前写法缺少双引号,Go 的 encoding/json 包会忽略无效标签,导致序列化后的键名仍然是字段名 "Name"
            // 为了保持原始代码逻辑不变,此处保留原样,但实际开发中应更正为 `json:"user"` 以将字段重命名为 "user"
            Name    string `json:"user"`
            Message string              // 项目描述或状态信息,例如 "智能合约平台,支持去中心化应用"
            Age     int                  // 表示项目的已发行年数(也可表示区块数、代币总供应量等数值),这里设为 8 代表以太坊从 2015 年到 2023 年的年限
        }

        // 为结构体的字段赋值,模拟一个 Web3.0 项目(以太坊)的代币信息
        tokenInfo.Name = "Ethereum"            // 项目名称:以太坊
        tokenInfo.Message = "智能合约平台,支持去中心化应用" // 项目描述:体现 Web3.0 的智能合约和去中心化特性
        tokenInfo.Age = 8                       // 以太坊于 2015 年上线,到 2023 年约为 8 年

        // 将结构体 tokenInfo 序列化为 JSON 并返回给客户端
        // 由于 Name 字段的标签错误,实际输出的 JSON 键名将是 "Name" 而不是 "user"
        // 响应内容示例:{"Name":"Ethereum","Message":"智能合约平台,支持去中心化应用","Age":8}
        c.JSON(http.StatusOK, tokenInfo)
    })

    // r.Run() 方法用于启动 HTTP 服务并监听传入的连接
    // 它接受一个可选的地址字符串参数,格式为 "host:port"
    // 这里传入 ":8080" 表示监听所有网络接口(0.0.0.0)的 8080 端口
    // 如果服务启动成功,该方法会阻塞当前 goroutine,一直运行直到程序被终止或发生无法恢复的错误(如端口被占用)
    r.Run(":8080")
}

3、 c.JSONP() :数据获取技巧

c.JSONP() 是 Gin 框架中用于返回 JSONP 格式响应的函数,它接收 HTTP 状态码和数据对象作为参数,自动从请求的查询字符串中获取 回调函数 (默认参数名为 callback),并将数据序列化为 JSON 后包裹在该回调函数中返回(例如 callback({"key":"value"})),同时设置响应头 Content-Typeapplication/javascript,主要用于解决传统跨域请求问题(如 <script> 标签加载数据);如果请求未提供回调参数,则返回普通 JSON 数据。

JSON JSONP 的区别:

JSON 是一种独立于编程语言的数据格式 ,用于描述结构化的信息(如 {"name":"张三"}),但它本身受浏览器同源策略限制,无法直接跨域请求;而 JSONP 则是一种利用 <script> 标签天然支持跨域特性的数据获取技巧,它通过动态创建脚本标签向服务器发送请求,服务器返回的并非纯 JSON,而是一段调用预先定义好的回调函数的 JavaScript 代码,并将 JSON 数据作为参数传入,从而绕过同源策略实现跨域通信,不过由于它只支持 GET 请求且存在安全风险,在现代开发中已逐渐被更安全灵活的 CORS 方案所取代。

Go 复制代码
package main

// 导入所需的包
import (
    "net/http" // 提供 HTTP 状态码常量,如 http.StatusOK (200)
    "github.com/gin-gonic/gin" // Gin Web 框架,用于快速构建 HTTP 服务
)

// main 函数是程序的入口点
func main() {
    // 创建一个默认配置的 Gin 引擎实例
    // gin.Default() 会附加两个默认中间件:
    // - Logger:记录每个 HTTP 请求的日志(方法、路径、状态码、耗时等)
    // - Recovery:捕获处理函数中发生的任何 panic,返回 500 错误,防止程序崩溃
    r := gin.Default()

    // 注册一个处理 GET 请求的路由,路径为 "/JSONP"
    // r.GET() 方法用于注册路由,第一个参数是路径,第二个参数是处理函数(gin.HandlerFunc 类型)
    // 当客户端以 GET 方式请求 "/JSONP" 时,会执行该匿名函数
    r.GET("/JSONP", func(c *gin.Context) {
        // 定义一个 map 类型的数据,用于存储要返回的 JSON 内容
        // 这里的数据与 Web3.0 相关,模拟一个去中心化交易所的代币信息
        data := map[string]interface{}{
            "token":  "ETH",                 // 代币符号
            "price":  3500.50,                // 当前价格(美元)
            "chain":  "Ethereum",              // 所属公链
            "supply": 120000000,               // 总供应量
        }

        // c.JSONP() 方法用于返回 JSONP 格式的响应
        // JSONP (JSON with Padding) 是一种解决跨域请求的技术,常用于 <script> 标签加载数据
        // 该方法会检查请求的查询参数中是否包含回调函数名(默认参数名为 "callback")
        // 如果存在,例如请求 "/JSONP?callback=myFunc",则响应会被包装成:myFunc({"token":"ETH", ...})
        // 同时响应头的 Content-Type 会被设置为 "application/javascript"
        // 如果请求中没有提供 callback 参数,则返回普通的 JSON 数据,但 Content-Type 仍为 "application/javascript"
        // 参数说明:
        //   - http.StatusOK:HTTP 状态码 200,表示请求成功
        //   - data:要序列化为 JSON 的数据对象(可以是任意 Go 类型,如 map、结构体等)
        c.JSONP(http.StatusOK, data)

        // 假设客户端请求 "/JSONP?callback=handleData"
        // 则响应内容为:handleData({"chain":"Ethereum","price":3500.5,"supply":120000000,"token":"ETH"})
    })

    // 启动 HTTP 服务,监听在 8080 端口
    // r.Run() 会阻塞当前 goroutine,直到服务因错误而停止(如端口被占用)
    // 参数 ":8080" 表示监听所有网络接口(0.0.0.0)的 8080 端口
    r.Run(":8080")
}

postman的响应结果

4、c.XML() 返回 XML 数据

c.XML() 是 Gin 框架中用于返回 XML 格式 响应的函数,它接收 HTTP 状态码和要序列化的数据对象(如结构体、切片或 map)作为参数,内部使用 Go 标准库 encoding/xml 将数据编码为 XML 格式,并自动设置响应头 Content-Type: application/xml; charset=utf-8;开发者可以通过结构体字段的 XML 标签(如 xml:"elementName")自定义生成的 XML 元素名称和结构,适用于需要与遗留系统集成或提供 XML 接口的 Web 服务场景。

JSON 和XML 数据格式 的区别:

JSON和XML都是用于存储和传输数据的文本格式,但JSON源自JavaScript,语法更简洁轻量,直接支持对象、数组等数据类型,读写和解析速度更快,尤其适合Web API和前后端数据交互;而XML则是一种可扩展标记语言,通过自定义标签、属性和命名空间来描述结构化数据,虽然冗长且解析相对复杂,但具备Schema验证、命名空间等强大功能,更适合需要严格文档定义、元数据描述或复杂文档结构的场景,如配置文件、SOAP协议和出版行业。在现代Web开发中,JSON因其简洁高效逐渐成为主流,而XML仍在特定领域发挥不可替代的作用。

Go 复制代码
package main

// 导入所需的包
import (
    "net/http" // 提供 HTTP 状态码常量,如 http.StatusOK
    "github.com/gin-gonic/gin" // Gin Web 框架,用于快速构建 HTTP 服务
)

// main 函数是程序的入口点,当程序启动时自动执行
func main() {
    // 创建一个默认配置的 Gin 引擎实例
    // gin.Default() 会附加两个默认中间件:
    //   1. Logger 中间件:记录每个 HTTP 请求的日志(方法、路径、状态码、耗时等)
    //   2. Recovery 中间件:捕获处理函数中发生的任何 panic,返回 500 错误,防止程序崩溃
    r := gin.Default()

    // 注册 GET 路由 "/someXML"
    // r.GET() 方法用于注册处理 HTTP GET 请求的路由,第一个参数是路径,第二个参数是处理函数
    r.GET("/someXML", func(c *gin.Context) {
        // 方式一:使用 gin.H 快速构建要返回的数据
        // gin.H 是 map[string]interface{} 的预定义别名,常用于快速构造 JSON/XML 等响应数据
        // 这里构建了一个与 Web3.0 相关的简单欢迎消息
        // c.XML() 方法用于返回 XML 格式的响应,它接收两个参数:
        //   - 第一个参数:HTTP 状态码,这里使用 http.StatusOK (200)
        //   - 第二个参数:要序列化为 XML 的数据对象(可以是任意 Go 类型)
        // c.XML() 会自动设置响应头 Content-Type 为 application/xml; charset=utf-8
        c.XML(http.StatusOK, gin.H{
            "message": "Welcome to Web3.0 API", // 欢迎信息,体现 Web3.0 主题
        })
        // 实际输出的 XML 类似于:
        // <map><message>Welcome to Web3.0 API</message></map>
    })

    // 注册另一个 GET 路由 "/moreXML",返回更详细的 Web3.0 项目信息
    r.GET("/moreXML", func(c *gin.Context) {
        // 方式二:使用结构体来定义数据的结构和类型
        // 定义一个结构体类型 ProjectInfo,用于描述一个 Web3.0 项目的信息
        type ProjectInfo struct {
            Name        string // 项目名称(字段首字母大写,确保可导出)
            Description string // 项目描述
            LaunchYear  int    // 上线年份
        }

        // 创建 ProjectInfo 结构体实例并赋值
        var project ProjectInfo
        project.Name = "Ethereum"                    // 项目名称:以太坊
        project.Description = "智能合约平台,支持去中心化应用"   // 项目描述,突出 Web3.0 核心特性
        project.LaunchYear = 2015                    // 以太坊于 2015 年上线

        // 将结构体 project 序列化为 XML 并返回给客户端
        // 结构体字段将被映射为 XML 元素,元素名与字段名相同(首字母大写)
        // 输出示例:
        // <ProjectInfo><Name>Ethereum</Name><Description>智能合约平台,支持去中心化应用</Description><LaunchYear>2015</LaunchYear></ProjectInfo>
        c.XML(http.StatusOK, project)
    })

    // r.Run() 方法启动 HTTP 服务并监听传入的连接
    // 参数 ":8080" 表示监听所有网络接口(0.0.0.0)的 8080 端口
    // 该方法会阻塞当前 goroutine,直到服务因错误而停止(例如端口被占用或程序被中断)
    r.Run(":8080")
}

postman响应结果:

5、c.HTML()返回 XML 数据

c.HTML() 是 Gin 框架中用于返回 HTML 页面响应的函数,它接收 HTTP 状态码模板名称数据对象 作为参数,将数据渲染到指定的 HTML 模板中,并自动设置响应头 Content-Type: text/html; charset=utf-8;使用前需通过 r.LoadHTMLGlob()r.LoadHTMLFiles() 加载模板文件,适用于构建传统的 Web 应用或需要服务端渲染页面的场景。

注意: r.LoadHTMLGlob()r.LoadHTMLFiles() 都是 Gin 框架中用于加载 HTML 模板的方法,主要区别在于加载方式:r.LoadHTMLGlob() 接受一个 glob 模式字符串 (如 "templates/*" 或 "templates/**/*"),会自动匹配并加载该模式下的所有模板文件,适合批量加载大量文件;而 r.LoadHTMLFiles() 则接受 一个或多个具体的文件路径,需要手动列出每个要加载的模板文件,适用于只有少数几个文件或需要精确控制加载顺序的场景。两者都会将模板解析后存储在引擎中,供后续 c.HTML() 渲染使用。

以下是一个使用 c.HTML() 渲染 HTML 页面的完整案例,包含 Go 后端代码和简单的模板文件。

(1)项目结构

Go 复制代码
项目目录/
├── main.go
└── templates/
    └── index.tmpl

(2)main.go

Go 复制代码
package main

import (
    "net/http"
    "github.com/gin-gonic/gin"
)

func main() {
    // 创建默认的 Gin 引擎
    r := gin.Default()

    // 加载 templates 目录下的所有模板文件(支持 .tmpl 或 .html 后缀)
    // 也可以使用 r.LoadHTMLFiles("templates/index.tmpl") 加载单个文件
    r.LoadHTMLGlob("templates/*")

    // 定义路由,返回一个 HTML 页面
    r.GET("/", func(c *gin.Context) {
        // c.HTML() 方法用于渲染 HTML 模板
        // 参数:
        //   - http.StatusOK: HTTP 状态码 200
        //   - "index.tmpl": 要渲染的模板文件名(对应 LoadHTMLGlob 加载的文件)
        //   - gin.H{...}: 传递给模板的数据,可以在模板中使用 {{.title}} 等方式访问
        c.HTML(http.StatusOK, "index.tmpl", gin.H{
            "title":   "Web3.0 仪表盘",
            "message": "欢迎来到去中心化世界",
            "data": map[string]interface{}{
                "ethPrice": 3500.50,
                "btcPrice": 52000.00,
            },
        })
    })

    // 启动服务
    r.Run(":8080")
}

(3)templates/index.tmpl

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>{{.title}}</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 40px; }
        .card { border: 1px solid #ccc; padding: 20px; border-radius: 5px; max-width: 400px; }
        .price { font-size: 24px; color: #2ecc71; }
    </style>
</head>
<body>
    <h1>{{.title}}</h1>
    <p>{{.message}}</p>
    <div class="card">
        <h3>实时价格(Web3.0 数据)</h3>
        <p>ETH: <span class="price">${{.data.ethPrice}}</span></p>
        <p>BTC: <span class="price">${{.data.btcPrice}}</span></p>
    </div>
</body>
</html>

(4)运行说明

1、将上述两个文件放在同一目录(注意创建 templates 子目录)。

2、执行 go mod init examplego get ``github.com/gin-gonic/gin 安装依赖。

3、运行 go run main.go,访问 http://localhost:8080 即可看到渲染的 HTML 页面。

代码要点

  • 模板加载r.LoadHTMLGlob("templates/*") 加载所有模板文件,必须在路由之前执行。

  • 数据传递gin.H 是一个 map[string]interface{},可以传递任意结构的数据给模板。

  • 模板语法 :使用 {``{.字段名}} 访问传递的数据,支持点号访问嵌套字段(如 {``{.data.ethPrice}})。

  • 响应类型c.HTML 自动设置 Content-Type: text/html; charset=utf-8

网页访问结果:

1.4.5 Gin HTML 模板渲染

1、配置方法

全部模板放在一个目录里面的配置方法,我们首先在项目根目录新建 templates 文件夹,然后在文件夹中新建 index.html

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h1>这是一个 html 模板</h1>
    <h3>{{.title}}</h3>
</body>
</html>

代码解读:

(1)文档类型声明

html 复制代码
<!DOCTYPE html>
  • 作用:声明文档类型为 HTML5,确保浏览器以标准模式渲染页面。

  • Go 上下文:该声明与 Go 无关,是标准的 HTML 部分,模板引擎会原样输出。

(2) HTML 根元素

html 复制代码
<html lang="en">
  • 作用 :定义页面的根元素,lang="en" 表示页面内容语言为英语,有利于 SEO 和辅助技术。

  • Go 上下文:同样是静态 HTML,模板引擎直接输出。

(3) <head> 部分

html 复制代码
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
  • <meta charset="UTF-8"> **:**设置字符编码为 UTF-8,支持多语言(包括中文)显示,避免乱码。

  • <meta http-equiv="X-UA-Compatible" content="IE=edge"> **:**针对旧版 IE 浏览器,强制使用最新渲染模式。

  • <meta name="viewport" content="width=device-width, initial-scale=1.0"> **:**视口设置,使页面在移动端按设备宽度正确缩放,实现响应式设计。

  • <title>Document</title> 定义浏览器标签页标题,这里是一个静态占位符,实际开发中可用模板变量动态替换,例如 <title>{``{.pageTitle}}</title>。所有 <meta><title> 标签都是静态内容,模板引擎会原封不动地输出。

(4) <body> 部分

html 复制代码
<body>
    <h1>这是一个 html 模板</h1>
    <h3>{{.title}}</h3>
</body>
  • <h1>这是一个 html 模板</h1> **:**静态的一级标题,提示用户这是一个模板文件。

  • <h3>{``{.title}}</h3> :这是 Go 模板语法的核心{``{.title}} 是一个动作 (action),表示在渲染模板时,将传入的数据对象中的 title 字段的值输出到这个位置。

  • 点号 . 代表当前数据上下文(即传递给模板的数据对象)。

  • title 是数据对象的一个字段(或键),其值会被 HTML 转义后安全地插入页面,防止 XSS 攻击(这是 html/template 包的默认行为)。

  • 例如,如果后端传入数据 map[string]interface{}{"title": "Hello Go"},则渲染后此处会变成 <h3>Hello Go</h3>

2、加载模板

Gin 框架中使用 c.HTML 可以渲染模板,渲染模板前需要使用 LoadHTMLGlob()或者 LoadHTMLFiles()方法加载模板。

Go 复制代码
package main

import (
    "net/http"

    "github.com/gin-gonic/gin"
)

func main() {
    router := gin.Default()

    router.LoadHTMLGlob("templates/*")

    router.GET("/", func(c *gin.Context) {
        c.HTML(http.StatusOK, "index.html", gin.H{
            "title": "Main website",
        })
    })

    router.Run(":8080")
}

运行代码可得到如下的结果:

注意: 在 Go 的模板系统中,.tmpl. html 都是模板文件的扩展名,本质区别在于约定和用途 :.tmpl 是 "template" 的缩写,通常用于表示任何类型 的文本模板(可以是 HTML、XML、纯文本等),强调它是一个可被解析和渲染的模板文件;而 .html 则明确表示该模板的内容是 HTML 格式,便于编辑器提供 HTML 语法高亮,并且在使用 r.LoadHTMLGlob("templates/*") 时两者均可被加载,但 r.LoadHTMLFiles() 需要精确指定文件名。实际上,Go 的 html/template 包并不依赖扩展名来决定解析方式,因此选择哪个扩展名主要取决于项目规范和个人习惯,.tmpl 更通用,.html 更直观地表明输出类型。

2、模板放在不同目录里面的配置方法

Gin 框架中如果不同目录下面有同名模板的话我们需要使用下面方法加载模板

注意: 定义模板的时候需要通过 define 定义名称

templates/admin/index.html

<!-- 相当于给模板定义一个名字 {``{define}}...{``{ end}} 成对出现-->

html 复制代码
{{ define "admin/index.html" }}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h1>后台模板</h1>
    <h3>{{.title}}</h3>
</body>
</html>
{{ end }}

templates/default/index.html

<!-- 相当于给模板定义一个名字 define end 成对出现-->

html 复制代码
{{ define "default/index.html" }}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h1>前台模板</h1>
    <h3>{{.title}}</h3>
</body>
</html>
{{end}}

业务逻辑

Go 复制代码
package main

// 导入所需的包
import (
    "net/http"          // HTTP 状态码常量,如 http.StatusOK
    "github.com/gin-gonic/gin" // Gin 框架核心包,用于构建 Web 服务
)

func main() {
    // 创建一个默认配置的 Gin 引擎
    // 默认已附加 Logger 和 Recovery 中间件
    router := gin.Default()

    // 加载所有匹配 "templates/**/*" 模式的模板文件
    // "**" 表示任意层级的子目录,"*" 表示任意文件名
    // 例如:templates/default/index.html 和 templates/admin/index.html 都会被加载
    router.LoadHTMLGlob("templates/**/*")

    // 定义 GET 请求的路由 "/"
    router.GET("/", func(c *gin.Context) {
        // 渲染名为 "default/index.html" 的模板
        // 第二个参数是状态码 http.StatusOK (200)
        // 第三个参数是传递给模板的数据,gin.H 是 map[string]interface{} 的快捷方式
        c.HTML(http.StatusOK, "default/index.html", gin.H{
            "title": "前台首页", // 模板中可以通过 {{.title}} 访问
        })
    })

    // 定义 GET 请求的路由 "/admin"
    router.GET("/admin", func(c *gin.Context) {
        // 渲染后台首页模板 "admin/index.html"
        c.HTML(http.StatusOK, "admin/index.html", gin.H{
            "title": "后台首页",
        })
    })

    // 启动 HTTP 服务,监听在本机的 8080 端口
    // 默认监听 "0.0.0.0:8080",可在浏览器中访问 http://localhost:8080
    router.Run(":8080")
}

注意: 如果模板在多级目录里面的话需要这样配置r.LoadHTMLGlob("templates///*") /**表示目录。

3、gin 模板基本语法

(1){{.}} 输出数据 --> 提取后端数据的方式???

模板语法都包含在{{和}}中间,其中{{.}}中的点表示当前对象。当我们传入一个结构体对象时,我们可以根据.来访问结构体的对应字段。例如:

业务逻辑:

Go 复制代码
package main

import (
    "net/http"

    "github.com/gin-gonic/gin"
)

type UserInfo struct {
    Name   string
    Gender string
    Age    int
}

func main() {
    router := gin.Default()

    router.LoadHTMLGlob("templates/**/*")
    
    user := UserInfo{
        Name:   "张三",
        Gender: "男",
        Age:    18,
    }

    router.GET("/", func(c *gin.Context) {
        c.HTML(http.StatusOK, "default/index.html", map[string]interface{}{
            "title": "前台首页",
            "user":  user,
        })
    })

    router.Run(":8080")
}

模板 :<!-- 相当于给模板定义一个名字 define end 成对出现-->

html 复制代码
{{ define "default/index.html" }}

{{end}}
html 复制代码
{{ define "default/index.html" }}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h1>前台模板</h1>
    <h3>{{.title}}</h3>

    <h4>{{.user.Name}}</h4>
    <h4>{{.user.Age}}</h4>
</body>
</html>
{{end}}

(2)注释

html 复制代码
{{/* a comment */}}

注释,执行时会忽略。可以多行。注释不能嵌套,并且必须紧贴分界符始止。

(3)变量

我们还可以在模板中声明变量,用来保存传入模板的数据或其他语句生成的结果。具体语法如下:

html 复制代码
<h4>{{$obj := .title}}</h4>
<h4>{{$obj}}</h4>

(4)移除空格

有时候我们在使用模板语法的时候会不可避免的引入一下空格或者换行符,这样模板最终渲染出来的内容可能就和我们想的不一样,这个时候可以使用{{-语法去除模板内容左侧的所有空白符号,使用-}}去除模板内容右侧的所有空白符号。

例如:

html 复制代码
{{- .Name -}}

注意:-要紧挨{{和}},同时与模板值之间需要使用空格分隔。

以下通过一个基于 Gin 框架的具体案例,说明模板中空白字符(空格、换行)对渲染结果的影响,以及如何使用 {``{--}} 修剪空白,达到预期输出。

演示案例如下:

模板文件内容:

templates/new/without_trim.html

html 复制代码
<!DOCTYPE html>
<html>
<head>
    <title>未修剪示例</title>
</head>
<body>
    <h1>欢迎,
    {{ .Name }}
    来到 Go 世界!</h1>
</body>
</html>

templates/new/with_trim.html

html 复制代码
<!DOCTYPE html>
<html>
<head>
    <title>修剪示例</title>
</head>
<body>
    <h1>欢迎,
    {{- .Name -}}
    来到 Go 世界!</h1>
</body>
</html>

主程序 main.go

Go 复制代码
package main

import (
    "net/http"
    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()

    // 加载 templates 目录下的所有模板文件
    r.LoadHTMLGlob("templates/*")

    // 路由:使用未修剪的模板
    r.GET("/without", func(c *gin.Context) {
        c.HTML(http.StatusOK, "without_trim.html", gin.H{
            "Name": "张三",
        })
    })

    // 路由:使用修剪后的模板
    r.GET("/with", func(c *gin.Context) {
        c.HTML(http.StatusOK, "with_trim.html", gin.H{
            "Name": "张三",
        })
    })

    r.Run(":8080")
}

运行与测试:

  1. 在项目根目录执行 go run main.go

  2. 打开浏览器访问 http://localhost:8080/without,右键查看页面源代码,看到如下输出:

html 复制代码
<!DOCTYPE html>
<html>
<head>
    <title>未修剪示例</title>
</head>
<body>
    <h1>欢迎,
    张三
    来到 Go 世界!</h1>
</body>
</html>
  • 由于模板中 欢迎, 后面有换行和空格,{``{ .Name }} 前后也有换行,导致最终生成的 HTML 中出现了多余的换行和空格。虽然在浏览器中显示时可能被折叠,但在源代码中清晰可见,且可能影响布局(比如产生意外的空白字符)。
  1. 再访问 http://localhost:8080/with,查看页面源代码:
html 复制代码
<!DOCTYPE html>
<html>
<head>
    <title>修剪示例</title>
</head>
<body>
    <h1>欢迎,张三来到 Go 世界!</h1>
</body>
</html>
  • {``{- .Name -}} 中的 {``{- 移除了变量左侧的换行和空格(即 欢迎, 后面的空白),-}} 移除了变量右侧的换行和空格(即 来到 Go 世界! 前面的空白)。结果 欢迎,张三来到 Go 世界! 三个部分紧密连接,中间无多余空白。

进一步说明:

空白包含 :空格、制表符、换行符( \t\n\r)都会被修剪。

修剪位置

{``{- 只影响它紧邻左侧的空白,即前一个文本节点与当前动作之间的空白。

-}} 只影响它紧邻右侧的空白,即当前动作与后一个文本节点之间的空白。

适用场景:在生成 HTML、JSON、配置文件等对格式敏感的输出时,控制空白非常有用。

总结: 通过 Gin 框架演示的案例可以看出,模板中不经意的换行和空格可能破坏最终输出格式。使用 {``{--}} 可以精确控制空白,让渲染结果符合预期。这是 Go 模板引擎提供的一项实用功能,在开发中应灵活运用。

(5)比较函数

布尔函数会将任何类型的零值视为假,其余视为真。下面是定义为函数的二元比较运算的集合:

运算符 名称 功能描述
eq 等于 如果 arg1 == arg2 则返回真
ne 不等于 如果 arg1 != arg2 则返回真
lt 小于 如果 arg1 < arg2 则返回真
le 小于或等于 如果 arg1 <= arg2 则返回真
gt 大于 如果 arg1 > arg2 则返回真
ge 大于或等于 如果 arg1 >= arg2 则返回真

具体案例如下:

main.go

Go 复制代码
package main

import (
    "net/http"
    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()
    r.LoadHTMLGlob("templates/*")

    r.GET("/", func(c *gin.Context) {
        // 模拟不同用户数据,你可以修改这些值查看页面变化
        data := gin.H{
            "age":   20,          // 年龄
            "score": 85,          // 分数
            "role":  "editor",    // 角色
            "items": []string{"book", "pen"}, // 物品列表
            "user":  "Alice",      // 用户名(非零)
        }
        c.HTML(http.StatusOK, "index.html", data)
    })

    r.Run(":8080")
}

gin文件如下:

html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>运算符直接使用案例</title>
    <style>
        body { font-family: Arial; padding: 20px; }
        .block { margin: 20px 0; padding: 10px; border-left: 4px solid #2196F3; background: #f0f8ff; }
        .true-block { border-color: #4CAF50; background: #e8f5e8; }
        .false-block { border-color: #f44336; background: #ffebee; }
    </style>
</head>
<body>
    <h1>Gin 模板比较运算符直接使用示例</h1>
    <p>当前数据:年龄 = {{.age}}, 分数 = {{.score}}, 角色 = {{.role}}, 物品 = {{.items}}, 用户名 = {{.user}}</p>

    <!-- 1. 使用 eq 判断角色是否匹配,显示不同内容 -->
    <div class="block {{if eq .role "admin"}}true-block{{else}}false-block{{end}}">
        <h3>1. eq 运算符:{{if eq .role "admin"}}您是管理员,拥有所有权限。{{else if eq .role "editor"}}您是编辑,可以修改内容。{{else}}您是访客,只读权限。{{end}}</h3>
        <p>当前角色:{{.role}} → 显示对应提示。</p>
    </div>

    <!-- 2. 使用 ne 判断用户是否不是空(非零值) -->
    <div class="block {{if ne .user ""}}true-block{{else}}false-block{{end}}">
        <h3>2. ne 运算符:{{if ne .user ""}}欢迎您,{{.user}}!{{else}}用户未登录,请登录。{{end}}</h3>
        <p>用户名:{{.user}} → 若不为空显示欢迎语,否则提示登录。</p>
    </div>

    <!-- 3. 使用 lt 判断年龄是否小于18 -->
    <div class="block {{if lt .age 18}}true-block{{else}}false-block{{end}}">
        <h3>3. lt 运算符:{{if lt .age 18}}您未满18岁,内容受限。{{else}}您已成年,可浏览全部内容。{{end}}</h3>
        <p>年龄:{{.age}} → 根据是否小于18显示不同信息。</p>
    </div>

    <!-- 4. 使用 le 判断年龄是否小于等于18(包含18) -->
    <div class="block {{if le .age 18}}true-block{{else}}false-block{{end}}">
        <h3>4. le 运算符:{{if le .age 18}}年龄 ≤ 18,青少年模式。{{else}}年龄 > 18,成人模式。{{end}}</h3>
        <p>年龄:{{.age}} → 根据是否 ≤18 显示不同模式。</p>
    </div>

    <!-- 5. 使用 gt 判断分数是否大于90 -->
    <div class="block {{if gt .score 90}}true-block{{else}}false-block{{end}}">
        <h3>5. gt 运算符:{{if gt .score 90}}优秀!分数 > 90。{{else if gt .score 75}}良好,分数在76-90之间。{{else if gt .score 60}}及格,分数在61-75之间。{{else}}不及格,分数 ≤ 60。{{end}}</h3>
        <p>分数:{{.score}} → 多级判断展示不同评语。</p>
    </div>

    <!-- 6. 使用 ge 判断分数是否大于等于60(及格线) -->
    <div class="block {{if ge .score 60}}true-block{{else}}false-block{{end}}">
        <h3>6. ge 运算符:{{if ge .score 60}}恭喜,考试及格!{{else}}很遗憾,考试不及格。{{end}}</h3>
        <p>分数:{{.score}} → 根据是否 ≥60 显示是否及格。</p>
    </div>

    <!-- 7. 组合使用:eq 多参数判断角色 -->
    <div class="block {{if eq .role "admin" "editor"}}true-block{{else}}false-block{{end}}">
        <h3>7. eq 多参数:{{if eq .role "admin" "editor"}}您是管理员或编辑,可访问后台。{{else}}您是普通用户,仅可访问前台。{{end}}</h3>
        <p>角色:{{.role}} → 判断是否为 admin 或 editor。</p>
    </div>

    <!-- 8. 在 range 中配合 eq 判断物品是否存在(模拟 contains) -->
    <div class="block">
        <h3>8. range + eq 判断物品列表是否包含 "pen":</h3>
        {{$hasPen := false}}
        {{range .items}}
            {{if eq . "pen"}}
                {{$hasPen = true}}
            {{end}}
        {{end}}
        {{if $hasPen}}
            <p class="true-block" style="padding:5px;">✅ 您的物品中包含 pen。</p>
        {{else}}
            <p class="false-block" style="padding:5px;">❌ 您的物品中不包含 pen。</p>
        {{end}}
        <p>物品列表:{{.items}} → 动态判断是否包含 "pen"。</p>
    </div>
</body>
</html>

该 HTML 模板是一个用于 Gin 框架的 Go 模板文件,主要目的是演示如何在模板中直接使用比较运算符(eqneltlegtge)来控制页面内容的动态渲染。下面分阶段解读代码,并解释每个关键字的作用。

上述代码模板由以下几部分组成:

  • 文档声明与头部:定义文档类型、语言、元数据、标题和内联样式。

  • 主体内容

    • 一个段落显示当前传入的数据(年龄、分数、角色、物品列表、用户名)。

    • 8 个独立的区块(<div class="block">),每个区块展示一个或一组比较运算符的用法。

    • 每个区块内通过 {``{if ...}} 条件判断,根据传入数据动态显示不同的文本,并为外层 <div> 动态添加不同的 CSS 类(true-blockfalse-block),以视觉反馈条件真假。

分阶段详细解读

文档头部

html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>运算符直接使用案例</title>
    <style>
        body { font-family: Arial; padding: 20px; }
        .block { margin: 20px 0; padding: 10px; border-left: 4px solid #2196F3; background: #f0f8ff; }
        .true-block { border-color: #4CAF50; background: #e8f5e8; }
        .false-block { border-color: #f44336; background: #ffebee; }
    </style>
</head>
  • <!DOCTYPE html>:声明文档类型为 HTML5。

  • <html lang="zh">:根元素,设置页面语言为中文。

  • <meta charset="UTF-8">:指定字符编码为 UTF-8,支持中文显示。

  • <title>:定义浏览器标签页标题。

  • <style>:内联 CSS 样式,定义了三种类:

    • .block:所有区块的默认样式(蓝色左边框,浅蓝背景)。

    • .true-block:当条件为真时追加的类(绿色左边框,浅绿背景)。

    • .false-block:当条件为假时追加的类(红色左边框,浅红背景)。

主体开始与数据展示

html 复制代码
<body>
    <h1>Gin 模板比较运算符直接使用示例</h1>
    <p>当前数据:年龄 = {{.age}}, 分数 = {{.score}}, 角色 = {{.role}}, 物品 = {{.items}}, 用户名 = {{.user}}</p>
  • <h1>:页面主标题。

  • <p> :显示当前传入模板的数据。{``{.age}}{``{.score}} 等是 Go 模板语法,表示输出传入数据对象(点号 . 代表当前上下文)的对应字段值。这里用于让用户看到当前数据,方便理解条件判断的结果。

第 1 个区块: eq 运算符(单参数与多分支)

html 复制代码
<div class="block {{if eq .role "admin"}}true-block{{else}}false-block{{end}}">
    <h3>1. eq 运算符:{{if eq .role "admin"}}您是管理员,拥有所有权限。{{else if eq .role "editor"}}您是编辑,可以修改内容。{{else}}您是访客,只读权限。{{end}}</h3>
    <p>当前角色:{{.role}} → 显示对应提示。</p>
</div>
  • 外层 <div> 的 class 动态生成{``{if eq .role "admin"}}true-block{``{else}}false-block{``{end}} 判断角色是否为 "admin",若是则添加 true-block 类,否则添加 false-block 类。这使区块边框颜色反映角色是否为管理员。

  • 内层 <h3> 中的条件 :使用 eq 运算符进行多重判断。

    • {``{if eq .role "admin"}} ... {``{else if eq .role "editor"}} ... {``{else}} ... {``{end}}:依次判断角色是否等于 "admin""editor",若都不匹配则进入 else
  • eq :比较函数,判断两个参数是否相等。这里用于比较 .role 与字符串常量。

第 2 个区块: ne 运算符

html 复制代码
<div class="block {{if ne .user ""}}true-block{{else}}false-block{{end}}">
    <h3>2. ne 运算符:{{if ne .user ""}}欢迎您,{{.user}}!{{else}}用户未登录,请登录。{{end}}</h3>
    <p>用户名:{{.user}} → 若不为空显示欢迎语,否则提示登录。</p>
</div>
  • ne :判断两个参数是否不相等。这里判断 .user 是否不为空字符串 ""

  • 条件为真时,显示欢迎语并输出用户名 {``{.user}};为假时提示登录。

第 3 个区块: lt 运算符

html 复制代码
<div class="block {{if lt .age 18}}true-block{{else}}false-block{{end}}">
    <h3>3. lt 运算符:{{if lt .age 18}}您未满18岁,内容受限。{{else}}您已成年,可浏览全部内容。{{end}}</h3>
    <p>年龄:{{.age}} → 根据是否小于18显示不同信息。</p>
</div>
  • lt :判断第一个参数是否小于第二个参数(arg1 < arg2)。这里判断年龄是否小于 18。

  • 根据结果显示不同的提示信息。

第 4 个区块: le 运算符

html 复制代码
<div class="block {{if le .age 18}}true-block{{else}}false-block{{end}}">
    <h3>4. le 运算符:{{if le .age 18}}年龄 ≤ 18,青少年模式。{{else}}年龄 > 18,成人模式。{{end}}</h3>
    <p>年龄:{{.age}} → 根据是否 ≤18 显示不同模式。</p>
</div>
  • le :判断第一个参数是否小于等于第二个参数(arg1 <= arg2)。这里判断年龄是否 ≤ 18。

  • 注意:当年龄等于 18 时,条件为真,进入青少年模式。

第 5 个区块: gt 运算符(多级判断)

html 复制代码
<div class="block {{if gt .score 90}}true-block{{else}}false-block{{end}}">
    <h3>5. gt 运算符:{{if gt .score 90}}优秀!分数 > 90。{{else if gt .score 75}}良好,分数在76-90之间。{{else if gt .score 60}}及格,分数在61-75之间。{{else}}不及格,分数 ≤ 60。{{end}}</h3>
    <p>分数:{{.score}} → 多级判断展示不同评语。</p>
</div>
  • gt :判断第一个参数是否大于第二个参数(arg1 > arg2)。

  • 这里使用多个 else if 构建了分数等级判断逻辑:

    • 首先判断是否 >90 → 优秀

    • 否则判断是否 >75 → 良好

    • 否则判断是否 >60 → 及格

    • 否则 → 不及格

  • 注意:外层 <div> 的 class 只根据是否 >90 来添加 true-block(仅演示单一条件),内部则完整展示了多级分支。

第 6 个区块: ge 运算符

html 复制代码
<div class="block {{if ge .score 60}}true-block{{else}}false-block{{end}}">
    <h3>6. ge 运算符:{{if ge .score 60}}恭喜,考试及格!{{else}}很遗憾,考试不及格。{{end}}</h3>
    <p>分数:{{.score}} → 根据是否 ≥60 显示是否及格。</p>
</div>
  • ge :判断第一个参数是否大于等于第二个参数(arg1 >= arg2)。这里判断分数是否 ≥60,即是否及格。

第 7 个区块: eq 多参数用法

html 复制代码
<div class="block {{if eq .role "admin" "editor"}}true-block{{else}}false-block{{end}}">
    <h3>7. eq 多参数:{{if eq .role "admin" "editor"}}您是管理员或编辑,可访问后台。{{else}}您是普通用户,仅可访问前台。{{end}}</h3>
    <p>角色:{{.role}} → 判断是否为 admin 或 editor。</p>
</div>
  • eq 的多参数特性eq 可以接受两个以上参数,其含义是:第一个参数与后面的任意一个参数相等,则返回真。这里判断 .role 是否等于 "admin""editor"

  • 外层 <div> 的 class 同样根据此条件动态变化。

第 8 个区块: range eq 结合模拟 contains

html 复制代码
<div class="block">
    <h3>8. range + eq 判断物品列表是否包含 "pen":</h3>
    {{$hasPen := false}}
    {{range .items}}
        {{if eq . "pen"}}
            {{$hasPen = true}}
        {{end}}
    {{end}}
    {{if $hasPen}}
        <p class="true-block" style="padding:5px;">✅ 您的物品中包含 pen。</p>
    {{else}}
        <p class="false-block" style="padding:5px;">❌ 您的物品中不包含 pen。</p>
    {{end}}
    <p>物品列表:{{.items}} → 动态判断是否包含 "pen"。</p>
</div>
  • {``{$hasPen := false}} :在模板中定义一个临时变量 $hasPen,初始值为 false

  • {``{range .items}} ... {``{end}} :遍历 .items 切片(或数组),每次迭代将当前元素赋给点号 .(在循环内部,点号代表当前元素)。

    • 在循环体内,使用 {``{if eq . "pen"}} 判断当前元素是否等于字符串 "pen"

    • 如果相等,执行 {``{$hasPen = true}} 修改变量的值为 true

  • {``{if $hasPen}}:循环结束后,根据变量值决定显示包含或不包含的提示。

  • 此块演示了如何在模板中模拟 contains 功能,因为 Go 模板原生没有提供 incontains 函数。

关键字/函数详解

关键字/函数 作用
{``{ ... }} Go 模板的动作定界符,包裹模板语法。
. 点号,代表当前上下文。在顶层,它是传入的数据对象;在 range 循环内,它代表当前迭代的元素。
if 条件判断的起始,后跟条件表达式。必须与 end 配对。
else if 配合,表示条件为假时执行的分支。
else if 多重条件判断,相当于 else 后紧跟另一个 if
end 结束 ifrange 等块结构。
eq 比较函数,判断参数是否相等。可接受多个参数:eq arg1 arg2 ...,若 arg1 等于后面任意一个则返回真。
ne 比较函数,判断参数是否不相等:ne arg1 arg2
lt 比较函数,判断 arg1 是否小于 arg2:lt arg1 arg2
le 比较函数,判断 arg1 是否小于等于 arg2:le arg1 arg2
gt 比较函数,判断 arg1 是否大于 arg2:gt arg1 arg2
ge 比较函数,判断 arg1 是否大于等于 arg2:ge arg1 arg2
range 循环遍历数组、切片、映射或通道。格式:{``{range .items}} ... {``{end}}
$variable := value 在模板中定义临时变量。例如 {``{$hasPen := false}}
$variable = value 给已定义的变量重新赋值。例如 {``{$hasPen = true}}
{``{/* 注释 */}} 模板注释,渲染时会被忽略(本模板中未使用,但属于 Go 模板语法)。

**总结:**该模板通过 8 个实际场景,全面展示了 Go 模板(用于 Gin 框架)中比较运算符的用法:

  • 单条件判断eqneltlegtge 直接用于 if 条件。

  • 多分支判断 :结合 else if 实现多级逻辑。

  • 多参数 eq:简化"属于集合"的判断。

  • 循环与变量 :使用 range 遍历切片,并通过临时变量模拟 contains 操作。

每个区块还利用动态 CSS 类为条件结果提供了视觉反馈,使模板的渲染效果更加直观。整个文件清晰地展示了如何在服务端模板中根据数据动态生成 HTML 内容,是理解 Gin 模板编程的良好范例。

(6)条件判断

Go 模板语法中的条件判断有以下几种:

Go 复制代码
// 第一种:
{{if pipeline}} T1 {{end}}

// 第二种:
{{if pipeline}} T1 {{else}} T0 {{end}} 

// 第三种:
{{if pipeline}} T1 {{else if pipeline}} T0 {{end}} 

// 第四种:
{{if gt .score 60}} 
    及格 
{{else}} 
    不及格
{{end}} 

// 第五种:
{{if gt .score 90}}
    优秀
{{else if gt .score 60}}
    及格
{{else}}
    不及格
{{end}}

(7)range

Go 的模板语法中使用 range 关键字进行遍历,有以下两种写法,其中 pipeline 的值必须是数组、切片、字典或者通道。

Go 复制代码
{{range $key,$value := .obj}} 
    {{$value}} 
{{end}}

如果 pipeline 的值其长度为 0,不会有任何输出。

Go 复制代码
{{$key,$value := .obj}} 
    {{$value}} 
{{else}} 
    pipeline 的值其长度为 0 
{{end}}

如果 pipeline 的值其长度为 0,则会执行 T0。

Go 复制代码
package main

import (
    "net/http"

    "github.com/gin-gonic/gin"
)

type UserInfo struct {
    Name   string
    Gender string
    Age    int
}

func main() {
    router := gin.Default()

    router.GET("/", func(ctx *gin.Context) {
        c.HTML(http.StatusOK,"default/index.html",map[string]interface{}{
            "hobby":[]string{"吃饭","睡觉","写代码"},
        })
    })
/*
    {{range $key,$value := .hobby}}
        <p>{{$value}}<p>
    {{end}}
    router.Run(":8080")
*/
}

(7)With

在 Gin 框架(基于 Go 模板)中,with 是一个模板动作,用于将当前上下文(即点号 .)临时绑定到指定的值,并在其内部直接访问该值的字段或方法,同时可自动处理空值情况。例如,{``{with .User}}<p>用户名:{``{.Name}}</p>{``{end}} 会先判断 .User 是否为非空(非零值),若非空则将 . 指向 .User,从而在内部用 {``{.Name}} 直接输出用户名;若 .User 为空,则跳过整个 with 块。这简化了深层嵌套字段的访问,并提供了优雅的空值处理。

Go 复制代码
package main

import (
    "net/http"

    "github.com/gin-gonic/gin"
)

type UserInfo struct {
    Name   string
    Gender string
    Age    int
}

func main() {

    router := gin.Default()

    user := UserInfo{
        Name:   "张三",
        Gender: "男",
        Age:    18,
    }

    router.GET("/", func(c *gin.Context) {
        c.HTML(http.StatusOK, "default/index.html", map[string]interface{}{
            "user": user,
        })
    })
    router.Run(":8080")
}

以前要输出数据:

html 复制代码
{{ define "default/index.html" }}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h4>{{.user.Name}}</h4>
    <h4>{{.user.Gender}}</h4>
    <h4>{{.user.Age}}</h4>
</body>
</html>
{{end}}

现在要输出数据:

html 复制代码
{{ define "default/index.html" }}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    {{with .user}}
        <h4>姓名:{{.Name}}</h4>
        <h4>性别:{{.user.Gender}}</h4>
        <h4>年龄:{{.Age}}</h4>
    {{end}}
</body>
</html>
{{end}}

简单理解:相当于 var .=.user

(8)预定义函数 (了解)

执行模板时,函数从两个函数字典中查找:首先是模板函数字典,然后是全局函数字典。一般不在模板内定义函数,而是使用 Funcs 方法添加函数到模板里。

预定义的全局函数如下:

  • **and:**函数返回它的第一个 empty 参数或者最后一个参数; 就是说"and x y"等价于"if x then y else x";所有参数都会执行;

  • **or :**返回第一个非 empty 参数或者最后一个参数;亦即"or x y"等价于"if x then x else y";所有参数都会执行;

  • **not:**返回它的单个参数的布尔值的否定

  • **len :**返回它的参数的整数类型长度

  • **index:**执行结果为第一个参数以剩下的参数为索引/键指向的值; 如"index x 1 2 3"返回 x[1][2][3]的值;每个被索引的主体必须是数组、切片或者字典。

  • **print :**即 fmt.Sprint

  • **printf :**即 fmt.Sprintf

  • **println :**即 fmt.Sprintln

  • html **:**返回与其参数的文本表示形式等效的转义 HTML。 这个函数在 html/template 中不可用。

  • **urlquery :**以适合嵌入到网址查询中的形式返回其参数的文本表示的转义值。这个函数在 html/template 中不可用。

  • js **:**返回与其参数的文本表示形式等效的转义 JavaScript。

  • **call:**执行结果是调用第一个参数的返回值,该参数必须是函数类型,其余参数作为调用该函数的参数; 如"call .X.Y 1 2"等价于 go 语言里的 dot.X.Y(1, 2); 其中 Y 是函数类型的字段或者字典的值,或者其他类似情况; call 的第一个参数的执行结果必须是函数类型的值(和预定义函数如 print 明显不同); 该函数类型值必须有 1 到 2 个返回值,如果有 2 个则后一个必须是 error 接口类型; 如果有 2 个返回值的方法返回的 error 非 nil,模板执行会中断并返回给调用模板执行者该错误;

html 复制代码
{{len .title}} 
{{index .hobby 2}}

(9)自定义模板函数

html 复制代码
router.SetFuncMap(template.FuncMap{ 
    "formatDate": formatAsDate, 
})
Go 复制代码
package main

import (
    "fmt"
    "html/template"
    "net/http"
    "time"

    "github.com/gin-gonic/gin"
)

func formatAsDate(t time.Time) string {
    year, month, day := t.Date()
    return fmt.Sprintf("%d/%02d/%02d", year, month, day)
}

func main() {
    router := gin.Default()
    //注册全局模板函数 注意顺序,注册模板函数需要在加载模板上面
    router.SetFuncMap(template.FuncMap{"formatDate": formatAsDate})
    //加载模板
    router.LoadHTMLGlob("templates/**/*")
    router.GET("/", func(c *gin.Context) {
        c.HTML(http.StatusOK, "default/index.html", map[string]interface{}{
            "title": "前台首页", 
            "now": time.Now(),
        })
    })
    router.Run(":8080")
}
html 复制代码
{{.now | formatDate}} 
    或者 
{{formatDate .now }}

4、嵌套 template

(1)新建 templates/deafult/page_header.html

html 复制代码
{{ define "default/page_header.html" }} 
    <h1>这是一个头部</h1> 
{{end}}

(2)外部引入

注意:

1)引入的名字为 page_header.html 中定义的名字

2)引入的时候注意最后的点(.)

html 复制代码
{{template "default/page_header.html" .}}
html 复制代码
<!-- 相当于给模板定义一个名字 define end 成对出现--> 
{{ define "default/index.html" }} 
<!DOCTYPE html> 
<html lang="en"> 
<head> 
    <meta charset="UTF-8"> 
    <meta http-equiv="X-UA-Compatible" content="IE=edge"> 
    <meta name="viewport" content="width=device-width, initial-scale=1.0"> 
    <title>Document</title> 
</head> 
<body> 
    {{template "default/page_header.html" .}} 
</body> 
</html> 
{{end}}

1.4.6 静态文件服务

当我们渲染的 HTML 文件中引用了静态文件 时,我们需要配置静态 web 服务

r.Static("/static", "./static") 。前面的/static 表示路由后面的./static 表示路径。

静态文件是指网站中不需要服务器动态处理直接原样发送给客户端的文件 ,例如 CSS 样式表、JavaScript 脚本、图片、字体等资源。它们的内容固定不变,不依赖业务逻辑或数据库,只需配置静态文件服务(如 Gin 的 r.Static())让服务器直接返回这些文件,既能简化开发又能提升加载效率。

Go 复制代码
package main

import "github.com/gin-gonic/gin"

func main() {
    r := gin.Default()

    r.Static("/static", "./static")
    r.LoadHTMLGlob("templates/**/*")

    // ... r.Run(":8080")
}
Go 复制代码
<link rel="stylesheet" href="/static/css/base.css" />

项目案例:

项目结构

Go 复制代码
static-demo/
├── main.go
├── static/
│   ├── css/
│   │   └── style.css
│   └── images/
│       └── logo.png
└── templates/
    └── index.html

静态文件内容

Go 复制代码
body {
    font-family: Arial, sans-serif;
    background-color: #f0f0f0;
    color: #333;
}
h1 {
    color: #007bff;
}
img {
    max-width: 200px;
}

这段 CSS 代码定义了网页中三个元素(bodyh1img)的样式,作用是统一页面的基础外观,使内容更美观易读。下面逐条解释:

body 规则

  • font-family: Arial, sans-serif;

设置全局字体为 Arial,如果用户设备没有安装 Arial,则使用系统默认的无衬线字体(如 Helvetica 或微软雅黑)。这保证了文字的现代感和可读性。

  • background-color: #f0f0f0;

将页面背景色设为浅灰色(十六进制 #f0f0f0),营造柔和舒适的视觉环境,避免纯白色带来的刺眼感。

  • color: #333;

设置默认文字颜色为深灰色(#333),与浅色背景形成良好对比,同时比纯黑色更柔和,减轻阅读疲劳。

h1 规则

  • color: #007bff; 将一级标题的文字颜色设为亮蓝色(#007bff),这是 Bootstrap 等框架常用的品牌色,能使标题醒目突出,引导用户注意力。

img 规则

  • **max-width: 200px;**限制所有图片的最大宽度为 200 像素,防止大图撑破页面布局,同时保持图片比例不变(只限制宽度,高度自适应)。这适合需要统一图片尺寸的场景(如头像、缩略图)。

整体效果:页面背景柔和,文字清晰易读,标题颜色鲜明,图片尺寸可控,整体风格简洁统一。

static/images/logo.png (随便放一张图片,或用一个占位说明)

HTML 模板

templates/index.html

html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>静态文件案例</title>
    <!-- 引用静态 CSS 文件 -->
    <link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
    <h1>静态文件服务示例</h1>
    <!-- 引用静态图片 -->
    <img src="/static/images/logo.png" alt="Logo">
    <p>这是一个通过 Gin 提供的静态文件服务的页面。</p>
</body>
</html>

主程序 main.go

Go 复制代码
package main

import (
    "net/http"
    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()

    // 配置静态文件路由:URL 路径 /static 对应本地目录 ./static
    r.Static("/static", "./static")

    // 加载 HTML 模板
    r.LoadHTMLGlob("templates/*")

    // 首页路由
    r.GET("/", func(c *gin.Context) {
        c.HTML(http.StatusOK, "index.html", nil)
    })

    r.Run(":8080")
}

运行效果:

如果没有 配置 r.Static("/static", "./static"),浏览器加载 /static/css/style.css/static/images/logo.png 时会返回 404,页面将失去样式和图片。

**注意:**路径中图片的实际名称要和自己图片的名称保持一致。

为什么需要静态文件服务?

  • HTML 中引用的 CSS、JS、图片等文件是独立存放的,它们需要通过 HTTP 请求获取。

  • Gin 默认只处理动态路由(如 /),不会自动提供文件访问。

  • r.Static() 的作用就是将这些静态文件目录映射到指定的 URL 前缀,使客户端能够通过 HTTP 访问到这些文件。

1.5 路由详解

路由(Routing)是由一个 URI (或者叫路径)和一个特定的 HTTP 方法(GET、POST 等)组成的,涉及到应用如何响应客户端对某个网站节点的访问。 前面我们给大家介绍了路由基础以及路由配置,这里我们详细给大家讲讲路由传值、路由返回值。

1.5.1 GET POST 以及获取 Get Post 传值

1、Get 请求传值

GET /user?uid=20&page=1

Go 复制代码
router.GET("/user", func(c *gin.Context) { 
    uid := c.Query("uid") 
    page := c.DefaultQuery("page", "0") 
    c.String(200, "uid=%v page=%v", uid, page) 
})

2、 动态路由 传值

域名/user/20

Go 复制代码
r.GET("/user/:uid", func(c *gin.Context) { 
    uid := c.Param("uid") 
    c.String(200, "userID=%s", uid) 
})

3、Post 请求传值 获取 form 表单数据

定义一个 add_user.html 的页面

html 复制代码
{{ define "default/add_user.html" }} 
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <form action="/doAddUser" method="post">
        用户名:<input type="text" name="username" />
        密码: <input type="password" name="password" />
        <input type="submit" value="提交">
    </form>
</body>
</html>
{{end}}

通过 c.PostForm 接收表单传过来的数据

Go 复制代码
router.GET("/addUser", func(c *gin.Context) { 
    c.HTML(200, "default/add_user.html", gin.H{}) 
})

router.POST("/doAddUser", func(c *gin.Context) { 
    username := c.PostForm("username") 
    password := c.PostForm("password")
    age := c.DefaultPostForm("age", "20") 
    c.JSON(200, gin.H{ 
        "usernmae": username, 
        "password": password, 
        "age": age, 
    }) 
})

4、获取 GET POST 传递的数据绑定到结构体

为了能够更方便的获取请求相关参数,提高开发效率,我们可以基于请求的 Content-Type 识别请求数据类型并利用反射机制自动提取请求中 QueryString、form 表单、JSON、XML 等参数到结构体中。 下面的示例代码演示了.ShouldBind()强大的功能,它能够基于请求自动提取 JSON、form 表单和 QueryString 类型的数据,并把值绑定到指定的结构体对象

Go 复制代码
//注意首字母大写
type Userinfo struct { 
    Username string `form:"username" json:"user"` 
    Password string `form:"password" json:"password"` 
}

Get 传值绑定到结构体

/?username=zhangsan&password=123456

Go 复制代码
router.GET("/", func(c *gin.Context) { 
    var userinfo Userinfo 
    if err := c.ShouldBind(&userinfo); err == nil { 
        c.JSON(http.StatusOK, userinfo) 
    } else { 
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 
    } 
})
html 复制代码
返回数据 
{"user":"zhangsan","password":"123456"}

Post 传值绑定到结构体

Go 复制代码
router.POST("/doLogin", func(c *gin.Context) { 
    var userinfo Userinfo 
    if err := c.ShouldBind(&userinfo); err == nil { 
        c.JSON(http.StatusOK, userinfo) 
    } else { 
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 
    } 
})
bash 复制代码
返回数据 
{"user":"zhangsan","password":"123456"}

5、获取 Post Xml 数据

在 API 的开发中,我们经常会用到 JSON 或 XML 来作为数据交互的格式,这个时候我们 可以在 gin 中使用 c.GetRawData()获取数据。

html 复制代码
<?xml version="1.0" encoding="UTF-8"?> 
<article> 
    <content type="string">我是张三</content> 
    <title type="string">张三</title> 
</article>
复制代码
Go 复制代码
type Article struct { 
    Title 
    string `xml:"title"` 
    Content string `xml:"content"` 
}
 
router.POST("/xml", func(c *gin.Context) { 
    b, _ := c.GetRawData() // 从 c.Request.Body 读取请求数据
    article := &Article{} 
    if err := xml.Unmarshal(b, &article); err == nil { 
        c.JSON(http.StatusOK, article) 
    } else { 
        c.JSON(http.StatusBadRequest, err.Error()) 
    } 
})

1.5.2 简单的路由组

Gin 中的路由组(Router Group)是一种将具有相同 URL 前缀共享中间件 的路由进行统一管理的机制,通过 r.Group("/prefix") 创建,后续在该组上定义的路由会自动添加该前缀,并且可以为组统一应用中间件(如认证、日志等),从而避免重复代码,使路由结构更清晰、维护更方便。

https://gin-gonic.com/zh-cn/docs/examples/grouping-routes/

Go 复制代码
package main

import (
    "net/http"
    "github.com/gin-gonic/gin"
)

func main() {
    // 创建一个默认配置的 Gin 引擎(自带 Logger 和 Recovery 中间件)
    router := gin.Default()

    // 创建 API v1 版本的路由组,所有路由前缀为 /v1
    v1 := router.Group("/v1")
    // 在 v1 组上定义路由
    v1.POST("/login", loginEndpoint)  // 处理 POST /v1/login 请求
    v1.POST("/submit", submitEndpoint) // 处理 POST /v1/submit 请求
    v1.POST("/read", readEndpoint)     // 处理 POST /v1/read 请求

    // 创建 API v2 版本的路由组,所有路由前缀为 /v2
    v2 := router.Group("/v2")
    v2.POST("/login", loginEndpoint)  // 处理 POST /v2/login 请求
    v2.POST("/submit", submitEndpoint) // 处理 POST /v2/submit 请求
    v2.POST("/read", readEndpoint)     // 处理 POST /v2/read 请求

    // 启动 HTTP 服务,监听在本机 8080 端口
    router.Run(":8080")
}

// loginEndpoint 处理登录请求
func loginEndpoint(c *gin.Context) {
    // 这里编写登录逻辑,例如验证用户名密码
    c.String(http.StatusOK, "login success")
}

// submitEndpoint 处理提交请求
func submitEndpoint(c *gin.Context) {
    // 提交逻辑
    c.String(http.StatusOK, "submit success")
}

// readEndpoint 处理读取请求
func readEndpoint(c *gin.Context) {
    // 读取逻辑
    c.String(http.StatusOK, "read success")
}

1.5.3 Gin 路由 文件 分组

在 Gin 框架中,路由文件分组是指通过创建路由组(router.Group)将具有相同前缀或共享中间件的路由归类,并可将不同模块的路由定义拆分到独立的 Go 文件或包中,例如将用户相关路由放在 routes/user.go,商品相关放在 routes/product.go,然后在主程序中导入这些包并注册路由组,从而实现代码的模块化、低耦合与高可维护性,避免单个文件过于臃肿,同时便于多人协作开发。

新建 routes 文件夹,routes 文件下面新建 adminRoutes.go、apiRoutes.go、 defaultRoutes.go

1、新建 adminRoutes.go

Go 复制代码
package routes

import ( 
    "net/http"
    "github.com/gin-gonic/gin"
)

func AdminRoutesInit(router *gin.Engine) {
    adminRouter := router.Group("/admin"){
        adminRouter.GET("/user", func(c *gin.Context) {
            c.String(http.StatusOK, "用户")
        })
        
        adminRouter.GET("/news", func(c *gin.Context) {
         c.String(http.StatusOK, "news")
        })
    }
}

2、新建 apiRoutes.go

Go 复制代码
package routes

import ( 
    "net/http" 
    "github.com/gin-gonic/gin" 
) 

func ApiRoutesInit(router *gin.Engine) { 
    apiRoute := router.Group("/api"){ 
        apiRoute.GET("/user", func(c *gin.Context) { 
            c.JSON(http.StatusOK, gin.H{
                "username": "张三",
                "age": 20, 
            }) 
        })
        
        apiRoute.GET("/news", func(c *gin.Context) { 
            c.JSON(http.StatusOK, gin.H{ 
                "title": "这是新闻", 
            }) 
        }) 
    } 
}

3、新建 defaultRoutes.go

Go 复制代码
package routes

import ( 
    "github.com/gin-gonic/gin" 
)

func DefaultRoutesInit(router *gin.Engine) {

    defaultRoute := router.Group("/"){ 
        defaultRoute.GET("/", func(c *gin.Context) { 
            c.String(200, "首页") 
        }) 
    } 
}

4、配置 main.go

Go 复制代码
package main 
import ( 
    "gin_demo/routes" 
    "github.com/gin-gonic/gin"
)

//注意首字母大写 
type Userinfo struct { 
    Username string `form:"username" json:"user"` 
    Password string `form:"password" json:"password"` 
}

func main() {
    r := gin.Default() 
    routes.AdminRoutesInit(r) 
    routes.ApiRoutesInit(r) 
    routes.DefaultRoutesInit(r) 
    r.Run(":8080") 
}

访问 /api/user /admin/user 测试


第二章:Gin 中自定义控制器

2.1 Gin 中自定义控制器

在 Gin 框架中,"自定义控制器"并不是框架内置的概念,而是一种由开发者主导的、用于组织代码的常见设计模式 。它的核心思想是将处理HTTP请求的逻辑(即路由绑定的函数)从零散的匿名函数全局函数 中抽离出来,通过 Go 语言的结构体(struct)及其方法(method)进行模块化封装 ,从而提升代码的可维护性可读性复用性。

简单来说,你可以创建一个结构体,比如 UserController,然后为它定义 IndexShowCreate 等方法。每个方法都接收 *gin.Context 参数,并包含具体的业务处理逻辑。之后,在设置路由时,不再直接绑定一个普通函数,而是实例化这个控制器结构体,并将其方法绑定到对应的路由上。这样,所有与用户相关的请求处理逻辑就被整洁地归类到了一个控制器中,符合 MVC 模式的思想。

MVC Model-View-Controller ): 是一种软件设计模式,将应用程序分为数据模型视图控制器三个部分,提高了应用程序的可维护性和可扩展性。

*gin.Context 参数详解: 在 Gin 框架中,*gin.Context 是指向 gin.Context 结构体的指针 ,它代表了当前 HTTP 请求的上下文,是处理请求时最重要的对象。可以把它理解为请求的"万能工具箱",它封装了与单个 HTTP 请求相关的所有信息,并提供了操作响应的方法。

具体来说,它的作用包括:

  • 获取请求信息 :通过它读取 URL 参数(c.Query())、路径参数(c.Param())、表单数据(c.PostForm())、JSON 数据(c.ShouldBindJSON())、请求头(c.GetHeader())等。

  • 设置响应内容 :通过它向客户端返回数据,例如 c.JSON() 返回 JSON、c.String() 返回文本、c.HTML() 渲染模板、c.File() 返回文件等。

  • 控制请求流程 :在中间件中使用 c.Next() 放行请求、c.Abort() 终止后续处理,还可以通过 c.Set()c.Get() 在请求的各个处理函数之间共享数据。

  • 管理元数据:访问请求的方法、路径、状态码、客户端 IP 等。

示例

Go 复制代码
func UserInfo(c *gin.Context) {
    // 获取路径中的 id 参数
    id := c.Param("id")
    // 获取查询参数 name
    name := c.Query("name")
    // 返回 JSON 响应
    c.JSON(200, gin.H{
        "id":   id,
        "name": name,
    })
}
总之,*gin.Context 是 Gin 框架中请求与响应的交汇点,所有请求处理函数都通过它来读取输入和产生输出。

总之,*gin.Context 是 Gin 框架中请求与响应的交汇点,所有请求处理函数都通过它来读取输入和产生输出。

代码示例(使用自定义控制器组织用户相关路由):

Go 复制代码
package main

import (
        "net/http"
        "github.com/gin-gonic/gin"
)

// 1. 定义一个控制器结构体
type UserController struct {
        // 可以在这里注入服务层依赖,例如 UserService
}

// 2. 为控制器添加方法,处理具体请求
func (uc *UserController) Index(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{"message": "用户列表"})
}

func (uc *UserController) Show(c *gin.Context) {
        userId := c.Param("id")
        c.JSON(http.StatusOK, gin.H{"user_id": userId})
}

func main() {
        router := gin.Default()

        // 3. 实例化控制器
        userController := &UserController{}

        // 4. 将控制器方法与路由绑定
        userRoutes := router.Group("/users")
        {
                userRoutes.GET("/", userController.Index)      // 绑定 Index 方法
                userRoutes.GET("/:id", userController.Show)    // 绑定 Show 方法
        }

        router.Run(":8080")
}

2.2 控制器分组

在 Gin 框架中,控制器分组是指将处理同一类资源(如用户、订单)的多个请求方法(如 增删改查 )封装在一个自定义控制器结构体中 ,并利用 Gin 的路由分组功能(router.Group)为这些方法统一设置路由前缀和共享中间件,从而实现代码的模块化和路由的清晰管理。这样做既能避免重复编写公共路径和中间件,又能让不同资源的控制器各司其职,提升项目的可维护性和可扩展性。例如,UserController 的 Index 和 Show 方法可以分别绑定到 GET /users 和 GET /users/:id,这些路由都属于 /users 分组。当我们的项目比较大的时候有必要对我们的控制器进行分组。

新建 controller /admin/NewsController.go

Go 复制代码
package admin

import ( 
    "net/http" 
    "github.com/gin-gonic/gin" 
)

type NewsController struct {
    
}

func (c NewsController) Index(ctx *gin.Context) { 
    ctx.String(http.StatusOK, "新闻首页") 
}

新建 controller /admin/UserController.go

Go 复制代码
package admin

import ( 
    "net/http" 
    "github.com/gin-gonic/gin" 
)

type UserController struct { 
}

func (c UserController) Index(ctx *gin.Context) { 
    ctx.String(http.StatusOK, "这是用户首页") 
}

func (c UserController) Add(ctx *gin.Context) { 
    ctx.String(http.StatusOK, "增加用户") 
}

配置对应的路由 --adminRoutes.go

其他路由的配置方法类似

Go 复制代码
package routes

import (
    "gin_demo/controller/admin"
    "net/http" 
    "github.com/gin-gonic/gin" 
)

func AdminRoutesInit(router *gin.Engine) {
    adminRouter := router.Group("/admin") {
        adminRouter.GET("/user", admin.UserController{}.Index) 
        adminRouter.GET("/user/add", admin.UserController{}.Add) 
        adminRouter.GET("/news", admin.NewsController{}.Add) 
    }
}

2.3 控制器的 继承

在 Gin 框架中,控制器的 继承 并非传统面向对象语言的类继承,而是利用 Go 语言的结构体嵌入(composition) 特性,将一个基础控制器的字段和方法"注入"到子控制器中,从而实现代码复用。通过嵌入匿名结构体,子控制器自动获得基础控制器的所有公开方法,并可以按需覆盖或添加新方法,同时还能共享公共字段(如服务依赖、配置等)。这种方式让开发者能够构建出类似继承的层次结构,例如创建一个 BaseController 包含通用方法(如响应格式化、错误处理),然后让 UserControllerOrderController 通过嵌入来复用这些基础逻辑,既保持了代码的简洁,又遵循了 Go 的组合优于继承的设计哲学。

1、新建 controller /admin/BaseController.go

Go 复制代码
package admin

import ( 
    "net/http" 
    "github.com/gin-gonic/gin" 
)

type BaseController struct { 
}

func (c BaseController) Success(ctx *gin.Context) { 
    ctx.String(http.StatusOK, "成功")
} 

func (c BaseController) Error(ctx *gin.Context) { 
    ctx.String(http.StatusOK, "失败") 
}

2、NewsController 继承 BaseController

继承后就可以调用控制器里面的公共方法了

Go 复制代码
package admin

import ( 
    "github.com/gin-gonic/gin" 
)

type NewsController struct { 
    BaseController 
}

func (c NewsController) Index(ctx *gin.Context) { 
    c.Success(ctx) 
}

第三章:Gin 中间件

Gin 框架允许开发者在处理请求的过程中,加入用户自己的钩子(Hook)函数。这个钩子函数就叫中间件,中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等。

通俗的讲:中间件就是匹配路由前和匹配路由完成后执行的一系列操作。

钩子函数是一种特殊的程序,可以通过系统调用把它挂入系统,从而在特定事件发生时,自动执行的函数

3.1 路由中间件

1、初识中间件

Gin 中的中间件本质上是一个符合 gin.HandlerFunc 签名的函数 ,它能够在 HTTP 请求到达最终路由处理函数之前或之后执行特定的逻辑,通过 c.Next() 控制调用链的流转,常用于处理横切关注点(如日志记录、身份验证、错误恢复、请求耗时统计等),可以全局注册(router.Use())或仅作用于特定路由分组,从而以非侵入的方式增强和扩展 Web 服务的功能。

Gin 中的中间件必须是一个 gin.HandlerFunc 类型,配置路由的时候可以传递多个 func 回调函数,最后一个 func 回调函数前面触发的方法都可以称为中间件。

gin.HandlerFunc 是 Gin 框架中定义的一个函数类型 ,其签名固定为 func(*gin.Context),代表一个 HTTP 请求处理单元。它既是路由处理函数(如 GET("/path", handler) 中的 handler),也是中间件函数的底层类型;当请求到达时,Gin 会调用该函数并传入 *gin.Context 指针,开发者通过该上下文对象读取请求参数、处理业务逻辑,并通过它返回响应,从而实现灵活且可组合的请求处理流程。

回调函数,又称为别名回调、处理程序、handler或call-after function,是指将函数 作为参数传递 给其他代码,使该代码可调用高层定义的子程序。

Go 复制代码
package main

import (
        "fmt"
        "github.com/gin-gonic/gin"
)

// initMiddleware 是一个Gin中间件,用于模拟Web3场景中的钱包验证流程。
// 在真实的去中心化应用中,该中间件会从HTTP请求头中提取钱包地址和签名,
// 通过智能合约或本地加密库验证签名的合法性,确保请求来自合法的钱包持有者。
// 此处仅打印日志模拟验证开始,并调用 ctx.Next() 将控制权交给后续处理函数。
func initMiddleware(ctx *gin.Context) {
        fmt.Println("Web3中间件:开始验证钱包地址和签名...")
        // 实际开发中可在此处添加验证逻辑,例如:
        // address := ctx.GetHeader("X-Wallet-Address")
        // signature := ctx.GetHeader("X-Signature")
        // if !verifySignature(address, signature) {
        //     ctx.AbortWithStatusJSON(401, gin.H{"error": "签名验证失败"})
        //     return
        // }
        ctx.Next() // 放行请求到具体的路由处理函数
}

func main() {
        r := gin.Default()

        // 首页路由 - 演示Web3场景下的公开API
        // 中间件会先执行模拟验证,验证通过后返回欢迎信息
        r.GET("/", initMiddleware, func(ctx *gin.Context) {
                // 在实际Web3后端中,这里可能返回DApp的全局状态、连接指引或用户基础信息
                ctx.String(200, "Web3首页 - 欢迎访问去中心化应用!请连接您的钱包。")
        })

        // 新闻页面路由 - 演示需要钱包验证的API
        // 该路由同样经过initMiddleware中间件,模拟只有合法钱包才能访问的新闻内容
        r.GET("/news", initMiddleware, func(ctx *gin.Context) {
                // 假设验证通过后,返回与区块链相关的新闻,例如当前区块高度
                ctx.String(200, "Web3新闻页面 - 当前区块高度:12345678,Gas价格:20 Gwei")
        })

        // 启动Gin服务,监听8080端口
        r.Run(":8080")
}

3.2 ctx.Next()调用该请求的剩余处理程序

在 Gin 框架中,ctx.Next() 是用于在中间件函数中将请求处理流程显式传递给下一个中间件或最终路由处理函数的方法 ;当中间件调用 ctx.Next() 时,当前中间件会暂停执行,等待后续处理器完成工作后再继续执行剩余代码,从而实现了在请求处理前后分别插入逻辑(如前置鉴权、后置日志)的能力,若不调用 Next(),请求链将被阻断,后续处理器不会执行。

中间件里面加上 ctx.Next() 可以让我们在路由匹配完成后执行一些操作。比如我们统计一个请求的执行时间。

Go 复制代码
package main

import (
  "fmt"
  "time"

  "github.com/gin-gonic/gin"
)

// initMiddleware 是一个Gin中间件,用于模拟 Web3场景 下的签名验证和Gas消耗统计
func initMiddleware(ctx *gin.Context) {
  // 1. 前置处理:模拟开始验证钱包签名
  fmt.Println("1-中间件:开始验证钱包签名...")

  // 记录开始时间(模拟Gas消耗计时起点)
  start := time.Now().UnixNano()

  // 调用该请求的剩余处理程序(路由处理函数或下一个中间件)
  // 在Web3场景中,这相当于让后续业务逻辑(如智能合约调用)继续执行
  ctx.Next()

  // 3. 后置处理:模拟签名验证完成,计算整个请求处理的耗时(类比Gas消耗)
  fmt.Println("3-中间件:签名验证完成,计算请求处理耗时(模拟Gas消耗)")

  // 计算耗时(纳秒),在实际应用中可用于性能监控或Gas估算
  end := time.Now().UnixNano()
  fmt.Printf("本次请求处理耗时:%d ns\n", end-start)
}

func main() {
  r := gin.Default()

  // 首页路由 - 模拟Web3应用的首页,返回当前网络状态和Gas价格
  r.GET("/", initMiddleware, func(ctx *gin.Context) {
    // 2. 业务处理:模拟获取区块链基础信息
    fmt.Println("2-路由处理:获取当前网络状态(区块高度、Gas价格)")
    ctx.String(200, "Web3首页 - 当前网络:以太坊主网,区块高度:12345678,Gas价格:20 Gwei")
  })

  // 新闻路由 - 模拟获取与区块链相关的新闻资讯,同样需要经过中间件验证
  r.GET("/news", initMiddleware, func(ctx *gin.Context) {
    // 2. 业务处理:模拟返回区块链新闻数据
    fmt.Println("2-路由处理:返回Web3新闻数据")
    ctx.String(200, "Web3新闻 - 以太坊即将进行Dencun升级,Layer2费用将大幅降低")
  })

  // 启动Gin服务,监听8080端口
  r.Run(":8080")
}

3.3 一个路由配置多个中间件的执行顺序

在 Gin 框架中,为一个路由配置多个中间件时,执行顺序遵循"洋葱模型":中间件按照注册的先后顺序依次执行,每个中间件内部通过调用 c.Next() 将控制权交给下一个中间件或最终的处理函数。具体来说,请求首先进入第一个中间件,执行其 c.Next() 之前的代码,然后调用 c.Next() 进入第二个中间件,以此类推,直到最终处理函数;之后响应逆序返回,依次执行每个中间件 c.Next() 之后的代码。这种机制使得前置逻辑(如请求日志、鉴权)按顺序执行,后置逻辑(如响应处理)按相反顺序执行。

Go 复制代码
func initMiddlewareOne(ctx *gin.Context) { 
    fmt.Println("initMiddlewareOne--1-执行中中间件") 
    // 调用该请求的剩余处理程序 
    ctx.Next() 
    fmt.Println("initMiddlewareOne--2-执行中中间件") 
}
 
func initMiddlewareTwo(ctx *gin.Context) { 
    fmt.Println("initMiddlewareTwo--1-执行中中间件") 
    // 调用该请求的剩余处理程序 
    ctx.Next() 
    fmt.Println("initMiddlewareTwo--2-执行中中间件") 
}

func main() {

    r := gin.Default() 
    r.GET("/", initMiddlewareOne, initMiddlewareTwo, func(ctx *gin.Context) { 
        fmt.Println("执行路由里面的程序")
        ctx.String(200, "首页--中间件演示") 
    })
    
    r.Run(":8080")
    
}

控制台内容:

Go 复制代码
initMiddlewareOne--1-执行中中间件 
initMiddlewareTwo--1-执行中中间件 
执行路由里面的程序
initMiddlewareTwo--2-执行中中间件 
initMiddlewareOne--2-执行中中间件

3.4 c.Abort()--(了解)

c.Abort() 用于在 Gin 中间件中终止后续中间件和最终处理函数的执行 ,但不会立即停止当前中间件的剩余代码。调用后,Gin 会跳过所有尚未执行的中间件和处理函数,直接进入响应阶段。通常与权限验证或错误处理配合使用,例如在验证失败时调用 c.Abort() 并返回错误响应,同时建议在 Abort() 后加上 return 以避免当前函数继续执行。

总结: Abort 是终止的意思, c.Abort() 表示终止调用该请求的剩余处理程序。

Go 复制代码
package main 
import ( 
    "fmt" 
    "github.com/gin-gonic/gin" 
)

func initMiddlewareOne(ctx *gin.Context) { 
    fmt.Println("initMiddlewareOne--1-执行中中间件") 
    // 调用该请求的剩余处理程序 
    ctx.Next() 
    fmt.Println("initMiddlewareOne--2-执行中中间件") 
} 

func initMiddlewareTwo(ctx *gin.Context) { 
    fmt.Println("initMiddlewareTwo--1-执行中中间件") 
    // 终止调用该请求的剩余处理程序
    ctx.Abort()
    fmt.Println("initMiddlewareTwo--2-执行中中间件") 
    }
    
func main() {
    r := gin.Default() 
    r.GET("/", initMiddlewareOne, initMiddlewareTwo, func(ctx *gin.Context) { 
        fmt.Println("执行路由里面的程序") 
        ctx.String(200, "首页--中间件演示") 
    })
    
    r.Run(":8080") 
    
}

输出结果:

Go 复制代码
initMiddlewareOne--1-执行中间件
initMiddlewareTwo--1-执行中间件
initMiddlewareTwo--2-执行中间件
initMiddlewareOne--2-执行中间件

3.5 全局中间件

Gin 中的全局中间件是通过 r.Use() 注册的中间件,它会作用于所有路由,相当于为整个应用程序设置了一道"通用关卡"。可以将其理解为商场的入口安检------无论顾客想去哪家店铺(对应不同的路由),都必须先通过安检(全局中间件)才能进入。安检可能完成记录访客信息(日志)、检查是否携带危险品(权限校验)等统一任务,确保所有请求都经过相同的前置处理。例如,在 Web 服务中,我们可以注册一个全局的日志中间件,记录每个请求的 URL 和耗时,这样所有接口都会自动带上这个功能,无需在每个路由单独配置。

Go 复制代码
package main

import (
    "fmt"
    "github.com/gin-gonic/gin"
)

// initMiddleware 是一个自定义的全局中间件函数
// 它会在每个请求处理前执行,并通过 ctx.Next() 将控制权传递给后续的中间件或最终处理函数
func initMiddleware(ctx *gin.Context) {
    // 在请求处理前打印日志(该部分代码会在进入实际路由处理函数之前执行)
    fmt.Println("全局中间件 通过 r.Use 配置")

    // 调用该请求的剩余处理程序(即下一个中间件或最终的路由处理函数)
    // 注意:如果不调用 ctx.Next(),后续的处理将不会执行
    ctx.Next()

    // 在 ctx.Next() 之后可以编写后置处理代码,例如记录响应时间、清理资源等
    // 这里的代码会在所有后续中间件和路由处理函数执行完后,按相反顺序执行
    // 例如,可以在这里添加:fmt.Println("中间件后置处理")
}

func main() {
    // 创建一个默认的 Gin 引擎,默认自带 Logger 和 Recovery 中间件
    r := gin.Default()

    // 通过 r.Use 注册全局中间件 initMiddleware,该中间件将应用于所有路由
    // 这意味着无论访问哪个路径,都会先执行 initMiddleware 中的逻辑
    r.Use(initMiddleware)

    // 定义根路径 "/" 的 GET 请求处理函数
    r.GET("/", func(ctx *gin.Context) {
        // 向客户端返回状态码 200 和字符串内容
        ctx.String(200, "首页--中间件演示")
    })

    // 定义 "/news" 路径的 GET 请求处理函数
    r.GET("/news", func(ctx *gin.Context) {
        ctx.String(200, "新闻页面--中间件演示")
    })

    // 启动 HTTP 服务,监听在本地的 8080 端口
    // 默认监听在 0.0.0.0:8080
    r.Run(":8080")
}

在 Web3.0 开发中,Gin 的全局中间件依然是处理通用横切关注点的利器,但业务场景会围绕"去中心化"和"链上交互"这两个核心特征展开。

  • 去中心化身份验证:Web3 应用通常不采用传统的用户名密码登录,而是基于钱包签名。此时可以设计一个全局中间件,在每个需要验证身份的请求(如查看个人NFT、发起交易)前,自动解析请求头中的钱包地址和签名,验证其有效性。这相当于传统Web中的登录态校验,但它验证的是链上身份的 ownership。

  • 链上数据 预加载 与Gas优化:许多Web3应用需要频繁读取链上数据(如代币余额)。为了避免每个接口都重复进行昂贵的节点RPC调用,可以在全局中间件中实现一个缓存层。它可以根据请求中的用户地址,批量查询并缓存其关键的链上资产信息,再将数据注入请求上下文。这不仅提升了响应速度,也有效减少了后端对区块链节点的请求次数,从而优化了潜在的 Gas 成本(如果后端节点是计费服务的话)。

  • 跨链 请求路由 :如果后端服务需要同时与多条区块链(如以太坊、Solana)交互,全局中间件可以根据请求参数(如 chain-id)动态选择并注入对应的区块链客户端实例(如不同的 RPC 连接池)。这隔离了下层区块链协议的复杂性,使得上层业务代码无需关心底层是与哪条链交互。

  • 合规与风控拦截:对于涉及现实世界资产(RWA)或合规交易平台的应用,全局中间件可以集成 KYC/AML 检查模块。例如,在用户发起提现请求时,中间件自动拦截请求,通过调用合规服务验证目标地址是否在黑名单中,或检查该笔交易是否符合反洗钱规则,不符合则直接拒绝。

这些场景的核心思想都是利用全局中间件"处理所有请求的通用逻辑"这一特性,来封装 Web3 应用中复杂、通用且与具体业务无关的基础设施交互,让业务 Handler 更专注于处理核心的去中心化业务逻辑。

以下是针对四个 Web3.0 场景的 Gin 全局中间件具体代码实现,每个案例都包含核心逻辑及详细注释。

1. 去中心化身份验证中间件

Go 复制代码
package middleware

import (
    "fmt"
    "net/http"
    "strings"
    "time"

    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/crypto"
    "github.com/gin-gonic/gin"
)

// AuthMiddleware 验证钱包签名,确认用户身份
func AuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        // 从请求头获取钱包地址和签名
        addrHex := c.GetHeader("X-Wallet-Addr")
        signatureHex := c.GetHeader("X-Signature")
        // 获取用于签名的原始消息,例如当前时间戳(应防重放)
        timestamp := c.GetHeader("X-Timestamp")

        // 基础校验:地址和签名不能为空
        if addrHex == "" || signatureHex == "" || timestamp == "" {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing auth headers"})
            return
        }

        // 构造签名消息(与前端签名内容一致)
        // 示例格式: "Login to NFT market at " + timestamp
        message := fmt.Sprintf("Login to NFT market at %s", timestamp)
        // 以太坊签名需要对消息进行哈希: "\x19Ethereum Signed Message:\n" + len(message) + message
        data := []byte(message)
        hash := crypto.Keccak256Hash([]byte(fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(data), data)))

        // 解码签名(hex 转 []byte)
        sig := common.FromHex(signatureHex)
        if len(sig) != 65 {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid signature length"})
            return
        }

        // 以太坊签名末尾的v值可能是27或28,需要转换为0/1
        if sig[64] == 27 || sig[64] == 28 {
            sig[64] -= 27
        }

        // 恢复公钥
        pubKey, err := crypto.SigToPub(hash.Bytes(), sig)
        if err != nil {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "signature recovery failed"})
            return
        }

        // 从公钥推导地址
        recoveredAddr := crypto.PubkeyToAddress(*pubKey).Hex()

        // 验证地址是否匹配
        if !strings.EqualFold(recoveredAddr, addrHex) {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "address mismatch"})
            return
        }

        // 可选:检查 timestamp 是否在合理时间窗口内,防止重放攻击
        // 此处假设 timestamp 是 RFC3339 格式字符串
        if t, err := time.Parse(time.RFC3339, timestamp); err != nil || time.Since(t) > 5*time.Minute {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "timestamp expired"})
            return
        }

        // 验证通过,将用户地址存入上下文,供后续 handler 使用
        c.Set("userAddr", addrHex)

        // 继续处理请求
        c.Next()
    }
}

2. 链上数据 预加载 与 Gas 优化中间件

Go 复制代码
package middleware

import (
    "context"
    "fmt"
    "sync"
    "time"

    "github.com/ethereum/go-ethereum/ethclient"
    "github.com/gin-gonic/gin"
)

// 模拟缓存(生产环境应使用 Redis)
var cache sync.Map

// PreloadBalanceMiddleware 预加载用户资产余额
func PreloadBalanceMiddleware(rpcClient *ethclient.Client) gin.HandlerFunc {
    return func(c *gin.Context) {
        // 从上下文中获取用户地址(假设已通过 AuthMiddleware 注入)
        addrAny, exists := c.Get("userAddr")
        if !exists {
            // 如果不需要认证的接口,可以跳过;这里假设需要
            c.Next()
            return
        }
        userAddr := addrAny.(string)

        // 检查缓存(5秒有效期)
        cacheKey := "balance:" + userAddr
        if val, ok := cache.Load(cacheKey); ok {
            // 缓存命中,直接注入上下文
            c.Set("userBalances", val)
            c.Next()
            return
        }

        // 并发批量查询多个资产余额(示例:ETH 和两个 ERC20 代币)
        type balanceResult struct {
            Symbol string
            Value  string // 实际应为 *big.Int,此处简化为 string
        }
        results := make([]balanceResult, 0)
        var wg sync.WaitGroup
        var mu sync.Mutex

        // 查询 ETH 余额
        wg.Add(1)
        go func() {
            defer wg.Done()
            balance, err := rpcClient.BalanceAt(context.Background(), common.HexToAddress(userAddr), nil)
            if err == nil {
                mu.Lock()
                results = append(results, balanceResult{Symbol: "ETH", Value: balance.String()})
                mu.Unlock()
            }
        }()

        // 查询 ERC20 代币余额(需要合约地址和 ABI,这里简化调用)
        tokenAddresses := []string{"0x...TokenA", "0x...TokenB"}
        for _, tokenAddr := range tokenAddresses {
            wg.Add(1)
            go func(addr string) {
                defer wg.Done()
                // 此处应构造 ERC20 balanceOf 调用,省略具体实现
                // 假设调用 tokenBalance 函数
                balance := "1000000000000000000" // 模拟结果
                mu.Lock()
                results = append(results, balanceResult{Symbol: addr, Value: balance})
                mu.Unlock()
            }(tokenAddr)
        }

        wg.Wait()

        // 将结果存入缓存(5秒过期)
        cache.Store(cacheKey, results)
        // 设置定时删除(实际应使用带过期时间的缓存库)
        time.AfterFunc(5*time.Second, func() { cache.Delete(cacheKey) })

        // 注入上下文
        c.Set("userBalances", results)

        c.Next()
    }
}

3. 跨链 请求路由中间件

Go 复制代码
package middleware

import (
    "errors"
    "github.com/ethereum/go-ethereum/ethclient"
    "github.com/gin-gonic/gin"
)

// 定义全局 RPC 客户端池(实际应在 main 中初始化)
var clients = map[string]*ethclient.Client{
    "eth": initEthClient(),
    "bsc": initBscClient(),
}

func initEthClient() *ethclient.Client {
    // 实际代码:连接以太坊节点
    client, _ := ethclient.Dial("https://mainnet.infura.io/v3/your-project-id")
    return client
}

func initBscClient() *ethclient.Client {
    // 实际代码:连接 BSC 节点
    client, _ := ethclient.Dial("https://bsc-dataseed.binance.org/")
    return client
}

// ChainRouterMiddleware 根据请求参数注入对应链的客户端
func ChainRouterMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        // 从查询参数或请求头获取 chain 标识
        chain := c.Query("chain") // 例如 ?chain=eth
        if chain == "" {
            chain = c.GetHeader("X-Chain-ID")
        }
        if chain == "" {
            // 未指定 chain,可以默认走以太坊或返回错误
            c.AbortWithStatusJSON(400, gin.H{"error": "missing chain parameter"})
            return
        }

        // 从连接池获取对应客户端
        client, ok := clients[chain]
        if !ok {
            c.AbortWithStatusJSON(400, gin.H{"error": "unsupported chain"})
            return
        }

        // 注入客户端到上下文
        c.Set("blockchainClient", client)

        // 也可注入 chain ID 供后续使用
        c.Set("chainID", chain)

        c.Next()
    }
}

4. 合规与风控拦截中间件

Go 复制代码
package middleware

import (
    "bytes"
    "encoding/json"
    "net/http"
    "time"

    "github.com/gin-gonic/gin"
)

// ComplianceMiddleware 检查提现地址是否在制裁名单中
func ComplianceMiddleware(complianceAPI string) gin.HandlerFunc {
    return func(c *gin.Context) {
        // 仅拦截提现相关路由,可通过路径判断
        if c.Request.URL.Path != "/withdraw" {
            c.Next()
            return
        }

        // 解析请求体中的提现地址(假设是 JSON 格式)
        var req struct {
            ToAddress string `json:"toAddress"`
        }
        if err := c.ShouldBindJSON(&req); err != nil {
            // 如果解析失败,继续后续处理(或返回错误),这里为了演示不拦截
            c.Next()
            return
        }

        // 调用合规服务检查地址
        payload, _ := json.Marshal(map[string]string{"address": req.ToAddress})
        resp, err := http.Post(complianceAPI+"/check", "application/json", bytes.NewReader(payload))
        if err != nil {
            // 服务不可用时,保守起见拒绝请求
            c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{"error": "compliance service unavailable"})
            return
        }
        defer resp.Body.Close()

        var result struct {
            Blocked bool `json:"blocked"`
        }
        if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
            c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "compliance response invalid"})
            return
        }

        if result.Blocked {
            // 地址在黑名单中,拒绝请求并记录审计日志
            // 异步记录日志(避免阻塞)
            go auditLog(req.ToAddress, c.ClientIP())
            c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "address is blocked"})
            return
        }

        // 通过,继续处理
        c.Next()
    }
}

// auditLog 模拟审计日志记录
func auditLog(address, ip string) {
    // 可将日志写入文件或数据库
    println(time.Now().Format(time.RFC3339), "blocked withdrawal attempt", address, ip)
}

使用示例:main.go 中注册这些中间件:

Go 复制代码
package main

import (
    "github.com/gin-gonic/gin"
    "your-project/middleware"
)

func main() {
    r := gin.Default()

    // 全局注册:身份验证 + 预加载 + 合规风控(按需注册,此处仅示例)
    r.Use(middleware.AuthMiddleware())
    r.Use(middleware.PreloadBalanceMiddleware(ethClient))
    r.Use(middleware.ComplianceMiddleware("http://compliance-api.local"))

    // 针对跨链路由,可能放在具体分组中
    chainGroup := r.Group("/api/v1")
    chainGroup.Use(middleware.ChainRouterMiddleware())
    {
        chainGroup.GET("/balance", getBalanceHandler)
    }

    r.Run()
}

以上代码展示了如何在 Gin 中利用全局中间件处理 Web3.0 特有的身份验证、数据预加载、跨链路由和合规风控,实现了关注点分离和通用逻辑复用。

3.6 在路由分组中配置中间件

在 Gin 框架中,路由分组中间件允许为具有相同路径前缀 的一组路由批量应用通用处理逻辑,例如对 /admin 分组统一进行权限校验或日志记录。这种配置方式既避免了为每个路由重复注册中间件,又能与全局中间件协同工作,实现对不同模块的精细化控制。分组中间件仅在匹配该分组的路由上执行,从而实现关注点分离和代码复用。

为路由组注册中间件有以下两种写法:

写法 1:

Gin 框架中创建路由分组时直接绑定中间件的方法:通过 r.Group("/shop", statCost())statCost 中间件应用于整个 /shop 分组,使得分组内所有路由(如 /shop/index)在执行业务逻辑前都会自动执行该中间件(如统计请求耗时),实现了中间件与路由分组的简洁整合,便于对特定模块进行统一处理。

Go 复制代码
// 创建一个路由分组,路径前缀为 "/shop",并应用 StatCost() 中间件
// StatCost() 是一个返回 gin.HandlerFunc 的函数,通常用于统计请求处理耗时
shopGroup := r.Group("/shop", StatCost())

// 在分组内部定义路由
{
    // 定义 GET 请求路径 "/shop/index" 的处理函数
    // 该路由会先执行 StatCost 中间件,然后执行此匿名函数
    shopGroup.GET("/index", func(c *gin.Context) {
        // 处理 /shop/index 请求的业务逻辑
        // ...
    })

    // 可以在此继续添加其他分组路由,例如:
    // shopGroup.POST("/order", placeOrderHandler)
    // ...
}

案例1:

Go 复制代码
package main

import (
    "fmt"
    "time"

    "github.com/gin-gonic/gin"
)

// statCost 是一个返回 gin.HandlerFunc 的中间件工厂函数
// 用于统计请求处理耗时,并在控制台打印
func statCost() gin.HandlerFunc {
    return func(c *gin.Context) {
        // 记录请求开始时间
        start := time.Now()

        // 调用后续的处理函数
        c.Next()

        // 计算耗时
        cost := time.Since(start)
        // 打印请求路径和处理耗时
        fmt.Printf("请求路径: %s, 处理耗时: %v\n", c.Request.URL.Path, cost)
    }
}

func main() {
    // 创建一个默认的 Gin 引擎
    r := gin.Default()

    // 创建一个路由分组,路径前缀为 "/shop",并应用 statCost() 中间件
    // 所有以 /shop 开头的请求都会先执行 statCost 中间件
    shopGroup := r.Group("/shop", statCost())

    // 在分组内部定义路由
    {
        // 定义 GET 请求路径 "/shop/index" 的处理函数
        // 该路由会先执行 statCost 中间件,然后执行此匿名函数
        shopGroup.GET("/index", func(c *gin.Context) {
            // 模拟业务处理耗时
            time.Sleep(50 * time.Millisecond)
            c.String(200, "欢迎访问商店首页")
        })

        // 定义 POST 请求路径 "/shop/order" 的处理函数
        // 同样会先执行 statCost 中间件
        shopGroup.POST("/order", func(c *gin.Context) {
            // 模拟业务处理耗时
            time.Sleep(100 * time.Millisecond)
            c.JSON(200, gin.H{"message": "订单创建成功"})
        })
    }

    // 启动 HTTP 服务,监听在 8080 端口
    r.Run(":8080")
}

写法 2:

Gin 框架中为路由分组添加中间件的另一种方式:先通过 r.Group("/shop") 创建分组,再使用 Use 方法单独为分组绑定 statCost 中间件。这种方式将分组的创建与中间件的应用分离,使得代码更灵活清晰,同样确保分组内所有路由(如 /shop/index)在执行业务逻辑前自动执行中间件,实现对特定路由模块的统一处理(如请求耗时统计)。

Go 复制代码
// 创建一个路由分组,路径前缀为 "/shop"
// 此时分组尚未绑定任何中间件
shopGroup := r.Group("/shop")

// 通过 Use 方法为 shopGroup 分组添加中间件 StatCost()
// StatCost() 是一个返回 gin.HandlerFunc 的函数,通常用于统计请求处理耗时
// 该中间件将应用于 shopGroup 分组内的所有路由
shopGroup.Use(StatCost())

// 使用大括号包裹分组内的路由定义,提升代码可读性(非必需,但推荐)
{
    // 定义 GET 请求路径 "/shop/index" 的处理函数
    // 该路由会先执行 StatCost 中间件,然后执行此匿名函数处理业务逻辑
    shopGroup.GET("/index", func(c *gin.Context) {
        // 处理 /shop/index 请求的具体业务逻辑
        // ...
    })

    // 可以在此继续添加其他分组路由,例如:
    // shopGroup.POST("/order", placeOrderHandler)
    // ...
}

案例如下:

Go 复制代码
package main

import (
    "fmt"
    "time"

    "github.com/gin-gonic/gin"
)

// statCost 是一个返回 gin.HandlerFunc 的中间件工厂函数
// 用于统计请求处理耗时,并在控制台打印
func statCost() gin.HandlerFunc {
    return func(c *gin.Context) {
        // 记录请求开始时间
        start := time.Now()

        // 调用后续的处理函数
        c.Next()

        // 计算耗时
        cost := time.Since(start)
        // 打印请求路径和处理耗时
        fmt.Printf("请求路径: %s, 处理耗时: %v\n", c.Request.URL.Path, cost)
    }
}

func main() {
    // 创建一个默认的 Gin 引擎
    r := gin.Default()

    // 创建一个路由分组,路径前缀为 "/shop"
    // 此时分组尚未绑定任何中间件
    shopGroup := r.Group("/shop")

    // 通过 Use 方法为 shopGroup 分组添加中间件 statCost()
    // 该中间件将应用于 shopGroup 分组内的所有路由
    shopGroup.Use(statCost())

    // 使用大括号包裹分组内的路由定义,提升代码可读性(非必需,但推荐)
    {
        // 定义 GET 请求路径 "/shop/index" 的处理函数
        // 该路由会先执行 statCost 中间件,然后执行此匿名函数处理业务逻辑
        shopGroup.GET("/index", func(c *gin.Context) {
            // 模拟业务处理耗时
            time.Sleep(50 * time.Millisecond)
            c.String(200, "欢迎访问商店首页")
        })

        // 定义 POST 请求路径 "/shop/order" 的处理函数
        // 同样会先执行 statCost 中间件
        shopGroup.POST("/order", func(c *gin.Context) {
            // 模拟业务处理耗时
            time.Sleep(100 * time.Millisecond)
            c.JSON(200, gin.H{"message": "订单创建成功"})
        })
    }

    // 启动 HTTP 服务,监听在 8080 端口
    r.Run(":8080")
}

2、分组路由 AdminRoutes.go 中配置中间件

Go 复制代码
// Package routes 负责定义和初始化应用程序的所有路由
package routes

import (
    "fmt"                                   // 提供格式化输入输出功能,用于打印日志
    "gin_demo/controller/admin"             // 导入后台管理控制器包,用于处理具体的业务逻辑
    "net/http"                              // 提供 HTTP 状态码等常量
    "github.com/gin-gonic/gin"              // Gin Web 框架的核心包
)

// initMiddleware 是一个自定义的中间件函数,仅应用于后台管理路由分组
// 它会在每个匹配 /admin 前缀的请求处理前执行,打印日志,然后调用 ctx.Next() 将控制权交给下一个处理器
func initMiddleware(ctx *gin.Context) {
    fmt.Println("路由分组中间件")               // 控制台输出,标记中间件执行
    // 调用该请求的剩余处理程序(即后续的中间件或最终的路由处理函数)
    ctx.Next()
}

// AdminRoutesInit 初始化后台管理相关的所有路由
// 参数 router 是 Gin 的引擎实例,通过它来注册路由分组和具体的路由规则
func AdminRoutesInit(router *gin.Engine) {

    // 创建一个路由分组,路径前缀为 "/admin",并立即应用 initMiddleware 中间件
    // 这意味着该分组内的所有路由(如 /admin/user, /admin/user/add, /admin/news)都会先执行 initMiddleware
    adminRouter := router.Group("/admin", initMiddleware)

    // 使用大括号包裹分组内的路由定义,提高代码可读性(语法上并非必需)
    {
        // 注册 GET 请求路径 "/admin/user"
        // 当访问 /admin/user 时,首先执行 initMiddleware,然后调用 admin.UserController{}.Index 方法处理请求
        adminRouter.GET("/user", admin.UserController{}.Index)

        // 注册 GET 请求路径 "/admin/user/add"
        // 处理流程同上,由 admin.UserController{}.Add 方法处理
        adminRouter.GET("/user/add", admin.UserController{}.Add)

        // 注册 GET 请求路径 "/admin/news"
        // 此处直接使用匿名函数作为处理程序,返回字符串 "news"
        adminRouter.GET("/news", func(c *gin.Context) {
            c.String(http.StatusOK, "news")   // 向客户端返回状态码 200 和内容 "news"
        })
    }
}

3.7 中间件和对应控制器之间共享数据

在 Gin 框架中,中间件和控制器之间通过 gin.ContextSetGet 方法实现数据共享。中间件在处理请求时可以将用户信息、请求ID、 预加载 数据 等存入上下文(例如 c.Set("user", userInfo)),后续的控制器或其他中间件可以通过 c.Get("user") 取出这些数据,从而在同一请求的生命周期内实现灵活、安全的跨处理器数据传递。

设置值:

Go 复制代码
ctx.Set("username", "张三")

获取值:

Go 复制代码
username, _ := ctx.Get("username")

中间件设置值:

Go 复制代码
func InitAdminMiddleware(ctx *gin.Context) { 
    fmt.Println("路由分组中间件") 
    // 可以通过 ctx.Set 在请求上下文中设置值,后续的处理函数能够取到该值 
    ctx.Set("username", "张三")
    
    // 调用该请求的剩余处理程序 
    ctx.Next() 
}

控制器获取值:

Go 复制代码
func (c UserController) Index(ctx *gin.Context) { 
    username, _ := ctx.Get("username") 
    fmt.Println(username)
    ctx.String(http.StatusOK, "这是用户首页 111")
}

以下是一个完整的 Gin 代码案例,演示中间件如何向控制器共享数据:

Go 复制代码
package main

import (
    "fmt"
    "net/http"

    "github.com/gin-gonic/gin"
)

// UserController 定义用户相关的控制器结构体
type UserController struct{}

// Index 处理用户首页请求,从上下文中获取中间件设置的值
func (c UserController) Index(ctx *gin.Context) {
    // 从上下文中获取中间件设置的 "username"
    username, _ := ctx.Get("username")
    // 打印用户名到控制台
    fmt.Println(username)
    // 返回响应
    ctx.String(http.StatusOK, "这是用户首页 111")
}

// InitAdminMiddleware 是一个自定义的中间件,用于在上下文中设置值
func InitAdminMiddleware(ctx *gin.Context) {
    fmt.Println("路由分组中间件")
    // 通过 ctx.Set 在请求上下文中设置值,后续的处理函数能够取到该值
    ctx.Set("username", "张三")
    // 调用该请求的剩余处理程序(即下一个中间件或最终的控制器)
    ctx.Next()
}

func main() {
    // 创建默认的 Gin 引擎
    r := gin.Default()

    // 创建后台管理路由分组,路径前缀为 "/admin",并应用中间件
    adminRouter := r.Group("/admin", InitAdminMiddleware)
    {
        // 注册路由 /admin/user,由 UserController 的 Index 方法处理
        adminRouter.GET("/user", UserController{}.Index)
    }

    // 启动服务,监听在 8080 端口
    r.Run(":8080")
}

代码说明:

1、中间件 AuthMiddleware

  • 从请求头 X-Token 中获取 token。

  • 模拟数据库查询,若 token 存在则获取对应用户名。

  • 使用 c.Set("userName", userName) 将用户名存入 Gin 上下文。

2、控制器(处理函数)

  • 通过 c.Get("userName") 取出中间件存入的用户名。

  • 如果存在,则继续处理业务逻辑(如返回欢迎消息或订单列表)。

  • 使用类型断言将取出的 interface{} 转换为字符串。

3、请求流程

  • 访问 /api/profile 时,先执行 AuthMiddleware,验证通过后存入 userName

  • 接着执行控制器,从上下文获取用户名并返回。

这个案例清晰地展示了中间件如何设置数据,以及后续控制器如何获取并使用这些共享数据,实现了请求处理过程中的数据传递。

3.8 中间件注意事项

gin 默认中间件

gin.Default()默认使用了 LoggerRecovery 中间件,其中:

  • Logger 中间件将日志写入 gin.DefaultWriter,即使配置了GIN_MODE=release。

  • Recovery 中间件会 recover 任何 panic。如果有 panic 的话,会写入 500 响应码。

如果不想使用上面两个默认的中间件,可以使用 **gin.New()**新建一个没有任何默认中间件的路由。

gin 中间件中使用 goroutine 当在中间件或 handler 中启动新的 goroutine 时,不能使用原始的上下文(c *gin.Context),必须使用其只读副本(c.Copy())

Go 复制代码
r.GET("/", func(c *gin.Context) { 
    cCp := c.Copy() 
    go func() { 
        // simulate a long task with time.Sleep(). 5 seconds 
        time.Sleep(5 * time.Second) 
        // 这里使用你创建的副本 
        fmt.Println("Done! in path " + cCp.Request.URL.Path) 
    }() 
    c.String(200, "首页")
})

在 Gin 框架中,*gin.Context 是每个请求的上下文对象,它包含了请求相关的数据、参数和响应等信息。Gin 为了高性能,采用了对象池(sync.Pool)来复用 Context 对象:当一个请求处理完成后,Context 会被重置并放回池中,供下一个请求使用。这意味着原始的 Context 在请求结束后可能被复用或修改,因此它不是并发安全的。如果在中间件或处理函数中启动新的 goroutine,并且直接使用原始的 c *gin.Context,可能会发生以下问题:

  • 数据竞争 :goroutine 可能与主请求处理流程同时读写 Context 中的字段(如请求头、参数),导致不可预料的错误。

  • 悬垂指针 :当主请求处理完成后,Context 被放回池中并可能被重新分配给其他请求,此时 goroutine 仍在访问旧的 Context,会造成数据错乱甚至程序崩溃。

因此,Gin 提供了 c.Copy() 方法,它会返回一个 Context 的只读副本。这个副本是独立的,不会受到主请求结束后 Context 复用的影响,可以安全地在 goroutine 中使用(但注意副本仍然是只读的,不能修改响应等操作)。这样既保证了并发安全,又避免了数据竞争。

具体案例实现: 演示如何在处理函数中启动 goroutine 并安全地使用 c.Copy()

Go 复制代码
package main

import (
    "fmt"
    "time"

    "github.com/gin-gonic/gin"
)

func main() {
    // 创建默认的 Gin 引擎
    r := gin.Default()

    // 定义一个 GET 路由,路径为根路径 "/"
    r.GET("/", func(c *gin.Context) {
        // 创建原始上下文的只读副本,用于在 goroutine 中安全使用
        cCp := c.Copy()

        // 启动一个 goroutine 执行后台任务(例如耗时操作)
        go func() {
            // 模拟一个耗时 5 秒的任务
            time.Sleep(5 * time.Second)
            // 使用副本 cCp 访问请求信息,不会与主请求上下文冲突
            fmt.Println("Done! in path " + cCp.Request.URL.Path)
        }()

        // 立即向客户端返回响应,不等待 goroutine 完成
        c.String(200, "首页")
    })

    // 启动 HTTP 服务,监听在 8080 端口
    r.Run(":8080")
}

第四章 Gin 中自定义 Model

4.1 关于 Model

如果我们的应用非常简单的话,我们可以在 Controller 里面处理常见的业务逻辑。但是如果我们有一个功能想在多个控制器、或者多个模板里面复用的话,那么我们就可以把公共的功能单独抽取出来作为一个模块(Model)。 Model 是逐步抽象的过程,一般我们会在 Model 里面封装一些公共的方法让不同 Controller 使用,也可以在 Model 中实现和数据库打交道。

4.1.1 Model 里面封装公共的方法

在 Gin 或其他 Web 框架的 MVC 架构中,Model 层封装公共方法是指将与数据持久化 相关的通用逻辑(如数据库的增删改查、数据校验、关联查询等)抽象为可复用的函数或方法,从而避免在每个业务处理函数中重复编写相同的底层数据库操作代码。这样做不仅能提高开发效率,还能确保数据访问逻辑的一致性,当数据源或表结构发生变化时,只需修改 Model 层的公共方法,而无需逐一调整所有业务调用,极大地提升了项目的可维护性和扩展性。

1、项目代码结构如下:

Go 复制代码
project/
├── main.go
├── models/
│   └── web3_model.go
├── controllers/
│   └── web3_controller.go
└── templates/
    └── index.html

文件路径:models/web3_model.go

封装 Web3 相关的公共方法(模拟数据获取、哈希计算、时间格式化)。

Go 复制代码
package models

import (
    "crypto/sha256"
    "fmt"
    "math/rand"
    "time"
)

// Web3Model 是一个用于封装 Web3 相关业务逻辑的结构体。
// 它可以包含数据库连接、区块链客户端等依赖项,此处作为示例未包含具体依赖。
type Web3Model struct{}

// GetCurrentBlockHeight 模拟获取当前区块链的区块高度。
// 实际生产环境中应通过 RPC 调用区块链节点获取真实高度。
// 返回一个 int64 类型的模拟区块高度,范围在 15,000,000 到 15,999,999 之间。
func (m Web3Model) GetCurrentBlockHeight() int64 {
    // 使用随机数模拟区块高度,范围:15000000 ~ 15999999
    return int64(rand.Intn(1000000) + 15000000)
}

// GetGasPrice 模拟获取当前的 Gas 价格。
// Gas 价格通常以 Gwei 为单位,此处返回一个 10 到 100 Gwei 之间的随机整数。
// 实际应用中应从区块链节点获取实时 Gas 价格。
func (m Web3Model) GetGasPrice() int64 {
    // 随机生成 10 到 100 之间的 Gas 价格(单位:Gwei)
    return int64(rand.Intn(90) + 10)
}

// FormatTimestamp 将 Unix 时间戳(秒)格式化为人类可读的日期时间字符串。
// 格式遵循 Go 的参考时间 "2006-01-02 15:04:05"。
// 参数 timestamp:Unix 时间戳(秒)。
// 返回格式化后的日期时间字符串,例如 "2023-10-05 14:30:00"。
func (m Web3Model) FormatTimestamp(timestamp int64) string {
    return time.Unix(timestamp, 0).Format("2006-01-02 15:04:05")
}

// HashData 使用 SHA256 算法计算输入字符串的哈希值。
// SHA256 是 Web3 开发中常见的哈希算法,用于生成数据的唯一指纹。
// 参数 data:待哈希的原始字符串。
// 返回一个十六进制字符串表示的哈希值,长度为 64 个字符。
func (m Web3Model) HashData(data string) string {
    h := sha256.New()
    h.Write([]byte(data))
    return fmt.Sprintf("%x", h.Sum(nil))
}

4.1.2 控制器中调用 Model

文件路径:controllers/web3_controller.go

处理 HTTP 请求,调用 Model 层方法,返回响应。

Go 复制代码
// Package controllers 包含 HTTP 请求的处理函数,负责与模型层交互并返回响应。
package controllers

import (
    "net/http"
    "time"

    "github.com/gin-gonic/gin"
    // 请根据实际模块路径替换 "yourmodule/models"
    // 例如:如果你的模块名为 "model_project",则导入 "model_project/models"
    "model_project/models"
)

// ShowIndex 处理 Web 应用的首页请求。
// 它通过调用 models.Web3Model 中的方法获取模拟的区块链数据(当前区块高度、Gas 价格),
// 并获取当前时间戳,然后将这些数据传递给 index.html 模板进行渲染。
// 参数 c: Gin 上下文,包含请求和响应的方法。
// 无返回值,但通过 c.HTML 直接向客户端返回 HTML 页面。
func ShowIndex(c *gin.Context) {
    // 实例化 models.Web3Model 结构体,准备调用其方法
    var model models.Web3Model

    // 调用模型层方法获取模拟的区块高度
    blockHeight := model.GetCurrentBlockHeight()
    // 调用模型层方法获取模拟的 Gas 价格(单位 Gwei)
    gasPrice := model.GetGasPrice()
    // 获取当前 Unix 时间戳(秒)
    currentTime := time.Now().Unix()

    // 使用 gin.H 将数据封装为 map,并渲染 index.html 模板
    c.HTML(http.StatusOK, "index.html", gin.H{
            "blockHeight": blockHeight,
            "gasPrice":    gasPrice,
            "currentTime": currentTime,
    })
}

// HashHandler 处理哈希计算请求(通常通过 GET 方式调用)。
// 它从查询参数中获取待哈希的字符串(参数名为 "data"),
// 调用 models.Web3Model.HashData 方法计算其 SHA256 哈希值,
// 并以 JSON 格式返回原始输入和计算出的哈希值。
// 如果未提供 data 参数,则返回 400 Bad Request 错误信息。
// 参数 c: Gin 上下文,用于读取请求参数和返回响应。
// 无返回值,但通过 c.String 或 c.JSON 直接返回响应。
func HashHandler(c *gin.Context) {
    // 从 URL 查询参数中获取 "data" 的值,例如 /hash?data=hello
    input := c.Query("data")
    // 如果参数为空,返回错误信息并终止处理
    if input == "" {
            c.String(http.StatusBadRequest, "缺少参数 data")
            return
    }

    // 实例化 models.Web3Model
    var model models.Web3Model
    // 调用模型层方法计算输入字符串的 SHA256 哈希
    hash := model.HashData(input)

    // 以 JSON 格式返回原始输入和哈希结果
    c.JSON(http.StatusOK, gin.H{
            "input": input,
            "hash":  hash,
    })
}

4.1.3 main函数

文件路径: main.go

**程序入口:**初始化 Gin,加载模板,注册模板函数和路由。

Go 复制代码
// Package main 是程序的入口包,负责初始化 Gin 引擎、配置模板函数、注册路由并启动 HTTP 服务。
package main

import (
    "html/template" // 用于自定义模板函数

    // 导入自定义包,注意根据实际模块名调整导入路径
    "model_project/controllers" // 控制器包,处理具体路由请求
    "model_project/models"      // 模型包,提供 Web3 相关的数据操作方法

    "github.com/gin-gonic/gin" // Gin Web 框架
)

// main 函数是整个应用程序的入口点。
// 它执行以下步骤:
// 1. 创建默认的 Gin 引擎(包含 Logger 和 Recovery 中间件)。
// 2. 设置自定义模板函数,将 models.Web3Model 的 FormatTimestamp 方法注册为 "formatTime",
//    以便在模板中格式化时间戳。
// 3. 加载 templates 目录下的所有 HTML 模板文件,使模板函数生效。
// 4. 注册路由:
//    - GET "/" 由 controllers.ShowIndex 处理,返回 Web3 信息页面。
//    - GET "/hash" 由 controllers.HashHandler 处理,返回哈希计算结果的 JSON。
// 5. 启动 HTTP 服务,监听在 8080 端口。
func main() {
    // 创建 Gin 引擎实例,使用默认中间件(日志和恢复)
    r := gin.Default()

    // 设置自定义模板函数映射
    // 必须在加载模板之前调用,否则模板编译时会找不到函数
    r.SetFuncMap(template.FuncMap{
            "formatTime": func(timestamp int64) string {
                    // 实例化模型,调用其格式化时间方法
                    var model models.Web3Model
                    return model.FormatTimestamp(timestamp)
            },
    })

    // 加载 templates 目录下的所有以 .html 结尾的文件
    // 此时自定义函数已注册,模板可以正确解析 {{ .currentTime | formatTime }}
    r.LoadHTMLGlob("templates/*")

    // 配置路由
    // 首页路由,展示区块高度、Gas 价格等信息
    r.GET("/", controllers.ShowIndex)
    // 哈希计算接口,接受查询参数 data,返回其 SHA256 哈希
    r.GET("/hash", controllers.HashHandler)

    // 启动 Web 服务,默认监听 0.0.0.0:8080
    // 可通过参数自定义端口,例如 r.Run(":9090")
    r.Run(":8080")
}

4. 视图模板

文件路径: templates/index.html

视图模板,展示 Web3 信息并使用模板函数。

html 复制代码
<!DOCTYPE html>
<html>
<head>
    <title>Web3 信息看板</title>
</head>
<body>
    <h1>Web3 实时信息</h1>
    <p>当前区块高度:{{ .blockHeight }}</p>
    <p>当前 Gas 价格:{{ .gasPrice }} Gwei</p>
    <p>当前时间(原始时间戳):{{ .currentTime }}</p>
    <p>当前时间(格式化后):{{ .currentTime | formatTime }}</p>
    <hr>
    <h2>计算哈希</h2>
    <form action="/hash" method="get">
        <input type="text" name="data" placeholder="输入字符串">
        <button type="submit">计算 SHA256</button>
    </form>
</body>
</html>

相关包问题处理:

在项目目录下重新执行以下命令即可:

html 复制代码
go mod init model_project

之后,你就可以在代码中正常导入本地包(如 modelscontrollers),并使用正确的模块路径了。如果之前已经生成了 go.mod 文件但内容有误,可以先删除它再重新初始化。

在项目根目录下打开终端,执行以下命令:

html 复制代码
go get github.com/gin-gonic/gin

该命令会下载 Gin 框架并将其添加到 go.modgo.sum 文件中。之后,再执行:

html 复制代码
go mod tidy

用于同步依赖,确保所有需要的包都已记录并下载。完成后,IDE 中的红色报错应该就会消失。如果仍然有错,可以尝试重启 IDE 或重新打开项目。

最终项目文件目录如下:

运行说明: 运行 go run main.go 或者 fresh 启动服务,访问 http://localhost:8080 即可看到效果。

这样,Model 的公共方法被独立封装,控制器调用 Model 逻辑,模板函数注册后可在视图中使用,实现了清晰的代码分层。

4.2 Golang MD5 加密

在 Go 语言中,MD5 加密通过标准库 crypto/md5 实现,核心是使用 md5.Sum() 函数对字节切片计算哈希,返回一个 16 字节的数组 ,通常使用 fmt.Sprintf("%x", ...) 格式化为十六进制字符串,或者通过 hash.Hash 接口进行流式写入(如 md5.New() 配合 Write()Sum()),适用于大文件或流数据;

**注意:**MD5 算法本身不可逆,且因安全性问题(易碰撞)已不适用于密码存储等安全敏感场景,但仍广泛用于数据完整性校验(如文件一致性检查)。

打开 golang 包对应的网站:https://pkg.go.dev/,搜索 md5

方法一:

Go 复制代码
package main

import (
        "crypto/md5" // 导入 MD5 哈希算法包,用于计算数据的 MD5 摘要
        "fmt"        // 导入格式化包,用于输出结果
)

func main() {
        // 定义要计算 MD5 的原始数据,这里使用字符串 "123456" 转换为字节切片
        data := []byte("123456")

        // md5.Sum 函数计算数据的 MD5 哈希值,返回一个 [16]byte 数组(即 16 字节的固定长度数组)
        // 注意:md5.Sum 接受字节切片参数,并直接返回哈希结果,不需要创建哈希对象
        has := md5.Sum(data)

        // 使用 fmt.Sprintf 将字节数组格式化为十六进制字符串
        // "%x" 格式动词表示将数据转换为十六进制表示形式(小写字母)
        md5str := fmt.Sprintf("%x", has)

        // 打印最终的 MD5 哈希字符串到控制台
        fmt.Println(md5str)
}

方法二:

Go 复制代码
package main

import (
        "crypto/md5" // 提供 MD5 哈希算法
        "fmt"        // 格式化输入输出
        "io"         // 提供 I/O 工具函数,如 WriteString
)

func main() {
    // 创建一个新的 MD5 哈希对象
    // md5.New() 返回 hash.Hash 接口,该接口实现了 io.Writer 和 io.Reader
    h := md5.New()

    // 将字符串 "123456" 写入哈希对象中
    // io.WriteString 将字符串写入到实现了 io.Writer 接口的对象 h 中
    // 写入的数据会参与哈希计算,但不直接返回结果
    io.WriteString(h, "123456")

    // 计算并输出最终的 MD5 哈希值
    // h.Sum(nil) 会将当前哈希值追加到参数切片中,由于传入 nil,会返回一个新的字节切片
    // 使用 %x 格式动词将字节切片格式化为十六进制字符串并打印
    fmt.Printf("%x\n", h.Sum(nil))
}

第五章 Gin 文件上传

在 Gin 框架中实现文件上传,首先需要在HTML 表单中设置 enctype="multipart/form-data",然后在后端通过 c.FormFile("file") 获取上传的文件对象,接着可以使用 c.SaveUploadedFile() 方法将文件保存到指定路径,或者手动打开文件进行流式处理;Gin 会自动解析 multipart 表单,并限制默认内存大小(32MB),可通过 c.Request.ParseMultipartForm() 调整,同时需要注意处理错误和设置合理的文件大小限制以防止内存攻击。

注意: 需要在上传文件的 form 表单上面需要加入 enctype="multipart/form-data"

5.1 单文件上传

**官网地址:**https://gin-gonic.com/zh-cn/docs/examples/upload-file/single-file/

官方示例,实现文件上传:

1、 main函数 内容

Go 复制代码
package main

import (
    "fmt"
    "log"
    "net/http"
    "github.com/gin-gonic/gin"
)

func main() {
    // 创建一个默认配置的 Gin 引擎
    // gin.Default() 会附加 Logger 和 Recovery 两个中间件
    router := gin.Default()

    // 设置 multipart/form-data 解析时允许的最大内存
    // 默认值为 32 MiB,这里修改为 8 MiB,超过大小的文件部分会写入临时文件
    router.MaxMultipartMemory = 8 << 20 // 8 MiB

    // 注册 POST 路由 /upload,用于处理文件上传
    router.POST("/upload", func(c *gin.Context) {
        // 从表单中获取上传的文件,表单字段名为 "file"
        // c.FormFile 返回第一个上传的文件和可能的错误
        // 注意:实际生产代码应处理 error,此处为简洁而忽略
        file, _ := c.FormFile("file")
        
        if err != nil {
            // 区分不同错误类型给出更具体的提示
            if err == http.ErrMissingFile {
                    c.JSON(http.StatusBadRequest, gin.H{"error": "未提供文件,请选择文件上传"})
            } else {
                    // 其他错误(如超过 MaxMultipartMemory 限制等)
                    c.JSON(http.StatusBadRequest, gin.H{"error": "文件上传失败: " + err.Error()})
            }
            return
        }
        
        // 打印上传的文件名到日志
        log.Println("Received file:", file.Filename)

        // 定义文件保存的目标路径(需要预先定义 dst,例如 "./uploads/"+file.Filename)
        // 此处假设 dst 已经定义,实际使用时请替换为有效路径
        dst := "./uploads/" + file.Filename

        // 将上传的文件保存到指定路径
        // c.SaveUploadedFile 会处理文件的保存,如果目录不存在会返回错误
        err := c.SaveUploadedFile(file, dst)
        if err != nil {
                // 如果保存失败,返回 500 错误
                c.String(http.StatusInternalServerError, fmt.Sprintf("保存文件失败: %s", err.Error()))
                return
        }

        // 返回成功响应,告知客户端文件已上传
        c.String(http.StatusOK, fmt.Sprintf("'%s' 上传成功!", file.Filename))
    })

    // 启动 HTTP 服务,监听在 8080 端口
    // router.Run 默认监听 :8080,可自定义地址如 "localhost:8080"
    router.Run(":8080")
}

函数功能说明:

  • gin.Default():创建 Gin 引擎实例,并附加了两个默认中间件(日志记录和 panic 恢复),便于快速开发。

  • router.MaxMultipartMemory:设置解析 multipart 表单时允许的内存上限,超出部分存入临时文件,防止大文件导致内存溢出。

  • router.POST() :注册路由和处理函数,当客户端以 POST 方法请求/upload路径时执行匿名函数。

  • c.FormFile("file") :从 HTTP 请求中提取名为 "file" 的上传文件,返回 *multipart.FileHeader 对象和错误信息。

  • log.Println():打印日志信息,便于调试和监控。

  • c.SaveUploadedFile(file, dst) :将上传的文件保存到服务器的指定路径 dst 中。

  • c.String():向客户端返回纯文本响应,并指定 HTTP 状态码。

  • router.Run():启动 Gin 服务器,开始监听并处理请求。

2、定义模板

需要在上传文件的 form 表单上面需要加入 enctype="multipart/form-data"

html 复制代码
<!-- 相当于给模板定义一个名字 define end 成对出现--> 
{{ define "admin/user/add.html" }} 
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title> 
</head>
<body>
    <form action="/admin/user/doAdd" method="post" enctype="multipart/form-data">
    用户名: <input type="text" name="username" placeholder="用户名"> <br> <br>
    头像:<input type="file" name="face"><br> <br>
    <input type="submit" value="提交">
    </form>
</body>
</html>
{{ end }}

3、定义业务逻辑

Go 复制代码
// DoAdd 处理用户添加请求,接收用户名和头像文件并保存
// 该方法从 POST 表单中获取用户名和上传的头像文件,将文件保存到指定目录,
// 最后返回包含用户名和文件名的 JSON 响应
func (c UserController) DoAdd(ctx *gin.Context) {
    // 从 POST 表单中获取用户名字段的值
    username := ctx.PostForm("username")

    // 从表单中获取名为 "face" 的上传文件
    // 返回文件头信息和可能的错误
    file, err := ctx.FormFile("face")
    if err != nil {
        // 如果获取文件失败,返回 500 内部错误,并将错误信息返回给客户端
        ctx.JSON(http.StatusInternalServerError, gin.H{
                "message": err.Error(),
        })
        return
    }

    // 构建文件保存的目标路径
    // 使用 path.Join 拼接静态上传目录和原始文件名(注意:生产环境应避免直接使用原始文件名)
    dst := path.Join("./static/upload", file.Filename)
    fmt.Println(dst) // 打印保存路径,便于调试(生产环境建议使用日志)

    // 将上传的文件保存到指定的路径
    // 如果保存失败,SaveUploadedFile 会返回错误,但此处未处理(生产环境应添加错误处理)
    ctx.SaveUploadedFile(file, dst)

    // 返回成功响应,包含用户名和文件名信息
    ctx.JSON(http.StatusOK, gin.H{
        "message":  fmt.Sprintf("'%s' uploaded!", file.Filename),
        "username": username,
    })
}

5.2 多文件上传--不同名字的多个文件

1、定义模板

需要在上传文件的 form 表单上面需要加入enctype="multipart/form-data"

html 复制代码
<!-- 相当于给模板定义一个名字 define end 成对出现--> 
{{ define "admin/user/add.html" }} 
<!DOCTYPE html> 
<html lang="en"> 
<head> 
    <meta charset="UTF-8"> 
    <meta http-equiv="X-UA-Compatible" content="IE=edge"> 
    <meta name="viewport" content="width=device-width, initial-scale=1.0"> 
    <title>Document</title> 
</head> 
<body> 
    <form action="/admin/user/doAdd" method="post" enctype="multipart/form-data"> 
    用户名: <input type="text" name="username" placeholder="用户名"> <br> <br> 
    头像 1:<input type="file" name="face1"><br> <br> 
    头像 2:<input type="file" name="face2"><br> <br> 
    <input type="submit" value="提交"> 
    </form> 
</body> 
</html> 
{{ end }}

2、定义业务逻辑

Go 复制代码
// DoAdd 处理用户添加请求,接收用户名和两个头像文件(face1 和 face2)
// 该函数从 POST 表单中获取用户名字段,并尝试获取两个上传的文件,
// 如果文件存在则分别保存到服务器的 ./static/upload 目录下,
// 最后返回一个 JSON 格式的成功响应,包含用户名和成功信息。
// 参数 ctx: Gin 上下文对象,包含请求和响应的方法。
func (c UserController) DoAdd(ctx *gin.Context) {
    // 从 POST 表单中获取 "username" 字段的值
    username := ctx.PostForm("username")

    // 尝试从表单中获取名为 "face1" 的上传文件
    // ctx.FormFile 返回文件头信息和错误,如果文件未上传则 err1 不为 nil
    face1, err1 := ctx.FormFile("face1")
    // 尝试从表单中获取名为 "face2" 的上传文件
    face2, err2 := ctx.FormFile("face2")

    // 处理第一个文件:如果 err1 为 nil,说明文件上传成功
    if err1 == nil {
        // 构建文件的保存路径:将原始文件名拼接到静态上传目录下
        // 注意:path.Join 会根据操作系统使用正确的路径分隔符
        dst1 := path.Join("./static/upload", face1.Filename)
        // 将上传的文件保存到指定路径
        // ctx.SaveUploadedFile 会创建文件并写入内容,如果目录不存在会返回错误
        // 生产环境中应检查 SaveUploadedFile 返回的错误并适当处理
        ctx.SaveUploadedFile(face1, dst1)
    }

    // 处理第二个文件:如果 err2 为 nil,说明文件上传成功
    if err2 == nil {
        dst2 := path.Join("./static/upload", face2.Filename)
        ctx.SaveUploadedFile(face2, dst2)
    }

    // 返回 JSON 格式的成功响应,状态码为 200 OK
    ctx.JSON(http.StatusOK, gin.H{
        "message":  "文件上传成功", // 提示信息
        "username": username,      // 回显用户名
    })
    // 下面这一行是注释掉的代码,原本打算返回纯文本响应,但已被 JSON 响应替代
    // ctx.String(200, username)
}

5.3 多文件上传--相同名字的多个文件

**参考:**https://gin-gonic.com/zh-cn/docs/examples/upload-file/multiple-file/

1、定义模板

需要在上传文件的 form 表单上面需要加入 enctype="multipart/form-data"

html 复制代码
<!-- 相当于给模板定义一个名字 define end 成对出现--> 
{{ define "admin/user/add.html" }} 
<!DOCTYPE html> 
<html lang="en"> 
<head> 
    <meta charset="UTF-8"> 
    <meta http-equiv="X-UA-Compatible" content="IE=edge"> 
    <meta name="viewport" content="width=device-width, initial-scale=1.0"> 
    <title>Document</title> 
</head> 
<body> 
    <form action="/admin/user/doAdd" method="post" enctype="multipart/form-data"> 
        用户名: <input type="text" name="username" placeholder="用户名"> <br> <br> 
        头 像 1:<input type="file" name="face[]"><br> <br> 
        头 像 2:<input type="file" name="face[]"><br> <br> 
        <input type="submit" value="提交"> 
    </form> 
</body> 
</html> 
{{ end }}

2、定义业务逻辑

Go 复制代码
// DoAdd 处理用户添加请求,支持批量上传多个头像文件(字段名为 "face[]")
// 该函数从 POST 表单中获取用户名,并使用 MultipartForm 解析整个表单,
// 获取所有名为 "face[]" 的上传文件,将它们逐一保存到服务器的静态目录下,
// 最后返回一个 JSON 响应,包含用户名和成功信息。
// 参数 ctx: Gin 上下文对象,包含请求和响应的方法。

func (c UserController) DoAdd(ctx *gin.Context) {
    // 从 POST 表单中获取 "username" 字段的值(普通文本字段)
    username := ctx.PostForm("username")

    // 解析 multipart 表单,获取整个表单对象
    // ctx.MultipartForm() 会返回 *multipart.Form 和错误
    // 注意:生产环境中应处理可能出现的错误,此处为简洁使用了 _ 忽略
    form, _ := ctx.MultipartForm()

    // 从表单中获取所有名为 "face[]" 的上传文件
    // form.File 是一个 map[string][]*multipart.FileHeader,key 为字段名
    // 如果字段不存在或没有文件,files 可能为 nil 或空切片
    files := form.File["face[]"]

    // 循环遍历每个上传的文件
    for _, file := range files {
        // 构建文件保存的目标路径:静态上传目录 + 原始文件名
        // path.Join 会根据操作系统使用正确的路径分隔符
        dst := path.Join("./static/upload", file.Filename)

        // 将上传的文件保存到指定路径
        // ctx.SaveUploadedFile 会创建文件并写入内容,如果目录不存在会返回错误
        // 生产环境中应检查并处理 SaveUploadedFile 返回的错误
        ctx.SaveUploadedFile(file, dst)
    }

    // 返回 JSON 格式的成功响应,状态码为 200 OK
    ctx.JSON(http.StatusOK, gin.H{
        "message":  "文件上传成功", // 提示信息
        "username": username,      // 回显用户名
    })
}

5.4 文件上传--按照日期存储

1、定义模板

需要在上传文件的 form 表单上面需要加入 enctype="multipart/form-data"

Go 复制代码
<!-- 相当于给模板定义一个名字 define end 成对出现--> 
{{ define "admin/user/add.html" }} 
<!DOCTYPE html> 
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body> 
<form action="/admin/user/doAdd" method="post" enctype="multipart/form-data">
    用户名:<input type="text" name="username" placeholder="用户名"> <br> <br>
    头 像: <input type="file" name="face"><br> <br>
    <input type="submit" value="提交">
</form> 
</body> 
</html>
{{ end }}

2、定义业务逻辑

Go 复制代码
// DoAdd 处理用户添加请求,接收用户名和单个头像文件
// 该函数执行完整的文件上传流程:
// 1. 获取上传的文件并进行类型验证(仅允许 .jpg, .png, .gif, .jpeg)
// 2. 根据当前日期创建存储目录(如 ./static/upload/20230312)
// 3. 使用当前时间戳生成唯一文件名,避免重名覆盖
// 4. 保存文件到指定路径,并返回 JSON 格式的成功响应
// 参数 ctx: Gin 上下文对象,包含请求和响应的方法
func (c UserController) DoAdd(ctx *gin.Context) {
    // 从 POST 表单中获取用户名(普通文本字段)
    username := ctx.PostForm("username")

    // 1、获取上传的文件(表单字段名为 "face")
    file, err1 := ctx.FormFile("face")
    if err1 == nil { // 只有文件存在时才处理,否则跳过文件上传部分
        // 2、获取文件后缀名(例如 .jpg, .png),用于类型验证
        extName := path.Ext(file.Filename)

        // 定义允许上传的文件扩展名白名单
        allowExtMap := map[string]bool{
                ".jpg":  true,
                ".png":  true,
                ".gif":  true,
                ".jpeg": true,
        }

        // 检查当前文件后缀是否在白名单中
        // ok 为 true 表示存在(允许),否则为 false
        if _, ok := allowExtMap[extName]; !ok {
                // 如果类型不合法,返回错误信息并终止处理
                ctx.String(200, "文件类型不合法")
                return
        }

        // 3、创建按日期分组的文件保存目录
        // models.GetDay() 应返回当前日期字符串,格式如 "20230312"(年月日)
        day := models.GetDay()
        dir := "./static/upload/" + day

        // 使用 os.MkdirAll 创建多级目录,如果目录已存在则不会报错
        // 权限 0666 表示目录可读写,但实际权限受 umask 影响
        if err := os.MkdirAll(dir, 0666); err != nil {
                // 记录目录创建失败的错误日志
                log.Error(err)
        }

        // 4、生成唯一的文件名,避免多用户同时上传时覆盖
        // models.GetUnix() 返回当前 Unix 时间戳(秒),作为文件名前缀
        fileUnixName := strconv.FormatInt(models.GetUnix(), 10)

        // 拼接完整的文件保存路径:目录 + 时间戳 + 原始扩展名
        // 例如:"./static/upload/20230312/1678601234.jpg"
        saveDir := path.Join(dir, fileUnixName+extName)

        // 将上传的文件保存到指定的路径
        // ctx.SaveUploadedFile 会创建文件并写入内容,如果保存失败应处理错误
        ctx.SaveUploadedFile(file, saveDir)
    }

    // 返回 JSON 格式的成功响应,包含提示信息和用户名
    ctx.JSON(http.StatusOK, gin.H{
            "message":  "文件上传成功",
            "username": username,
    })

    // 下面是被注释掉的纯文本响应,已被上面的 JSON 响应替代
    // ctx.String(200, username)
}

3、models/tools.go

Go 复制代码
package models

import (
        "crypto/md5"
        "fmt"
        "time"
        // 注意:代码中使用了 beego.Info(err),但未导入 beego 包
        // 实际使用时应导入 "github.com/astaxie/beego" 或改用标准 log
)

// UnixToDate 将整数型时间戳(秒)转换为格式化的日期时间字符串。
// 格式为 "2006-01-02 15:04:05"(Go 的时间格式化参考时间)。
// 参数 timestamp:以秒为单位的时间戳(int 类型,会自动转换为 int64)。
// 返回对应的日期时间字符串。
func UnixToDate(timestamp int) string {
        t := time.Unix(int64(timestamp), 0)
        return t.Format("2006-01-02 15:04:05")
}

// DateToUnix 将日期时间字符串转换为 Unix 时间戳(秒)。
// 字符串必须严格符合 "2006-01-02 15:04:05" 格式,否则返回 0 并记录错误。
// 参数 str:要转换的日期时间字符串。
// 返回对应的 Unix 时间戳(秒),若解析失败则返回 0。
// 注意:此函数使用了 beego.Info 记录错误,需确保 beego 包已导入。
func DateToUnix(str string) int64 {
        template := "2006-01-02 15:04:05"
        t, err := time.ParseInLocation(template, str, time.Local)
        if err != nil {
                beego.Info(err) // 需要导入 "github.com/astaxie/beego" 才能使用
                return 0
        }
        return t.Unix()
}

// GetUnix 返回当前时间的 Unix 时间戳(秒)。
func GetUnix() int64 {
        return time.Now().Unix()
}

// GetDate 返回当前时间的格式化字符串,格式为 "2006-01-02 15:04:05"。
func GetDate() string {
        template := "2006-01-02 15:04:05"
        return time.Now().Format(template)
}

// GetDay 返回当前日期的紧凑字符串,格式为 "20060102"(例如 20250312)。
func GetDay() string {
        template := "20060102"
        return time.Now().Format(template)
}

// Md5 计算输入字符串的 MD5 哈希值,并返回其十六进制表示(末尾带有换行符)。
// 注意:返回的字符串末尾包含一个换行符,如 "e10adc3949ba59abbe56e057f20f883e\n"。
// 参数 str:要计算 MD5 的原始字符串。
// 返回 MD5 哈希的十六进制字符串(带换行)。
func Md5(str string) string {
        data := []byte(str)
        return fmt.Sprintf("%x\n", md5.Sum(data))
}

// Hello 是一个示例函数,将输入字符串与 "world" 拼接后返回。
// 参数 in:输入字符串。
// 返回 out:拼接后的结果(in + "world")。
func Hello(in string) (out string) {
        out = in + "world"
        return
}

6.1.1 为什么需要 Cookie?

因为 HTTP 协议本身是无状态的,服务器无法通过协议记住两次请求是否来自同一个用户,导致网站无法识别用户身份、无法维持登录状态或记录用户的操作(如购物车内容)。Cookie 通过在用户浏览器中存储一小段文本数据,并在每次请求时自动携带给服务器,为 HTTP 协议赋予了"记忆能力",从而实现了用户会话跟踪、登录状态保持、个性化设置保存等关键功能,让网站能够提供连贯、个性化的交互体验。

cookie 是存储于访问者计算机的浏览器中,可以让我们用同一个浏览器访问同一个域名的时候共享数据。

它的具体工作流程和作用如下:

1、身份标识(记住谁是谁)

  • 当你第一次访问服务器时,服务器会在响应中附带一个 Cookie,里面可能包含一个唯一的 Session ID。

  • 浏览器会自动保存这个 Cookie。

  • 当你下一次(或刷新页面后)再次请求同一个网站的页面时,浏览器会自动把这个 Cookie 携带在请求头中发送给服务器。

  • 服务器通过读取 Cookie 中的 Session ID,就能识别出你是刚才那个用户。

2、状态维持(记住你做过什么)

  • 它可以在浏览器本地存储一些简单的状态数据。比如:

    • 登录状态:记录你已登录,下次访问无需重复输入密码。

    • 购物车数据:你把商品加入购物车,这些信息写入 Cookie,即使刷新页面或跳转到其他页面,商品依然在购物车中。

    • 用户偏好:如网页的字体大小、深色模式设置、浏览历史记录等。

  • 位置 :确实如第二句话所说,它存储在用户的浏览器中,而不是服务器上。

  • 大小限制:通常只有 4KB 左右,所以不能存太多东西。

  • 作用域 :它遵循同源策略(或更宽松的 Domain 设置),默认情况下只会在访问设置它的那个域名时才会被携带。

  • 生命周期:可以设置过期时间。如果不设置过期时间(会话级 Cookie),关掉浏览器它就自动消失了;如果设置了过期时间,它会一直存在硬盘里,直到过期。

在 Gin 框架中,Cookie 操作通过 *gin.Context 提供的 SetCookie() 方法进行设置(可指定名称、值、过期时间、路径、域名、安全标志如 HttpOnlySecure),通过 Cookie() 方法按名称获取对应的值,并通过将 MaxAge 设为负值来删除 Cookie。Gin 还支持设置 SameSite 模式以防范 CSRF 攻击,同时为了安全起见,生产环境应启用 SecureHttpOnly 标志,并可结合 Session 中间件或对 Cookie 值进行签名来防止篡改。

总结: Cookie 的作用就是通过在浏览器本地保存一小段文本信息,为原本无记忆能力的 HTTP 协议提供了"记忆力",使得服务器能够区分不同的用户,并维持用户与网站之间的交互状态(如登录状态、购物车内容等),从而提升用户体验。

  • 保持用户登录状态: 当用户在网站上登录成功后,服务器会生成一个唯一的身份标识(如 Session ID),并通过 Set-Cookie 响应头将其存储在浏览器的 Cookie 中。此后,浏览器每次向该网站发送请求时都会自动携带这个 Cookie,服务器通过解析 Cookie 中的标识即可识别用户身份,从而维持登录状态,用户无需在访问每个页面时重复输入账号密码,实现了无缝的登录体验。

  • **保存用户浏览的历史记录:**网站可以将用户最近浏览过的商品或内容信息(如商品 ID 列表)存储在浏览器的 Cookie 中。当用户在不同页面间切换或下次访问时,浏览器携带的 Cookie 会将这些历史数据传回服务器,网站据此展示"最近浏览"列表,帮助用户快速找回之前感兴趣的物品,提升浏览效率。

  • **猜你喜欢,智能推荐:**通过 Cookie 中携带的用户标识(如 Session ID 或用户 ID),网站能够将用户在一段时间内的浏览、点击、搜索等行为关联到同一个用户画像上。服务器端分析这些行为数据,构建兴趣模型,从而在用户访问时动态生成个性化的"猜你喜欢"推荐内容,提高用户粘性和转化率。

  • **电商网站的加入购物车:**当用户将商品加入购物车时,商品信息可以写入浏览器的 Cookie(或通过 Cookie 中的会话 ID 关联到服务器端的购物车数据)。即使未登录或在不同页面间跳转,浏览器携带的 Cookie 都能让服务器识别出当前用户的购物车内容,确保购物车中的商品始终一致,并在结算时准确提交。

1、设置和获取 Cookie

案例代码:

**官网源码地址:**https://gin-gonic.com/zh-cn/docs/examples/cookie/

Go 复制代码
package main

import (
    "fmt" // 导入 fmt 包,用于控制台输出

    "github.com/gin-gonic/gin" // 导入 Gin 框架核心包
)

func main() {
    // 创建一个默认配置的 Gin 引擎
    // gin.Default() 会附加 Logger 和 Recovery 两个中间件
    router := gin.Default()

    // 注册一个 GET 路由,路径为 "/cookie"
    // 当客户端通过 GET 方法访问该路径时,执行后面的匿名函数
    router.GET("/cookie", func(c *gin.Context) {
        // 1. 尝试从 HTTP 请求中读取名为 "gin_cookie" 的 Cookie
        // c.Cookie(name string) 方法返回两个值:
        //    - 找到的 Cookie 值(字符串)
        //    - 一个错误,如果 Cookie 不存在或解析失败,err 不为 nil
        cookie, err := c.Cookie("gin_cookie")

        // 2. 判断是否发生了错误(通常意味着 Cookie 不存在)
        if err != nil {
            // 如果 Cookie 不存在,将变量 cookie 的值设为 "NotSet"
            // 这个值仅用于本次打印,并不会写入浏览器
            cookie = "NotSet"

            // 3. 在浏览器中设置一个新的 Cookie
            // c.SetCookie(name, value, maxAge, path, domain, secure, httpOnly)
            // 参数说明:
            //   - "gin_cookie": Cookie 的名称
            //   - "test"      : Cookie 的值
            //   - 3600        : 有效时间(秒),3600秒 = 1小时。负数表示删除,0表示不设置 MaxAge
            //   - "/"         : 路径,"/" 表示整个网站都可用
            //   - "localhost" : 域名,只在访问 localhost 时发送此 Cookie
            //   - false       : secure 标志,true 表示仅通过 HTTPS 发送,false 表示 HTTP/HTTPS 均可
            //   - true        : httpOnly 标志,true 表示禁止 JavaScript 访问(如 document.cookie),提高安全性
            c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true)
        }

        // 4. 在控制台(终端)打印最终获取到的 Cookie 值
        // 如果是第一次访问,打印 "Cookie value: NotSet"
        // 如果之前已设置过 Cookie,打印实际的值 "test"
        fmt.Printf("Cookie value: %s \n", cookie)

        // 注意:这里没有使用 c.JSON 或 c.String 返回 HTTP 响应给客户端,
        // 所以浏览器访问该路由时会看到空白页面,但控制台会有输出。
    })

    // 启动 HTTP 服务
    // 如果不指定端口,默认监听 :8080
    router.Run()
}

SetCookie参数设置

Go 复制代码
c.SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool)
  • **name:**Cookie 的名称,用于标识该 Cookie,浏览器在后续请求中会通过该名称将其值传回服务器。

  • **value:**Cookie 的值,存储具体的状态信息(如用户标识、偏好设置),应避免存储敏感数据,且最好进行编码处理。

  • maxAge: Cookie 的有效期(秒)。正数表示持久化 Cookie,在指定秒数后过期;负数(通常设为 -1)表示立即删除该 Cookie;0 表示不设置 Max-Age 字段,此时 Cookie 成为会话级 Cookie,仅在当前浏览器会话期间有效(关闭浏览器即失效)。

  • path: Cookie 的作用路径,只有匹配该路径的请求才会携带此 Cookie。通常设为 "/" 表示对整个站点生效,也可设为子路径(如 /user)限制只在特定模块使用。

  • domain: Cookie 的可用域名,用于跨子域共享。例如设为 ".``example.com``" 则所有 example.com 的子域都可读取。默认为当前请求的域名,但不可跨主域设置。

  • secure: 安全标志。若为 true,则 Cookie 仅在 HTTPS 协议下传输,防止在不安全的网络中泄露,生产环境建议启用。

  • httpOnly: HTTP-only 标志。若为 true,则禁止 JavaScript 通过 document.cookie 访问该 Cookie,有效缓解跨站脚本(XSS)攻击,建议对敏感 Cookie(如 Session ID)开启此选项。

该方法底层调用 Go 标准库的 http.SetCookie,最终通过 Set-Cookie 响应头告知浏览器存储 Cookie。正确使用这些参数可以平衡功能性、安全性和作用域控制。

2、获取 Cookie

Go 复制代码
cookie, err := c.Cookie("name")

c.Cookie("name") 方法会返回两个值:第一个是字符串类型的 Cookie 值,第二个是 error 类型的错误信息。如果 Cookie 存在且解析成功,errnilcookie 变量存储对应的值;如果 Cookie 不存在或发生其他错误,err 不为 nil,此时 cookie 为空字符串,开发者通常需要根据 err 判断并进行相应处理(例如设置默认值或创建新 Cookie)。

实际案例:

Go 复制代码
package main

// 导入所需的包
import (
    "gin_demo/models"               // 导入自定义的 models 包,其中包含 UnixToDate 函数
    "html/template"                  // 用于自定义模板函数
    "github.com/gin-gonic/gin"       // Gin Web 框架
)

func main() {
    // 创建一个默认配置的 Gin 引擎,默认附带 Logger 和 Recovery 中间件
    r := gin.Default()

    // 设置自定义模板函数映射,将 models.UnixToDate 函数注册为模板函数 "unixToDate"
    // 这样在渲染模板时就可以使用 {{ unixToDate .timestamp }} 来格式化时间戳
    r.SetFuncMap(template.FuncMap{
            "unixToDate": models.UnixToDate,
    })

    // 定义根路径 "/" 的 GET 请求处理函数
    r.GET("/", func(c *gin.Context) {
            // 设置一个名为 "usrename"(拼写错误,可能应为 "username")的 Cookie
            // 参数:名称、值、有效时间(秒)、路径、域名、是否仅HTTPS、是否仅HTTP
            c.SetCookie("usrename", "张三", 3600, "/", "localhost", false, true)
            // 返回纯文本响应 "首页"
            c.String(200, "首页")
    })

    // 定义路径 "/user" 的 GET 请求处理函数
    r.GET("/user", func(c *gin.Context) {
            // 从请求中获取名为 "usrename" 的 Cookie 值
            username, _ := c.Cookie("usrename")
            // 返回纯文本响应,内容为 "用户-" 加上获取到的用户名
            c.String(200, "用户-"+username)
    })

    // 启动 HTTP 服务,监听在 8080 端口
    r.Run(":8080")
}

在 Web3.0 场景中,经常需要用户在多个子域名(如 DApp 市场、NFT 交易平台、跨链桥)之间保持会话状态,比如共享连接的钱包地址、网络偏好或签名信息。通过设置 domain 为公共父域(如 .``web3demo.com),可以在所有子域间共享 Cookie,实现统一身份识别。

下面提供一个完整的 Gin 框架案例,演示两个二级域名(user.web3demo.comnft.web3demo.com)之间通过共享 Cookie 传递 Web3 用户信息(钱包地址和首选网络)。

1、完整案例代码

Go 复制代码
package main

import (
        "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()

    // 模拟用户在 user.web3demo.com 上连接钱包,设置偏好
    r.GET("/set", func(c *gin.Context) {
        // 假设用户连接了钱包,地址为 0x742d35Cc6634C0532925a3b844BcC454e3f1e1f7
        walletAddress := "0x742d35Cc6634C0532925a3b844BcC454e3f1e1f7"
        network := "Ethereum Mainnet"

        // 将钱包地址和网络存入 Cookie,共享给所有 .web3demo.com 子域
        // domain: ".web3demo.com" 表示所有二级子域均可读取
        // maxAge: 3600 秒(1小时),生产环境可延长
        c.SetCookie("wallet", walletAddress, 3600, "/", ".web3demo.com", false, true)
        c.SetCookie("network", network, 3600, "/", ".web3demo.com", false, true)

        c.String(200, "✅ 钱包信息已设置,请访问 nft.web3demo.com/get 查看共享数据")
    })

    // 在 nft.web3demo.com 上读取共享的 Web3 用户信息
    r.GET("/get", func(c *gin.Context) {
        wallet, err1 := c.Cookie("wallet")
        network, err2 := c.Cookie("network")

        if err1 != nil || err2 != nil {
                c.String(200, "❌ 未检测到钱包信息,请先在 user.web3demo.com/set 连接钱包")
                return
        }

        c.String(200, "🔗 共享的 Web3 信息:\n钱包地址:%s\n网络:%s", wallet, network)
    })

    // 启动服务(需以管理员/root 运行监听 80 端口,或改用 8080 配合反向代理)
    r.Run(":8080")
}

2、测试步骤(本地环境)

(1)修改 hosts 文件

建议使用命令行的方式来更改

  1. C:\Windows\System32\drivers\etc\hosts(Windows)或 /etc/hosts(Linux/Mac)中添加:

    Go 复制代码
    127.0.0.1   user.web3demo.com
    127.0.0.1   nft.web3demo.com

使用命令行的方式更改此文件:

以管理员身份运行命令提示符

  • Win + S,输入 cmd,右键点击"命令提示符" → "以管理员身份运行"。
  1. 用记事本打开 hosts 文件 在命令行中输入以下命令并回车:
Go 复制代码
notepad C:\Windows\System32\drivers\etc\hosts
  1. 此时会弹出记事本窗口,显示 hosts 文件内容。

  2. 添加你的域名解析 在文件末尾另起一行,输入:

Go 复制代码
127.0.0.1 user.web3demo.com
127.0.0.1 nft.web3demo.com
  1. 保存并关闭 按 Ctrl + S 保存,然后关闭记事本。如果提示"没有权限",说明没有以管理员身份运行记事本,请重新执行第 1 步。

(2)运行程序

  1. 以管理员/root 权限运行(因为监听 80 端口),或改为 r.Run(":8080") 后通过 nginx 反向代理。执行如下代码:

(3) 设置 Cookie

  1. 浏览器访问 http://user.web3demo.com:8080/set,页面提示设置成功。

(4)验证共享

  1. 同一浏览器 中访问 http://nft.web3demo.``com:8080``/get,应看到显示的钱包地址和网络信息。

(5)查看 Cookie 详情

  1. 打开浏览器开发者工具 → Application → Cookies,可以看到 walletnetwork 的 Domain 列为 .web3demo.com,表明它们对所有子域可见。

3、核心机制说明

  • domain 参数 :设置为 ".web3demo.com"(开头的点表示匹配所有子域),浏览器在向任何 *.web3demo.com 发送请求时都会携带该 Cookie。

  • path 参数"/" 表示网站所有路径下均可访问。

  • httpOnly :设为 true 可防止 XSS 攻击获取 Cookie(生产环境必选)。

  • secure :生产环境中应设为 true(仅 HTTPS 发送),本地测试用 false

4、Web3.0 业务扩展

在实际 Web3 DApp 中,Cookie 存储的可以是:

  • 用户连接的钱包地址(用于识别身份)

  • 用户选择的 RPC 网络(如 Ethereum、Polygon、BSC)

  • 用户签署的一次性登录凭证(Siwe,Sign-In with Ethereum)

  • 用户偏好的 Gas 价格策略

通过子域名共享 Cookie,可以在多个去中心化应用之间实现无缝体验,例如:

  • id.web3demo.com 上连接钱包 → 在 swap.web3demo.com 上直接交易(无需重复连接)。

  • profile.web3demo.com 设置头像 → 在 dao.web3demo.com 投票时自动显示。

这样既保留了 Web2 的便捷性,又符合 Web3 去中心化身份的理念。

6.2 Gin 中的 Session

6.2.1 Session 简单介绍

在 Gin 框架中,Session(会话)是一种在服务器端存储用户特定数据 (如登录状态、用户信息)的机制,它通过中间件(如 gin-contrib/sessions)实现:当用户首次访问时,服务器会生成一个唯一的 Session ID,并通过 Cookie 发送给客户端存储;后续请求中,浏览器自动携带该 Cookie,中间件解析出 Session ID 后从服务器端存储(支持内存、Redis、数据库等)中加载对应的数据,从而让无状态的 HTTP 协议能够记住用户,实现登录状态保持、购物车等跨请求的用户状态管理。

6.2.2 Session 的工作流程

当客户端浏览器第一次访问服务器并发送请求时,服务器端会创建一个 session 对象,生成一个类似于 key,value 的键值对,然后将 value 保存到服务器 将 key(cookie)返回到浏览器(客户)端。浏览器下次访问时会携带 key(cookie),找到对应的 session(value)。

6.2.3 Gin 中使用 Session

Gin 官方没有给我们提供 Session 相关的文档,这个时候我们可以使用第三方的 Session 中间件来实现 https://github.com/gin-contrib/sessions

gin-contrib/sessions 中间件支持的存储引擎:

  • cookie

  • memstore

  • redis

  • memcached

  • mongodb

基于 Cookie 存储 Session 是指将会话数据(如用户 ID、登录状态)直接加密后存储在客户端的 Cookie 中,服务器端不再保存任何会话信息;每次请求时浏览器自动携带该 Cookie,服务器解密并验证数据完整性后即可识别用户身份,从而实现无状态的会话管理。这种方式的优势在于服务器无需内存或磁盘存储,易于横向扩展,但受限于 Cookie 大小(通常 4KB)和安全性(必须对内容加密或签名防止篡改和窃取),因此仅适合存储非敏感的轻量级数据,且需配合 HTTPS 使用。

1、安装 session 包

Go 复制代码
go get github.com/gin-contrib/sessions

这条命令 go get ``github.com/gin-contrib/sessions 会将 session 包下载到你电脑上的 Go 模块缓存中(全局共享),同时自动更新当前项目的 go.mod 文件,把该包作为项目的依赖记录下来。也就是说,它既安装到了电脑的全局缓存中,也安装到了你当前的项目中------后续编译该项目时,Go 会使用缓存里的这个包。如果你的项目尚未初始化 Go module,建议先执行 go mod init 模块名 创建模块,再用此命令添加依赖。

2、基本的 session 用法

Go 复制代码
package main 
import ( 
    "github.com/gin-contrib/sessions" 
    "github.com/gin-contrib/sessions/cookie" 
    "github.com/gin-gonic/gin" 
)

func main() { 
    r := gin.Default() 
    // 创建基于 cookie 的存储引擎,secret11111 参数是用于加密的密钥 
    store := cookie.NewStore([]byte("secret11111")) 
    
    // 设置 session 中间件,参数 mysession,指的是 session 的名字,也是 cookie 的名字 
    // store 是前面创建的存储引擎,我们可以替换成其他存储引擎 
    r.Use(sessions.Sessions("mysession", store)) 
    
    r.GET("/", func(c *gin.Context) { 
        //初始化 session 对象 
        session := sessions.Default(c) 
        //设置过期时间 
        session.Options(sessions.Options{ 
        MaxAge: 3600 * 6, // 6hrs 
        })
        
        //设置 Session 
        session.Set("username", "张三") 
        session.Save() 
        c.JSON(200, gin.H{"msg": session.Get("username")})
    }) 
    
    r.GET("/user", func(c *gin.Context) { 
        // 初始化 session 对象 
        session := sessions.Default(c) 
        // 通过 session.Get 读取 session 值 
        username := session.Get("username") 
        c.JSON(200, gin.H{"username": username}) 
    })
    
    r.Run(":8000")
    
}

6.2.5 基于 Redis 存储 Session

基于 Redis 存储 Session 是指将用户的会话数据存放在 Redis 内存数据库中,而不是默认的服务器内存中。这样做的好处是:当 Web 应用重启或部署多台服务器(分布式集群)时,Session 数据不会丢失或分散,所有服务器实例都可以通过访问同一个 Redis 来读取和写入 Session,从而实现会话的持久化与共享,同时 Redis 的高性能也保证了读写速度。在 Gin 框架中,只需使用 gin-contrib/sessions 中间件并配置 sessions.RedisStore 即可轻松实现。

如果我们想将 session 数据保存到 redis 中,只要将 session 的存储引擎改成 redis 即可。

**1、使用 redis 作为存储引擎的例子:**首先安装 redis 存储引擎的包

Go 复制代码
go get github.com/gin-contrib/sessions/redis

例子:

Go 复制代码
package main 
import ( 
    "github.com/gin-contrib/sessions" 
    "github.com/gin-contrib/sessions/redis" 
    "github.com/gin-gonic/gin" 
)

func main() { 
    r := gin.Default() 
    // 初始化基于 redis 的存储引擎 
    // 参数说明: 
    // 第 1 个参数 - redis 最大的空闲连接数 
    // 第 2 个参数 - 数通信协议 tcp 或者 udp 
    // 第 3 个参数 - redis 地址, 格式,host:port 
    // 第 4 个参数 - redis 密码 
    // 第 5 个参数 - session 加密密钥 
    store, _ := redis.NewStore(10, "tcp", "localhost:6379", "", []byte("secret"))
    
    r.Use(sessions.Sessions("mysession", store)) 
    r.GET("/", func(c *gin.Context) { 
        session := sessions.Default(c) 
        session.Set("username", "李四") 
        session.Save() 
        c.JSON(200, gin.H{"username": session.Get("username")}) 
    }) 
    r.GET("/user", func(c *gin.Context) { 
        // 初始化 session 对象 
        session := sessions.Default(c) 
        // 通过 session.Get 读取 session 值 
        username := session.Get("username") 
        c.JSON(200, gin.H{"username": username}) 
    })
    
    r.Run(":8000")
    
}

第七章:Gin 中使用 GORM 操作 PostgreSQL 数据库

7.1 GORM 详情

7.1.1 GORM 基础

GORM(Go Object Relational Mapping) 是 Go 语言中最流行的 ORM(对象关系映射)库,它将数据库表映射为 Go 语言的结构体,让开发者能够使用面向对象的方式操作数据库,而无需编写大量原生 SQL。它支持 MySQL、PostgreSQL、SQLite 等多种数据库,提供了自动迁移、关联查询、钩子方法、事务、预加载等丰富功能,遵循约定优于配置的原则,极大地简化了数据持久化层的开发,提高了代码的可维护性和可读性。

7.1.2 Gorm 特性

(1)基础能力

  • 全功能 ORM:GORM 不是简单的 SQL 构建器,而是一个完整的对象关系映射工具,能将数据库表完整地映射为 Go 结构体。

  • Auto Migration自动迁移特性。可以根据你定义的 Go 结构体,自动在数据库中创建、更新表结构(如添加字段、索引)。

  • 注意:生产环境建议谨慎使用或手动控制,以防数据丢失。

(2)数据关系处理

  • 关联:完美支持数据库表之间的各种关系映射:

    • Has One (一对一:用户-详情)

    • Has Many (一对多:文章-评论)

    • Belongs To (属于:文章属于某个用户)

    • Many To Many (多对多:学生-课程)

    • 多态/单表 继承:更高级的关联方式,比如一个表可以关联多个不同类型的模型。

  • 预加载 :解决 N+1 查询问题。例如查询文章时,通过 PreloadJoins 一次性把作者和评论也查出来,避免循环查询数据库。

(3) 增删改查 操作

  • 钩子方法 :在 CRUD 操作前后插入自定义逻辑。比如在 Create 之前自动生成 UUID,或在 Update 之后记录日志。

  • 灵活的查询

    • 不仅能用结构体查询,还能用 Map 作为条件。

    • 支持 FindInBatches(分批查询处理大数据),SQL 表达式(直接在查询条件里写 SQL,如 age > ?)。

    • 子查询:可以在查询中嵌套子查询。

  • 批量插入:一次性高效插入大量数据。

(4)高级数据库特性

  • 事务

    • 支持嵌套事务保存点,可以在长事务中回滚到特定点,而不是全部回滚。

    • 保证数据一致性。

  • SQL 构建器 :当 ORM 方法无法满足复杂需求时,可以直接写原生 SQL,同时支持 Upsert(存在则更新,否则插入)、数据库锁(悲观锁/乐观锁)。

  • 索引/约束:支持通过标签在结构体中定义索引和约束。

(5)性能与优化

  • 预编译模式:预编译 SQL 语句,提高执行效率。

  • DryRun 模式试运行,只生成 SQL 而不真正执行,非常便于调试。

  • Context 支持:可以传递超时控制、链路追踪信息等。

(6)可扩展性

  • 自定义 Logger:可以替换 GORM 的默认日志,接入自己的日志系统。

  • 插件 API

    • Database Resolver :实现读写分离(主库写,从库读)和多数据库、多数据源管理。

    • 可集成 Prometheus 等监控插件。

总结:GORM 不仅封装了基础的数据库操作,还通过预加载、事务、读写分离等特性,让开发者能在 Go 语言中高效、优雅地处理复杂的数据库业务逻辑。

官方文档https://gorm.io/zh_CN/docs/index.html

7.1.3 Gin 中使用 GORM

(1)安装

在使用 go mod 管理项目时,如果你已经通过 go mod init 初始化了模块,只需在代码中直接导入所需的包(如 import "gorm.io/gorm"),然后运行 go mod tidy,Go 工具链会自动分析代码、下载依赖并更新 go.mod 文件,效果等同于执行了 go get。因此,"忽略此步骤"意味着在 go mod 环境下可以省去手动 go get 的操作,由模块管理自动完成。不过,如果你希望立即下载并查看依赖,或者指定特定版本,执行这两条命令也是完全可行的。

Go 复制代码
# mysql数据库版本
go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql

# postgre数据库版本
go get -u gorm.io/gorm
go get -u gorm.io/driver/postgres

2、Gin 中使用 Gorm 连接数据库

在 models 下面新建 core.go ,建立数据库链接

Go 复制代码
// Package main 演示使用 GORM 连接 PostgreSQL 并在指定模式下操作区块链数据模型。
package main

import (
    "fmt"  // 用于格式化输出
    "log"  // 用于记录日志和 fatal 错误
    "time" // 用于处理时间类型

    "gorm.io/driver/postgres" // GORM 的 PostgreSQL 驱动,用于连接 PostgreSQL 数据库
    "gorm.io/gorm"            // GORM 核心库,提供 ORM 功能
)

// Block 对应区块链中的一个区块,映射到数据库中的 blockchain_data.blocks 表。
// 通过内嵌 gorm.Model 自动获得 ID、创建时间、更新时间、软删除字段。
type Block struct {
    gorm.Model
    Height    uint64    `gorm:"uniqueIndex;comment:区块高度"` // 区块高度,设置唯一索引
    Hash      string    `gorm:"size:66;comment:区块哈希"`     // 区块哈希,通常为0x开头的66字符
    Timestamp time.Time `gorm:"comment:出块时间"`             // 出块时间
    TxCount   int       `gorm:"comment:交易数量"`             // 包含的交易数量
}

// TableName 是 GORM 的钩子方法,用于指定 Block 模型对应的数据库表名。
// 返回 "blockchain_data.blocks" 表示显式指定 PostgreSQL 模式为 blockchain_data,
// 表名为 blocks。这样所有针对 Block 的数据库操作都会自动使用这个完整的表名。
func (Block) TableName() string {
    return "blockchain_data.blocks"
}

// main 函数是程序的入口点,演示了以下步骤:
// 1. 配置 PostgreSQL 连接字符串
// 2. 连接数据库
// 3. 自动迁移(创建表)
// 4. 插入一条模拟的区块数据
// 5. 查询刚插入的区块数据
// 6. (可选)演示使用 Table 方法访问其他模式中的表
func main() {
    // 1. 配置 PostgreSQL 连接参数(DSN)
    // 注意:这里没有在 DSN 中设置 search_path,而是通过 TableName 指定模式
    // DSN 格式说明:
    //   host: 数据库主机地址
    //   user: 数据库用户名
    //   password: 数据库密码
    //   dbname: 要连接的数据库名称
    //   port: PostgreSQL 端口(默认 5432)
    //   sslmode: 是否使用 SSL(disable 表示禁用)
    //   TimeZone: 会话时区
    dsn := "host=localhost user=postgres password=123456 dbname=postgres port=5432 sslmode=disable TimeZone=Asia/Shanghai"

    // 2. 连接数据库
    // gorm.Open 根据驱动和 DSN 建立数据库连接,返回 *gorm.DB 实例。
    // 如果连接失败(如数据库服务未启动、认证失败等),会返回错误。
    db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
    if err != nil {
        log.Fatal("数据库连接失败:", err)
    }
    fmt.Println("数据库连接成功!")

    // 3. 自动迁移:根据 Block 模型在 blockchain_data 模式下创建或更新 blocks 表
    // AutoMigrate 会对比模型结构体与数据库表,自动添加缺失的列、索引等。
    // 注意:GORM 不会自动创建模式(schema),请确保数据库中已存在 blockchain_data 模式,
    // 或在迁移前执行原生 SQL 创建模式,例如:db.Exec("CREATE SCHEMA IF NOT EXISTS blockchain_data;")

    db.Exec("CREATE SCHEMA IF NOT EXISTS blockchain_data;")

    err = db.AutoMigrate(&Block{})
    if err != nil {
        log.Fatal("自动迁移失败:", err)
    }
    fmt.Println("表 blockchain_data.blocks 已创建或已存在")

    db.Exec("truncate table blockchain_data.blocks;")  // 清空表中的数据,主要是如果不清空多次调试的时候会出现主键冲突的问题

    // 4. 插入一条模拟的区块数据(例如从区块链节点解析而来)
    // 创建一个 Block 实例并调用 db.Create 将其插入数据库。
    // Create 会填充模型的主键(ID)和 gorm.Model 中的时间字段。
    block := Block{
        Height:    12345678,
        Hash:      "0x742d35Cc6634C0532925a3b844BcC454e3f1e1f7", // 示例哈希
        Timestamp: time.Now(),
        TxCount:   150,
    }
    result := db.Create(&block)
    if result.Error != nil {
        log.Fatal("插入区块失败:", result.Error)
    }
    fmt.Printf("新区块记录插入成功,ID: %d, 高度: %d\n", block.ID, block.Height)

    // 5. 查询刚插入的区块(按高度查询)
    // db.Where 构建查询条件,First 返回第一条匹配的记录并填充到 queriedBlock 中。
    var queriedBlock Block
    db.Where("height = ?", 12345678).First(&queriedBlock)
    fmt.Printf("查询到的区块: 高度=%d, 哈希=%s, 交易数=%d, 时间=%s\n",
        queriedBlock.Height, queriedBlock.Hash, queriedBlock.TxCount, queriedBlock.Timestamp)

    // 6. 演示使用 Table 方法临时指定其他模式或表(可选)
    // 有时候需要操作不同模式下的表,可以使用 db.Table("模式名.表名") 临时切换。
    // 这里以查询 public 模式下的 blocks_backup 表为例(假设该表存在)。
    type BlockBackup struct {
        Height uint64
        Hash   string
    }

    // 创建public.blocks_backup表并插入测试数据,以便演示查询
    // 使用原生SQL创建表
    db.Exec(`CREATE TABLE IF NOT EXISTS public.blocks_backup (
        height BIGINT,
        hash TEXT
    )`)

    // 插入一条数据(如果不存在则插入,避免重复)
    db.Exec(`INSERT INTO public.blocks_backup (height, hash) VALUES (?, ?) ON CONFLICT DO NOTHING`, 12345678, "0xbackup")

    var backup BlockBackup
    // Scan 将查询结果映射到结构体中,不要求模型预先定义(比 Find 更灵活)
    db.Table("public.blocks_backup").Where("height = ?", 12345678).Scan(&backup)
    fmt.Printf("从 public.blocks_backup 查询到: 高度=%d, 哈希=%s\n", backup.Height, backup.Hash)
}

在 PostgreSQL 中,数据库下面确实可以包含多个模式(Schema)来组织数据表。使用 GORM 连接时,你有两种方式指定要操作的模式:

(1)在连接字符串中设置默认模式

在 DSN 中添加 search_path 参数,这样所有未显式指定模式的表操作都会默认使用该模式:

Go 复制代码
dsn := "host=localhost user=postgres password=yourpassword dbname=testdb port=5432 sslmode=disable TimeZone=Asia/Shanghai search_path=your_schema"
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})

这种方式适合整个应用主要操作同一个模式的情况。

(2)在代码中动态指定模式

如果你需要在多个模式间切换,可以在查询时通过 Table 方法显式指定模式名:

Go 复制代码
// 使用 "模式名.表名" 的格式操作特定模式的表
db.Table("your_schema.products").Find(&products)

// 或者在模型定义时指定表名
func (Product) TableName() string {
    return "your_schema.products"
}

注意事项:

  • 如果两种方式都未指定,PostgreSQL 默认使用 public 模式

  • 需要确保连接数据库的用户有权限访问指定的模式

  • 在多租户应用中,可以通过动态切换模式实现数据隔离

根据你的业务场景选择合适的方式即可。

(3)定义操作数据库的模型

Gorm **模型定义:**https://gorm.io/zh_CN/docs/models.html

虽然在 gorm 中可以指定字段的类型以及自动生成数据表,但是在实际的项目开发中,我们是先设计数据库表,然后去实现编码的。

在实际项目中定义数据库模型注意以下几点:

  • **结构体的名称必须首字母大写 ,并和数据库表名称对应。**例如:表名称为 user 结构体名称定义成 User,表名称为 article_cate 结构体名称定义成 ArticleCate 。

  • **结构体中的字段名称首字母必须大写,并和数据库表中的字段一一对应。**例如:下面结构体中的 Id 和数据库中的 id 对应,Username 和数据库中的 username 对应,Age 和数据库中的 age 对应,Email 和数据库中的 email 对应,AddTime 和数据库中的 add_time 字段对应。

  • 默认情况表名是结构体名称的复数形式。如果我们的结构体名称定义成 User,表示这个模型默认操作的是 users 表。

  • 我们可以使用结构体中的自定义方法 TableName 改变结构体的默认表名称,如下:

Go 复制代码
// User 结构体默认操作的表改为 user 表 
func (User) TableName() string { 
    return "user" 
}

定义 user 模型:

Go 复制代码
package models 

type User struct { // 默认表名是 `users` 
    Id int 
    Username string 
    Age int 
    Email string 
    AddTime int 
}

func (User) TableName() string { 
    return "user" 
}

关于更多模型定义的方法参考:https://gorm.io/zh_CN/docs/conventions.html

7.1.4 GORM CURD

GORM 提供了简洁而强大的 API 实现数据库的 CRUD 操作:创建(Create)用于插入记录,查询(First、Find、Where 等)支持条件、排序、分页和预加载,更新(Save、Update、Updates)可灵活修改单条或多条记录的字段,删除(Delete)支持物理删除和软删除(通过内嵌 gorm.Model),此外还内置事务、关联操作、钩子方法和原生 SQL 支持,让开发者能够高效、安全地处理各种数据持久化需求。

(1)增加(C)

增加成功后会返回刚才增加的记录

Go 复制代码
// Package main 演示使用 GORM 连接 PostgreSQL 并创建钱包记录
package main

import (
    "net/http"
    "time"

    "github.com/gin-gonic/gin"
    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)

// Wallet 对应数据库中的 wallets 表,存储用户钱包信息
type Wallet struct {
    ID        uint      `gorm:"primarykey"`                                // 主键 ID
    Address   string    `gorm:"size:42;uniqueIndex;comment:以太坊钱包地址(0x开头)"` // 钱包地址,唯一索引
    Nickname  string    `gorm:"size:50;comment:用户昵称"`                      // 用户昵称
    CreatedAt time.Time `gorm:"comment:创建时间"`                              // 记录创建时间(GORM 自动管理)
}

// 全局数据库连接变量
var DB *gorm.DB

// initDB 初始化数据库连接并执行自动迁移
func initDB() {
    // 使用用户提供的 PostgreSQL 连接字符串
    dsn := "host=localhost user=postgres password=123456 dbname=postgres port=5432 sslmode=disable TimeZone=Asia/Shanghai"

    var err error
    DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
    if err != nil {
        panic("数据库连接失败: " + err.Error())
    }

    // 自动迁移:根据 Wallet 结构体创建或更新表
    // 注意:GORM 会自动使用 PostgreSQL 的 public 模式,如需指定模式可修改 TableName 方法
    if err := DB.AutoMigrate(&Wallet{}); err != nil {
        panic("自动迁移失败: " + err.Error())
    }
}

// WalletController 处理钱包相关的 HTTP 请求
type WalletController struct{}

// CreateWallet 处理 POST /wallets 请求,用于创建新的钱包记录
// 请求 JSON 格式示例:{"address": "0x742d35Cc6634C0532925a3b844BcC454e3f1e1f7", "nickname": "alice"}
func (wc WalletController) CreateWallet(c *gin.Context) {
    // 1. 定义请求数据结构体(使用匿名结构体便于解析和验证)
    var req struct {
        Address  string `json:"address" binding:"required,len=42"` // 必须提供,长度42(0x+40字符)
        Nickname string `json:"nickname" binding:"required,max=50"`
    }

    // 2. 绑定 JSON 请求体到 req,并自动验证
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误: " + err.Error()})
        return
    }

    // 3. 创建 Wallet 对象(准备插入)
    wallet := Wallet{
        Address:  req.Address,
        Nickname: req.Nickname,
        // CreatedAt 由 GORM 自动填充,无需手动赋值
    }

    // 4. 执行插入操作
    result := DB.Create(&wallet)

    // 5. 检查插入结果
    if result.Error != nil {
        // 如果违反唯一索引(地址重复),返回冲突错误
        c.JSON(http.StatusConflict, gin.H{"error": "钱包地址已存在"})
        return
    }

    // 6. 返回成功响应,包含新创建的记录 ID
    c.JSON(http.StatusOK, gin.H{
        "message": "钱包创建成功",
        "id":      wallet.ID,
    })
}

func main() {
    // 初始化数据库连接
    initDB()

    // 创建 Gin 引擎,使用默认中间件(日志和恢复)
    r := gin.Default()

    // 实例化控制器
    wc := WalletController{}

    // 注册路由
    r.POST("/wallets", wc.CreateWallet)

    // 启动 HTTP 服务,监听 8080 端口
    r.Run(":8080")
}

更多增加语句:https://gorm.io/zh_CN/docs/create.html

(2)查找(R)

以下代码在Web3.0 钱包场景下,演示了使用 GORM 实现按条件查找 (如按地址精确查询、按昵称模糊查询)和全部查找两种方式。代码基于 PostgreSQL 数据库,并提供了完整的 Gin 路由。

Go 复制代码
// Package main 演示 GORM 的按条件查询和全部查询操作
package main

import (
        "net/http"
        "time"

        "github.com/gin-gonic/gin"
        "gorm.io/driver/postgres"
        "gorm.io/gorm"
)

// Wallet 对应数据库中的 wallets 表
type Wallet struct {
        ID        uint      `gorm:"primarykey"`
        Address   string    `gorm:"size:42;uniqueIndex;comment:以太坊钱包地址"`
        Nickname  string    `gorm:"size:50;comment:用户昵称"`
        CreatedAt time.Time `gorm:"comment:创建时间"`
}

// 全局数据库连接
var DB *gorm.DB

// initDB 初始化 PostgreSQL 连接
func initDB() {
        dsn := "host=localhost user=postgres password=123456 dbname=postgres port=5432 sslmode=disable TimeZone=Asia/Shanghai"
        var err error
        DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
        if err != nil {
                panic("数据库连接失败: " + err.Error())
        }
        // 自动迁移(仅用于演示,生产环境建议手动控制)
        DB.AutoMigrate(&Wallet{})
}

// WalletController 处理钱包请求
type WalletController struct{}

// GetAllWallets 全部查找:返回所有钱包,按创建时间倒序
func (wc WalletController) GetAllWallets(c *gin.Context) {
        var wallets []Wallet
        if err := DB.Order("created_at desc").Find(&wallets).Error; err != nil {
                c.JSON(http.StatusInternalServerError, gin.H{"error": "数据库错误"})
                return
        }
        c.JSON(http.StatusOK, wallets)
}

// GetWalletByAddress 条件查找:根据地址精确查询(唯一)
func (wc WalletController) GetWalletByAddress(c *gin.Context) {
        address := c.Query("address")
        if address == "" {
                c.JSON(http.StatusBadRequest, gin.H{"error": "缺少地址参数"})
                return
        }
        var wallet Wallet
        if err := DB.Where("address = ?", address).First(&wallet).Error; err != nil {
                if err == gorm.ErrRecordNotFound {
                        c.JSON(http.StatusNotFound, gin.H{"error": "钱包不存在"})
                } else {
                        c.JSON(http.StatusInternalServerError, gin.H{"error": "数据库错误"})
                }
                return
        }
        c.JSON(http.StatusOK, wallet)
}

// SearchWalletsByNickname 条件查找:根据昵称模糊查询(可返回多条)
func (wc WalletController) SearchWalletsByNickname(c *gin.Context) {
        nickname := c.Query("nickname")
        if nickname == "" {
                c.JSON(http.StatusBadRequest, gin.H{"error": "缺少昵称参数"})
                return
        }
        var wallets []Wallet
        // 使用 LIKE 进行模糊查询(PostgreSQL 语法)
        if err := DB.Where("nickname LIKE ?", "%"+nickname+"%").Find(&wallets).Error; err != nil {
                c.JSON(http.StatusInternalServerError, gin.H{"error": "数据库错误"})
                return
        }
        c.JSON(http.StatusOK, wallets)
}

func main() {
        initDB()
        r := gin.Default()
        wc := WalletController{}

        // 全部查找
        r.GET("/wallets/all", wc.GetAllWallets)
        // 条件查找:按地址精确查询
        r.GET("/wallets", wc.GetWalletByAddress)
        // 条件查找:按昵称模糊查询
        r.GET("/wallets/search", wc.SearchWalletsByNickname)

        r.Run(":8080")
}

关键 GORM 查询方法解析

方法 说明 对应接口
Find(&wallets) 查询所有记录,无附加条件即返回全表数据。 GET /wallets/all
Where("address = ?", addr).First(&wallet) 带条件查询,First 返回第一条匹配记录(适用于唯一字段)。 GET /wallets?address=xxx
Where("nickname LIKE ?", "%"+name+"%").Find(&wallets) 模糊查询,Find 返回所有匹配记录。 GET /wallets/search?nickname=yyy

测试示例:

全部查找(假设已插入若干条数据):

Go 复制代码
curl http://localhost:8080/wallets/all

按地址精确查询

Go 复制代码
curl "http://localhost:8080/wallets?address=0x742d35Cc6634C0532925a3b844BcC454e3f1e1f7"

按昵称模糊查询(查询昵称中包含 "alice" 的所有钱包):

Go 复制代码
curl "http://localhost:8080/wallets/search?nickname=alice"

运行说明:

此代码完整覆盖了"按条件查找 "和"全部查找"两种场景,并保持了 Web3.0 业务的相关性。

**更多查询语句:**https://gorm.io/zh_CN/docs/query.html

(3)修改(U)

以下是基于相同 Web3.0 钱包场景,使用 GORM 实现**修改(Update)**操作的完整案例。代码在之前查询的基础上增加了按 ID 修改和按地址修改两个接口,并保持了与创建、查询一致的风格。

Go 复制代码
// Package main 演示 GORM 的更新操作(结合 Gin 和 PostgreSQL)
package main

import (
        "net/http"
        "time"

        "github.com/gin-gonic/gin"
        "gorm.io/driver/postgres"
        "gorm.io/gorm"
)

// Wallet 对应数据库中的 wallets 表
type Wallet struct {
        ID        uint      `gorm:"primarykey"`
        Address   string    `gorm:"size:42;uniqueIndex;comment:以太坊钱包地址"`
        Nickname  string    `gorm:"size:50;comment:用户昵称"`
        CreatedAt time.Time `gorm:"comment:创建时间"`
}

// 全局数据库连接
var DB *gorm.DB

// initDB 初始化 PostgreSQL 连接
func initDB() {
        dsn := "host=localhost user=postgres password=123456 dbname=postgres port=5432 sslmode=disable TimeZone=Asia/Shanghai"
        var err error
        DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
        if err != nil {
                panic("数据库连接失败: " + err.Error())
        }
        // 自动迁移(仅用于演示,生产环境建议手动控制)
        DB.AutoMigrate(&Wallet{})
}

// WalletController 处理钱包请求
type WalletController struct{}

// ------------------- 创建 -------------------
func (wc WalletController) CreateWallet(c *gin.Context) {
        var req struct {
                Address  string `json:"address" binding:"required,len=42"`
                Nickname string `json:"nickname" binding:"required,max=50"`
        }
        if err := c.ShouldBindJSON(&req); err != nil {
                c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误: " + err.Error()})
                return
        }
        wallet := Wallet{Address: req.Address, Nickname: req.Nickname}
        if err := DB.Create(&wallet).Error; err != nil {
                c.JSON(http.StatusConflict, gin.H{"error": "钱包地址已存在"})
                return
        }
        c.JSON(http.StatusOK, gin.H{"message": "钱包创建成功", "id": wallet.ID})
}

// ------------------- 查询 -------------------
// 按 ID 查询
func (wc WalletController) GetWalletByID(c *gin.Context) {
        id := c.Param("id")
        var wallet Wallet
        if err := DB.First(&wallet, id).Error; err != nil {
                if err == gorm.ErrRecordNotFound {
                        c.JSON(http.StatusNotFound, gin.H{"error": "钱包不存在"})
                } else {
                        c.JSON(http.StatusInternalServerError, gin.H{"error": "数据库错误"})
                }
                return
        }
        c.JSON(http.StatusOK, wallet)
}

// 按地址精确查询
func (wc WalletController) GetWalletByAddress(c *gin.Context) {
        address := c.Query("address")
        if address == "" {
                c.JSON(http.StatusBadRequest, gin.H{"error": "缺少地址参数"})
                return
        }
        var wallet Wallet
        if err := DB.Where("address = ?", address).First(&wallet).Error; err != nil {
                if err == gorm.ErrRecordNotFound {
                        c.JSON(http.StatusNotFound, gin.H{"error": "钱包不存在"})
                } else {
                        c.JSON(http.StatusInternalServerError, gin.H{"error": "数据库错误"})
                }
                return
        }
        c.JSON(http.StatusOK, wallet)
}

// 按昵称模糊查询
func (wc WalletController) SearchWalletsByNickname(c *gin.Context) {
        nickname := c.Query("nickname")
        if nickname == "" {
                c.JSON(http.StatusBadRequest, gin.H{"error": "缺少昵称参数"})
                return
        }
        var wallets []Wallet
        if err := DB.Where("nickname LIKE ?", "%"+nickname+"%").Find(&wallets).Error; err != nil {
                c.JSON(http.StatusInternalServerError, gin.H{"error": "数据库错误"})
                return
        }
        c.JSON(http.StatusOK, wallets)
}

// 全部查找
func (wc WalletController) GetAllWallets(c *gin.Context) {
        var wallets []Wallet
        if err := DB.Order("created_at desc").Find(&wallets).Error; err != nil {
                c.JSON(http.StatusInternalServerError, gin.H{"error": "数据库错误"})
                return
        }
        c.JSON(http.StatusOK, wallets)
}

// ------------------- 修改 -------------------
// UpdateWalletByID 根据 ID 更新钱包信息(例如修改昵称)
// PUT /wallets/:id
func (wc WalletController) UpdateWalletByID(c *gin.Context) {
        id := c.Param("id")
        // 先检查钱包是否存在
        var wallet Wallet
        if err := DB.First(&wallet, id).Error; err != nil {
                if err == gorm.ErrRecordNotFound {
                        c.JSON(http.StatusNotFound, gin.H{"error": "钱包不存在"})
                } else {
                        c.JSON(http.StatusInternalServerError, gin.H{"error": "数据库错误"})
                }
                return
        }

        // 定义可更新的字段(通常只允许修改昵称,地址不可变)
        var req struct {
                Nickname string `json:"nickname" binding:"required,max=50"`
        }
        if err := c.ShouldBindJSON(&req); err != nil {
                c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误: " + err.Error()})
                return
        }

        // 执行更新(只更新 Nickname 字段)
        if err := DB.Model(&wallet).Update("nickname", req.Nickname).Error; err != nil {
                c.JSON(http.StatusInternalServerError, gin.H{"error": "更新失败"})
                return
        }

        c.JSON(http.StatusOK, gin.H{"message": "更新成功", "wallet": wallet})
}

// UpdateWalletByAddress 根据地址更新钱包信息(例如修改昵称)
// PUT /wallets?address=0x...
func (wc WalletController) UpdateWalletByAddress(c *gin.Context) {
        address := c.Query("address")
        if address == "" {
                c.JSON(http.StatusBadRequest, gin.H{"error": "缺少地址参数"})
                return
        }

        // 查找该地址的钱包
        var wallet Wallet
        if err := DB.Where("address = ?", address).First(&wallet).Error; err != nil {
                if err == gorm.ErrRecordNotFound {
                        c.JSON(http.StatusNotFound, gin.H{"error": "钱包不存在"})
                } else {
                        c.JSON(http.StatusInternalServerError, gin.H{"error": "数据库错误"})
                }
                return
        }

        var req struct {
                Nickname string `json:"nickname" binding:"required,max=50"`
        }
        if err := c.ShouldBindJSON(&req); err != nil {
                c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误: " + err.Error()})
                return
        }

        // 更新
        if err := DB.Model(&wallet).Update("nickname", req.Nickname).Error; err != nil {
                c.JSON(http.StatusInternalServerError, gin.H{"error": "更新失败"})
                return
        }

        c.JSON(http.StatusOK, gin.H{"message": "更新成功", "wallet": wallet})
}

func main() {
        initDB()
        r := gin.Default()
        wc := WalletController{}

        // 创建
        r.POST("/wallets", wc.CreateWallet)

        // 查询
        r.GET("/wallets/:id", wc.GetWalletByID)
        r.GET("/wallets", wc.GetWalletByAddress)          // 按地址查询
        r.GET("/wallets/search", wc.SearchWalletsByNickname) // 模糊查询
        r.GET("/wallets/all", wc.GetAllWallets)           // 全部查询

        // 修改
        r.PUT("/wallets/:id", wc.UpdateWalletByID)        // 按 ID 修改
        r.PUT("/wallets", wc.UpdateWalletByAddress)       // 按地址修改

        r.Run(":8080")
}

修改操作详解

(1)按 ID 修改

  • 路由: PUT /wallets/:id

  • 过程: 先通过 First 检查记录是否存在,若不存在返回 404;存在则从 JSON 中解析新昵称,使用 Model(&wallet).Update("nickname", req.Nickname) 更新单字段。

  • **优点:**明确只更新昵称,避免意外修改地址。

(2)按地址修改

  • 路由: PUT /wallets?address=0x...

  • **过程:**通过查询参数获取地址,查找对应钱包,后续更新逻辑同上。

  • **适用场景:**当你知道钱包地址但不知道 ID 时。

(3) GORM 更新方法说明:

  • Save:保存所有字段(包括零值),通常用于更新整个对象。

  • Updates :更新非零值字段,可传入结构体或 map

  • Update:更新单个字段。

本例中使用 Update 明确更新昵称,更加安全可控。

测试示例:

创建一条记录:

Go 复制代码
curl -X POST http://localhost:8080/wallets \
  -H "Content-Type: application/json" \
  -d '{"address": "0x742d35Cc6634C0532925a3b844BcC454e3f1e1f7", "nickname": "alice"}'

按 ID 修改昵称:

Go 复制代码
curl -X PUT http://localhost:8080/wallets/1 \
  -H "Content-Type: application/json" \
  -d '{"nickname": "alice_updated"}'

按地址修改昵称:

Go 复制代码
curl -X PUT "http://localhost:8080/wallets?address=0x742d35Cc6634C0532925a3b844BcC454e3f1e1f7" \
  -H "Content-Type: application/json" \
  -d '{"nickname": "alice_v2"}'

验证修改结果:再次查询 GET /wallets/1GET /wallets?address=... 即可看到昵称已更新。

**更多修改的方法参考:**https://gorm.io/zh_CN/docs/update.html

(4)删除(D)

以下是在相同 Web3.0 钱包场景下,使用 GORM 实现**删除(Delete)**操作的完整案例。代码整合了之前的所有 CRUD 操作(创建、查询、修改、删除),并添加了按 ID 删除和按地址删除两种方式。

Go 复制代码
// Package main 演示 GORM 的删除操作(结合 Gin 和 PostgreSQL)
package main

import (
        "net/http"
        "time"

        "github.com/gin-gonic/gin"
        "gorm.io/driver/postgres"
        "gorm.io/gorm"
)

// Wallet 对应数据库中的 wallets 表
type Wallet struct {
        ID        uint      `gorm:"primarykey"`
        Address   string    `gorm:"size:42;uniqueIndex;comment:以太坊钱包地址"`
        Nickname  string    `gorm:"size:50;comment:用户昵称"`
        CreatedAt time.Time `gorm:"comment:创建时间"`
}

// 全局数据库连接
var DB *gorm.DB

// initDB 初始化 PostgreSQL 连接
func initDB() {
        dsn := "host=localhost user=postgres password=123456 dbname=postgres port=5432 sslmode=disable TimeZone=Asia/Shanghai"
        var err error
        DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
        if err != nil {
                panic("数据库连接失败: " + err.Error())
        }
        // 自动迁移(仅用于演示,生产环境建议手动控制)
        DB.AutoMigrate(&Wallet{})
}

// WalletController 处理钱包请求
type WalletController struct{}

// ------------------- 创建 -------------------
func (wc WalletController) CreateWallet(c *gin.Context) {
        var req struct {
                Address  string `json:"address" binding:"required,len=42"`
                Nickname string `json:"nickname" binding:"required,max=50"`
        }
        if err := c.ShouldBindJSON(&req); err != nil {
                c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误: " + err.Error()})
                return
        }
        wallet := Wallet{Address: req.Address, Nickname: req.Nickname}
        if err := DB.Create(&wallet).Error; err != nil {
                c.JSON(http.StatusConflict, gin.H{"error": "钱包地址已存在"})
                return
        }
        c.JSON(http.StatusOK, gin.H{"message": "钱包创建成功", "id": wallet.ID})
}

// ------------------- 查询 -------------------
// 按 ID 查询
func (wc WalletController) GetWalletByID(c *gin.Context) {
        id := c.Param("id")
        var wallet Wallet
        if err := DB.First(&wallet, id).Error; err != nil {
                if err == gorm.ErrRecordNotFound {
                        c.JSON(http.StatusNotFound, gin.H{"error": "钱包不存在"})
                } else {
                        c.JSON(http.StatusInternalServerError, gin.H{"error": "数据库错误"})
                }
                return
        }
        c.JSON(http.StatusOK, wallet)
}

// 按地址精确查询
func (wc WalletController) GetWalletByAddress(c *gin.Context) {
        address := c.Query("address")
        if address == "" {
                c.JSON(http.StatusBadRequest, gin.H{"error": "缺少地址参数"})
                return
        }
        var wallet Wallet
        if err := DB.Where("address = ?", address).First(&wallet).Error; err != nil {
                if err == gorm.ErrRecordNotFound {
                        c.JSON(http.StatusNotFound, gin.H{"error": "钱包不存在"})
                } else {
                        c.JSON(http.StatusInternalServerError, gin.H{"error": "数据库错误"})
                }
                return
        }
        c.JSON(http.StatusOK, wallet)
}

// 按昵称模糊查询
func (wc WalletController) SearchWalletsByNickname(c *gin.Context) {
        nickname := c.Query("nickname")
        if nickname == "" {
                c.JSON(http.StatusBadRequest, gin.H{"error": "缺少昵称参数"})
                return
        }
        var wallets []Wallet
        if err := DB.Where("nickname LIKE ?", "%"+nickname+"%").Find(&wallets).Error; err != nil {
                c.JSON(http.StatusInternalServerError, gin.H{"error": "数据库错误"})
                return
        }
        c.JSON(http.StatusOK, wallets)
}

// 全部查找
func (wc WalletController) GetAllWallets(c *gin.Context) {
        var wallets []Wallet
        if err := DB.Order("created_at desc").Find(&wallets).Error; err != nil {
                c.JSON(http.StatusInternalServerError, gin.H{"error": "数据库错误"})
                return
        }
        c.JSON(http.StatusOK, wallets)
}

// ------------------- 修改 -------------------
// 按 ID 修改昵称
func (wc WalletController) UpdateWalletByID(c *gin.Context) {
        id := c.Param("id")
        var wallet Wallet
        if err := DB.First(&wallet, id).Error; err != nil {
                if err == gorm.ErrRecordNotFound {
                        c.JSON(http.StatusNotFound, gin.H{"error": "钱包不存在"})
                } else {
                        c.JSON(http.StatusInternalServerError, gin.H{"error": "数据库错误"})
                }
                return
        }
        var req struct {
                Nickname string `json:"nickname" binding:"required,max=50"`
        }
        if err := c.ShouldBindJSON(&req); err != nil {
                c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误: " + err.Error()})
                return
        }
        if err := DB.Model(&wallet).Update("nickname", req.Nickname).Error; err != nil {
                c.JSON(http.StatusInternalServerError, gin.H{"error": "更新失败"})
                return
        }
        c.JSON(http.StatusOK, gin.H{"message": "更新成功", "wallet": wallet})
}

// 按地址修改昵称
func (wc WalletController) UpdateWalletByAddress(c *gin.Context) {
        address := c.Query("address")
        if address == "" {
                c.JSON(http.StatusBadRequest, gin.H{"error": "缺少地址参数"})
                return
        }
        var wallet Wallet
        if err := DB.Where("address = ?", address).First(&wallet).Error; err != nil {
                if err == gorm.ErrRecordNotFound {
                        c.JSON(http.StatusNotFound, gin.H{"error": "钱包不存在"})
                } else {
                        c.JSON(http.StatusInternalServerError, gin.H{"error": "数据库错误"})
                }
                return
        }
        var req struct {
                Nickname string `json:"nickname" binding:"required,max=50"`
        }
        if err := c.ShouldBindJSON(&req); err != nil {
                c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误: " + err.Error()})
                return
        }
        if err := DB.Model(&wallet).Update("nickname", req.Nickname).Error; err != nil {
                c.JSON(http.StatusInternalServerError, gin.H{"error": "更新失败"})
                return
        }
        c.JSON(http.StatusOK, gin.H{"message": "更新成功", "wallet": wallet})
}

// ------------------- 删除 -------------------
// DeleteWalletByID 根据 ID 删除钱包(物理删除)
// DELETE /wallets/:id
func (wc WalletController) DeleteWalletByID(c *gin.Context) {
        id := c.Param("id")
        // 执行删除操作,返回删除的记录数
        result := DB.Delete(&Wallet{}, id)
        if result.Error != nil {
                c.JSON(http.StatusInternalServerError, gin.H{"error": "删除失败"})
                return
        }
        if result.RowsAffected == 0 {
                c.JSON(http.StatusNotFound, gin.H{"error": "钱包不存在"})
                return
        }
        c.JSON(http.StatusOK, gin.H{"message": "删除成功"})
}

// DeleteWalletByAddress 根据地址删除钱包(物理删除)
// DELETE /wallets?address=0x...
func (wc WalletController) DeleteWalletByAddress(c *gin.Context) {
        address := c.Query("address")
        if address == "" {
                c.JSON(http.StatusBadRequest, gin.H{"error": "缺少地址参数"})
                return
        }
        // 使用 Where 条件删除
        result := DB.Where("address = ?", address).Delete(&Wallet{})
        if result.Error != nil {
                c.JSON(http.StatusInternalServerError, gin.H{"error": "删除失败"})
                return
        }
        if result.RowsAffected == 0 {
                c.JSON(http.StatusNotFound, gin.H{"error": "钱包不存在"})
                return
        }
        c.JSON(http.StatusOK, gin.H{"message": "删除成功"})
}

func main() {
        initDB()
        r := gin.Default()
        wc := WalletController{}

        // 创建
        r.POST("/wallets", wc.CreateWallet)

        // 查询
        r.GET("/wallets/:id", wc.GetWalletByID)
        r.GET("/wallets", wc.GetWalletByAddress)          // 按地址查询
        r.GET("/wallets/search", wc.SearchWalletsByNickname) // 模糊查询
        r.GET("/wallets/all", wc.GetAllWallets)           // 全部查询

        // 修改
        r.PUT("/wallets/:id", wc.UpdateWalletByID)        // 按 ID 修改
        r.PUT("/wallets", wc.UpdateWalletByAddress)       // 按地址修改

        // 删除
        r.DELETE("/wallets/:id", wc.DeleteWalletByID)      // 按 ID 删除
        r.DELETE("/wallets", wc.DeleteWalletByAddress)     // 按地址删除

        r.Run(":8080")
}

删除操作详解

  1. 按 ID 删除

    1. 路由:DELETE /wallets/:id

    2. 方法:DB.Delete(&Wallet{}, id)

    3. 说明:GORM 会根据主键删除记录。Delete 方法返回 *gorm.DB,通过 RowsAffected 判断是否有记录被删除。

  2. 按地址删除

    1. 路由:DELETE /wallets?address=0x...

    2. 方法:DB.Where("address = ?", address).Delete(&Wallet{})

    3. 说明:先通过 Where 构造条件,再执行删除。同样通过 RowsAffected 判断结果。

测试示例:

创建两条测试数据:

Go 复制代码
curl -X POST http://localhost:8080/wallets -H "Content-Type: application/json" -d '{"address": "0x111...", "nickname": "alice"}'
curl -X POST http://localhost:8080/wallets -H "Content-Type: application/json" -d '{"address": "0x222...", "nickname": "bob"}'

按 ID 删除:

Go 复制代码
curl -X DELETE http://localhost:8080/wallets/1

按地址删除:

Go 复制代码
curl -X DELETE "http://localhost:8080/wallets?address=0x222..."

验证删除结果:

Go 复制代码
curl http://localhost:8080/wallets/all

注意事项:

  • 物理删除 :由于 Wallet 结构体未包含 gorm.DeletedAt 字段,上述删除是物理删除(直接从数据库中移除)。若需要软删除,可在模型中添加 gorm.DeletedAt 字段,并使用 Delete 方法会自动软删除。

  • 错误处理:当记录不存在时返回 404 状态码,数据库错误返回 500。

  • 幂等性 :多次删除同一记录不会报错,但第二次会返回 RowsAffected == 0,按 404 处理。

此代码完整展示了 GORM 的删除操作,并整合了所有 CRUD 功能,可直接运行测试。

更多删除的方法参考:https://gorm.io/zh_CN/docs/delete.html

7.1.5 Gin GORM 查询语句详解

官网地址:https://gorm.io/zh_CN/docs/query.html

以下表格总结了常用 SQL 操作符在 GORM 中的使用方式,每个操作符都配有示例说明,便于你在 Where 条件中灵活运用。

操作符 描述 GORM 示例(db.Where(...)
= 等于 db.Where("age = ?", 18).Find(&users)
< 小于 db.Where("age < ?", 20).Find(&users)
> 大于 db.Where("age > ?", 30).Find(&users)
<= 小于等于 db.Where("age <= ?", 25).Find(&users)
>= 大于等于 db.Where("age >= ?", 60).Find(&users)
!= 不等于 db.Where("age != ?", 18).Find(&users)
IS NOT NULL 不为空 db.Where("email IS NOT NULL").Find(&users)
IS NULL 为空 db.Where("email IS NULL").Find(&users)
BETWEEN AND 在范围内 db.Where("age BETWEEN ? AND ?", 18, 30).Find(&users)
NOT BETWEEN AND 不在范围内 db.Where("age NOT BETWEEN ? AND ?", 18, 30).Find(&users)
IN 在给定集合中 db.Where("age IN ?", []int{18, 20, 22}).Find(&users)
OR 或条件 db.Where("age = ? OR name = ?", 18, "tom").Find(&users)
AND 且条件(默认) db.Where("age > ? AND name LIKE ?", 18, "%张%").Find(&users)
NOT 非条件 db.Not("age = ?", 18).Find(&users)db.Where("NOT age = ?", 18).Find(&users)
LIKE 模糊匹配 db.Where("name LIKE ?", "%张%").Find(&users)

补充说明:

  • OR AND 除了在 Where 字符串中组合,还可以使用 GORM 的链式作用域,例如 db.Where(...).Or(...)db.Where(...).Where(...)

  • NOT 可以用 db.Not() 方法,也可以在 Where 字符串中写 NOT 条件。

  • BETWEEN / IN 的参数需为切片或可变参数。

这些操作符覆盖了绝大部分日常查询需求,结合 GORM 的链式调用,可以灵活构建复杂的 SQL 条件。

Go 复制代码
nav := []models.Nav{} 
models.DB.Where("id<3").Find(&nav) 
c.JSON(http.StatusOK, gin.H{ 
    "success": true, 
    "result": nav, 
})
Go 复制代码
var n = 5 
nav := []models.Nav{} 
models.DB.Where("id>?", n).Find(&nav) 
c.JSON(http.StatusOK, gin.H{ 
    "success": true, 
    "result": nav, 
})
Go 复制代码
var n1 = 3 
var n2 = 9 
nav := []models.Nav{} 
models.DB.Where("id > ? AND id < ?", n1, n2).Find(&nav) 
c.JSON(http.StatusOK, gin.H{ 
    "success": true, 
    "result": nav, 
})
Go 复制代码
nav := []models.Nav{} 
models.DB.Where("id in (?)", []int{3, 5, 6}).Find(&nav) 
c.JSON(http.StatusOK, gin.H{ 
    "success": true, 
    "result": nav, 
})
Go 复制代码
nav := []models.Nav{} 
models.DB.Where("title like ?", "%会%").Find(&nav) 
c.JSON(http.StatusOK, gin.H{ 
    "success": true, 
    "result": nav, 
})
Go 复制代码
nav := []models.Nav{} 
models.DB.Where("id between ? and ?", 3, 6).Find(&nav) 
c.JSON(http.StatusOK, gin.H{ 
    "success": true, 
    "result": nav, 
})

2、Or 条件

Go 复制代码
nav := []models.Nav{} 
models.DB.Where("id=? OR id=?", 2, 3).Find(&nav)
Go 复制代码
nav := []models.Nav{} 
models.DB.Where("id=?", 2).Or("id=?", 3).Or("id=4").Find(&nav)

3、选择字段查询

Go 复制代码
nav := []models.Nav{} 
models.DB.Select("id, title,url").Find(&nav)

4、排序 Limit 、Offset

Go 复制代码
nav := []models.Nav{} 
models.DB.Where("id>2").Order("id Asc").Find(&nav) 
nav := []models.Nav{} 
models.DB.Where("id>2").Order("sort Desc").Order("id Asc").Find(&nav) 
nav := []models.Nav{} 
odels.DB.Where("id>1").Limit(2).Find(&nav)

跳过 2 条查询 2 条

Go 复制代码
nav := []models.Nav{} 
models.DB.Where("id>1").Offset(2).Limit(2).Find(&nav)

5、获取总数

Go 复制代码
nav := []models.Nav{} 
var num int 
models.DB.Where("id > ?", 2).Find(&nav).Count(&num)

6、Distinct

从模型中选择不相同的值

Go 复制代码
nav := []models.Nav{} 
models.DB.Distinct("title").Order("id desc").Find(&nav) 
c.JSON(200, gin.H{ 
    "nav": nav, 
})
sql 复制代码
SELECT DISTINCT `title` FROM `nav` ORDER BY id desc

7、Scan

sql 复制代码
type Result struct { 
    Name string 
    Age int 
} 
var result Result 
db.Table("users").Select("name", "age").Where("name = ?", "Antonio").Scan(&result) 

// 原生 SQL 
db.Raw("SELECT name, age FROM users WHERE name = ?", "Antonio").Scan(&result) 
var result []models.User 
models.DB.Raw("SELECT * FROM user").Scan(&result) 
fmt.Println(result)

8、Join (先了解 后面课程会讲关联查询)

sql 复制代码
type result struct { 
    Name string 
    Email string 
} 
db.Model(&User{}).Select("users.name, emails.email").Joins("left join emails on emails.user_i d = users.id").Scan(&result{}) 
SELECT users.name, emails.email FROM `users` left join emails on emails.user_id = users.id

7.1.6 Gin GORM 查看执行的 sql

sql 复制代码
func init() { 
    dsn := "root:123456@tcp(192.168.0.6:3306)/gin?charset=utf8mb4&parseTime=True&loc=Local" 
    DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{ 
        QueryFields: true, 
    }) 

    // DB.Debug() 
    if err != nil { 
        fmt.Println(err) 
    } 
}

7.2 原生 SQL 和 SQL 生成器

在 Gin 框架中,原生 SQL 指的是开发者直接编写数据库查询语句(如 SELECT * FROM users WHERE id = ?),通过 database/sql 或数据库驱动执行,灵活但需手动处理参数绑定和结果映射,容易出错且难以维护;而 SQL 生成器(如 GORM 的链式调用、sqlx 的命名查询等)则通过结构体方法或构建器动态生成 SQL,自动处理参数转义和结果映射,既提升了代码可读性,又降低了 SQL 注入风险,让开发者能更专注于业务逻辑,两者在 Gin 路由处理函数中均可结合使用,但推荐优先选用 SQL 生成器以保障安全与效率。

**内容可参考:**https://gorm.io/zh_CN/docs/sql_builder.html

7.2.1 原生 sql 删除 表中的一条数据

以下是一个使用原生 SQL 在 GORM 中删除表中一条数据的完整案例,基于 Gin 框架和 PostgreSQL 数据库,并贴合图中示例。

Go 复制代码
// Package main 是程序的入口包,演示在 Gin 框架中使用 GORM 执行原生 SQL 删除操作
package main

import (
        "fmt"      // 用于控制台输出,例如打印受影响的行数
        "net/http" // 提供 HTTP 相关的常量和方法,如 http.StatusOK
        "strconv"  // 提供字符串与其他类型转换的功能,此处用于将字符串 ID 转换为整数

        "github.com/gin-gonic/gin" // Gin Web 框架,用于处理 HTTP 请求和响应
        "gorm.io/driver/postgres"   // GORM 的 PostgreSQL 驱动,让 GORM 能够连接和操作 PostgreSQL 数据库
        "gorm.io/gorm"              // GORM 核心库,提供 ORM 功能,包括链式调用、自动迁移、SQL 执行等
)

// User 是一个 GORM 模型,对应数据库中的 user 表。
// 通过结构体定义字段,GORM 会根据这些定义自动生成对应的表结构(结合 AutoMigrate)。
// 结构体的字段名默认转换为小写蛇形命名作为列名,可以通过 gorm tag 自定义。
type User struct {
        ID   uint   `gorm:"primarykey"` // 主键,GORM 默认自动递增,对应数据库列 id
        Name string `gorm:"size:100"`   // 字符串类型,指定最大长度为 100,对应列 name
        Age  int                        // 整型,对应列 age
}

// 全局数据库连接变量 DB,它是指向 gorm.DB 的指针。
// 因为需要在多个路由处理函数中访问数据库,所以定义为全局变量,
// 在 main 函数之前 initDB 中初始化,之后所有路由都可以使用它。
var DB *gorm.DB

// initDB 负责初始化数据库连接和执行自动迁移。
// 该函数在 main 函数开始时被调用,确保数据库连接建立后再启动 Web 服务。
func initDB() {
        // DSN(Data Source Name)是 PostgreSQL 连接字符串,包含连接所需的所有参数。
        // 各参数含义:
        //   host: 数据库服务器地址,localhost 表示本机
        //   user: 数据库用户名,此处为 postgres
        //   password: 数据库密码,此处为 123456
        //   dbname: 要连接的数据库名称,此处为 postgres(默认数据库)
        //   port: PostgreSQL 默认端口 5432
        //   sslmode: SSL 模式,disable 表示禁用 SSL(适用于本地开发)
        //   TimeZone: 数据库会话时区,设为 Asia/Shanghai 以便正确处理时间
        dsn := "host=localhost user=postgres password=123456 dbname=postgres port=5432 sslmode=disable TimeZone=Asia/Shanghai"

        // gorm.Open 用于建立数据库连接。它接收一个数据库驱动(由 postgres.Open 返回)和配置。
        // 返回的 *gorm.DB 实例包含了连接池,并提供了所有数据库操作方法。
        var err error
        DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
        if err != nil {
                // 如果连接失败,直接终止程序,并输出错误信息。
                // 在生产环境中,可能需要更优雅的处理,例如重试或记录日志后退出。
                panic("数据库连接失败: " + err.Error())
        }

        // AutoMigrate 会自动根据传入的结构体创建或更新数据库表。
        // 它会添加新字段、建立索引、修改列类型等,但不会删除已存在的列或数据。
        // 注意:在生产环境中,自动迁移可能导致意外变更,建议手动管理迁移或仅在开发阶段使用。
        // 此处为了演示方便,使用 AutoMigrate 确保 user 表存在。
        if err := DB.AutoMigrate(&User{}); err != nil {
                panic("自动迁移失败: " + err.Error())
        }

        // 插入一些测试数据,便于演示删除接口。
        // FirstOrCreate 是 GORM 的便捷方法:它会查找第一条匹配条件的记录,如果不存在则创建。
        // 这里以 User 结构体中的 Name 和 Age 作为查询条件,若没有完全相同的记录则插入。
        // 通过这种方式,多次运行程序不会重复插入相同的数据。
        DB.FirstOrCreate(&User{Name: "张三", Age: 25})
        DB.FirstOrCreate(&User{Name: "李四", Age: 30})
}

func main() {
        // 首先调用 initDB 初始化数据库连接和表结构。
        // 这样在后续启动 Web 服务时,数据库已经准备就绪。
        initDB()

        // 创建一个默认的 Gin 引擎实例。
        // gin.Default() 会附加两个中间件:Logger(打印请求日志)和 Recovery(捕获 panic 并返回 500 错误)。
        r := gin.Default()

        // 定义一个 DELETE 路由,路径为 "/user/:id"。
        // ":id" 是路径参数,可以匹配类似 /user/1 的请求,其中 1 会被作为 id 参数的值。
        r.DELETE("/user/:id", func(c *gin.Context) {
                // 1. 获取路径中的 id 参数。
                // c.Param("id") 返回字符串类型的值。
                idStr := c.Param("id")

                // 2. 将字符串 id 转换为整数,因为 SQL 语句中需要整数类型。
                // strconv.Atoi 是 Go 标准库函数,将字符串转为 int,如果转换失败返回错误。
                id, err := strconv.Atoi(idStr)
                if err != nil {
                        // 如果 id 不是有效的数字,返回 400 Bad Request 错误。
                        c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
                        return
                }

                // 3. 使用 GORM 的 Exec 方法执行原生 SQL 语句。
                // Exec 可以执行任意的 SQL 命令,包括 DELETE、UPDATE、INSERT 等。
                // 这里执行 "DELETE FROM user WHERE id = ?",使用占位符 ? 传递 id 值。
                // 占位符可以防止 SQL 注入攻击,GORM 会对参数进行转义。
                // result 是 *gorm.DB 类型,包含了执行后的结果信息。
                result := DB.Exec("DELETE FROM user WHERE id = ?", id)

                // 4. 检查执行过程中是否有错误。
                // result.Error 保存了执行 SQL 时发生的错误,例如数据库连接断开、语法错误等。
                if result.Error != nil {
                        c.JSON(http.StatusInternalServerError, gin.H{"error": "删除失败: " + result.Error.Error()})
                        return
                }

                // 5. 获取受影响的行数。
                // result.RowsAffected 返回被 DELETE 语句影响的记录数。
                rowsAffected := result.RowsAffected
                // 在控制台打印受影响行数,方便调试。
                fmt.Println("受影响行数:", rowsAffected)

                // 6. 根据受影响行数判断删除结果。
                if rowsAffected == 0 {
                        // 如果为 0,说明没有找到对应的记录,返回 404 Not Found。
                        c.JSON(http.StatusNotFound, gin.H{"message": "用户不存在"})
                        return
                }

                // 7. 删除成功,返回 200 OK 和相关信息。
                c.JSON(http.StatusOK, gin.H{
                        "message":       "删除成功",
                        "rows_affected": rowsAffected, // 返回受影响行数,前端可用于校验
                })
        })

        // 定义一个 GET 路由,用于查询所有用户,方便测试删除操作后验证结果。
        r.GET("/users", func(c *gin.Context) {
                var users []User // 定义一个 User 切片,用于存储查询结果
                // DB.Find(&users) 查询所有用户记录,并按主键升序返回。
                // Find 方法会自动将结果映射到 users 切片中。
                DB.Find(&users)
                // 以 JSON 格式返回用户列表。
                c.JSON(http.StatusOK, users)
        })

        // 启动 Gin 服务器,监听在 8080 端口。
        // r.Run 默认会监听 0.0.0.0:8080,可以通过参数指定其他地址,例如 r.Run(":9090")。
        // 如果启动失败(例如端口被占用),会返回错误。
        r.Run(":8080")
}

代码说明:

(1)模型定义User 结构体对应数据库的 user 表,包含 ID、Name 和 Age 字段。

(2)数据库初始化:使用提供的 PostgreSQL DSN 连接数据库,并自动迁移创建表(若表不存在则创建)。同时插入两条测试数据。

(3)Gin 路由

  • DELETE /user/:id:通过 URL 路径参数接收要删除的用户 ID。

  • 内部使用 DB.Exec("DELETE FROM user WHERE id = ?", id) 执行原生 SQL 删除。

  • 通过 result.RowsAffected 获取受影响行数并打印(与图中示例一致)。

  • 根据结果返回相应的 JSON 响应。

(4)验证接口GET /users 可以查询所有用户,便于测试前后对比。

运行测试:

(1)确保 PostgreSQL 服务运行,并根据实际情况修改 DSN 中的用户名、密码和数据库名。

(2)执行 go mod init demo 并安装依赖:

Go 复制代码
go get -u gorm.io/gorm
go get -u gorm.io/driver/postgres
go get -u github.com/gin-gonic/gin

(3)运行程序:go run main.go

(4)测试删除:

  • 先访问 GET http://localhost:8080/users 查看现有用户。

  • 执行删除,例如删除 ID 为 1 的用户:curl -X DELETE http://localhost:8080/user/1

  • 再次查看用户列表,确认记录已被删除。

(5)终端会输出受影响行数,如 受影响行数: 1

该案例完整展示了在 Gin 中使用 GORM 执行原生 SQL 删除操作,并获取受影响行数的过程。

7.2.2 使用原生 sql 修改 user 表中的一条数据

Go 复制代码
// Package main 演示在 Gin 框架中使用 GORM 执行原生 SQL 更新操作
package main

import (
        "fmt"
        "net/http"
        "strconv"

        "github.com/gin-gonic/gin"
        "gorm.io/driver/postgres"
        "gorm.io/gorm"
)

// User 模型对应数据库中的 user 表
type User struct {
        ID       uint   `gorm:"primarykey"`
        Username string `gorm:"size:100;column:username"` // 图中字段名为 username
        Age      int
}

// TableName 指定表名为 user(默认会加复数,这里显式指定为单数)
func (User) TableName() string {
        return "user"
}

// 全局数据库连接
var DB *gorm.DB

// initDB 初始化 PostgreSQL 连接并自动迁移
func initDB() {
        // 根据实际情况修改 DSN
        dsn := "host=localhost user=postgres password=123456 dbname=postgres port=5432 sslmode=disable TimeZone=Asia/Shanghai"
        var err error
        DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
        if err != nil {
                panic("数据库连接失败: " + err.Error())
        }
        // 自动迁移创建 user 表(仅演示,生产环境建议手动控制)
        if err := DB.AutoMigrate(&User{}); err != nil {
                panic("自动迁移失败: " + err.Error())
        }
        // 插入测试数据
        DB.FirstOrCreate(&User{Username: "张三", Age: 25})
        DB.FirstOrCreate(&User{Username: "李四", Age: 30})
}

func main() {
        initDB()
        r := gin.Default()

        // 更新用户接口:PUT /user/:id
        // 请求体可接收 JSON 格式,例如 {"username": "新名字"}
        r.PUT("/user/:id", func(c *gin.Context) {
                // 1. 获取路径参数 id
                idStr := c.Param("id")
                id, err := strconv.Atoi(idStr)
                if err != nil {
                        c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
                        return
                }

                // 2. 定义接收请求体的结构
                var req struct {
                        Username string `json:"username" binding:"required"` // 新的用户名
                }
                // 绑定 JSON 请求体
                if err := c.ShouldBindJSON(&req); err != nil {
                        c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误: " + err.Error()})
                        return
                }

                // 3. 使用原生 SQL 执行更新操作
                // 图中示例:result := models.DB.Exec("update user set username=? where id=?", "哈哈")
                // 这里使用占位符传递参数,防止 SQL 注入
                result := DB.Exec("UPDATE user SET username = ? WHERE id = ?", req.Username, id)

                // 4. 检查执行错误
                if result.Error != nil {
                        c.JSON(http.StatusInternalServerError, gin.H{"error": "更新失败: " + result.Error.Error()})
                        return
                }

                // 5. 获取受影响的行数并打印(图中第二行)
                rowsAffected := result.RowsAffected
                fmt.Println("受影响行数:", rowsAffected)

                if rowsAffected == 0 {
                        c.JSON(http.StatusNotFound, gin.H{"message": "用户不存在"})
                        return
                }

                // 6. 返回成功响应
                c.JSON(http.StatusOK, gin.H{
                        "message":       "更新成功",
                        "rows_affected": rowsAffected,
                })
        })

        // 可选:查询所有用户用于验证
        r.GET("/users", func(c *gin.Context) {
                var users []User
                DB.Find(&users)
                c.JSON(http.StatusOK, users)
        })

        r.Run(":8080")
}

7.2.3 查询特定条件的数据(查询 uid=2 的数据)

Go 复制代码
// Package main 演示在 Gin 中使用 GORM 执行原生 SQL 查询
package main

import (
        "fmt"
        "net/http"
        "strconv"

        "github.com/gin-gonic/gin"
        "gorm.io/driver/postgres"
        "gorm.io/gorm"
)

// User 模型对应数据库中的 user 表
type User struct {
        ID       uint   `gorm:"primarykey"`        // 用户ID
        Username string `gorm:"size:100;column:username"` // 用户名
        Age      int                                 // 年龄
}

// TableName 指定表名为 user(GORM 默认会使用复数形式,这里强制使用单数)
func (User) TableName() string {
        return "user"
}

// 全局数据库连接变量
var DB *gorm.DB

// initDB 初始化 PostgreSQL 连接并自动迁移
func initDB() {
        // 请根据实际情况修改 DSN 中的用户名、密码和数据库名
        dsn := "host=localhost user=postgres password=123456 dbname=postgres port=5432 sslmode=disable TimeZone=Asia/Shanghai"
        var err error
        DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
        if err != nil {
                panic("数据库连接失败: " + err.Error())
        }

        // 自动迁移创建 user 表(仅用于演示,生产环境建议手动控制)
        if err := DB.AutoMigrate(&User{}); err != nil {
                panic("自动迁移失败: " + err.Error())
        }

        // 插入测试数据,确保数据库中有 uid=2 的记录(如果不存在则创建)
        DB.FirstOrCreate(&User{ID: 1, Username: "张三", Age: 25})
        DB.FirstOrCreate(&User{ID: 2, Username: "李四", Age: 30}) // 图中查询 id=2 的数据
}

func main() {
        initDB()
        r := gin.Default()

        // 查询用户接口:GET /user/:id,例如 /user/2
        r.GET("/user/:id", func(c *gin.Context) {
                // 1. 获取路径中的 id 参数并转换为整数
                idStr := c.Param("id")
                id, err := strconv.Atoi(idStr)
                if err != nil {
                        c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
                        return
                }

                // 2. 定义变量用于存储查询结果
                var result User

                // 3. 使用原生 SQL 查询指定 ID 的用户
                // 对应图中的代码:models.DB.Raw("SELECT * FROM user WHERE id = ?", 2).Scan(&result)
                // GORM 的 Raw 方法执行原生 SQL,Scan 将结果映射到结构体
                if err := DB.Raw("SELECT * FROM user WHERE id = ?", id).Scan(&result).Error; err != nil {
                        c.JSON(http.StatusInternalServerError, gin.H{"error": "查询失败: " + err.Error()})
                        return
                }

                // 4. 检查是否找到记录(如果 ID 不存在,result 会是零值,可以根据 ID 是否为0判断)
                if result.ID == 0 {
                        c.JSON(http.StatusNotFound, gin.H{"message": "用户不存在"})
                        return
                }

                // 5. 打印查询结果到控制台(图中示例的 fmt.Println(result))
                fmt.Printf("查询结果: %+v\n", result)

                // 6. 返回 JSON 格式的用户信息
                c.JSON(http.StatusOK, result)
        })

        // 可选:查询所有用户用于验证
        r.GET("/users", func(c *gin.Context) {
                var users []User
                DB.Find(&users)
                c.JSON(http.StatusOK, users)
        })

        r.Run(":8080")
}

7.2.4 查询 User 表中所有的数据

Go 复制代码
// Package main 演示在 Gin 中使用 GORM 执行原生 SQL 查询所有数据
package main

import (
        "fmt"
        "net/http"

        "github.com/gin-gonic/gin"
        "gorm.io/driver/postgres"
        "gorm.io/gorm"
)

// User 模型对应数据库中的 user 表
type User struct {
        ID       uint   `gorm:"primarykey"`               // 用户ID
        Username string `gorm:"size:100;column:username"` // 用户名
        Age      int                                       // 年龄
}

// TableName 指定表名为 user(GORM 默认使用复数形式,这里强制使用单数)
func (User) TableName() string {
        return "user"
}

// 全局数据库连接变量
var DB *gorm.DB

// initDB 初始化 PostgreSQL 连接并自动迁移
func initDB() {
        // 根据实际情况修改 DSN 中的用户名、密码和数据库名
        dsn := "host=localhost user=postgres password=123456 dbname=postgres port=5432 sslmode=disable TimeZone=Asia/Shanghai"

        var err error
        DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
        if err != nil {
                panic("数据库连接失败: " + err.Error())
        }

        // 自动迁移创建 user 表(仅用于演示,生产环境建议手动管理表结构)
        if err := DB.AutoMigrate(&User{}); err != nil {
                panic("自动迁移失败: " + err.Error())
        }

        // 插入测试数据,确保表中有数据可查(如果不存在则创建)
        DB.FirstOrCreate(&User{Username: "张三", Age: 25})
        DB.FirstOrCreate(&User{Username: "李四", Age: 30})
        DB.FirstOrCreate(&User{Username: "王五", Age: 28})
}

func main() {
        initDB()
        r := gin.Default()

        // 查询所有用户接口:GET /users
        r.GET("/users", func(c *gin.Context) {
                // 1. 定义切片用于存储查询结果
                var result []User

                // 2. 使用原生 SQL 查询所有用户数据
                // 对应图中的代码:models.DB.Raw("SELECT * FROM user").Scan(&result)
                // Raw 执行原生 SQL 语句,Scan 将结果映射到 result 切片中
                if err := DB.Raw("SELECT * FROM user").Scan(&result).Error; err != nil {
                        c.JSON(http.StatusInternalServerError, gin.H{"error": "查询失败: " + err.Error()})
                        return
                }

                // 3. 打印查询结果到控制台(对应图中的 fmt.Println(result))
                fmt.Printf("查询所有用户结果: %+v\n", result)

                // 4. 返回 JSON 响应
                c.JSON(http.StatusOK, result)
        })

        // 可选:单独查询某个用户的接口,用于对比测试
        r.GET("/user/:id", func(c *gin.Context) {
                id := c.Param("id")
                var user User
                if err := DB.Raw("SELECT * FROM user WHERE id = ?", id).Scan(&user).Error; err != nil {
                        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
                        return
                }
                if user.ID == 0 {
                        c.JSON(http.StatusNotFound, gin.H{"message": "用户不存在"})
                        return
                }
                c.JSON(http.StatusOK, user)
        })

        r.Run(":8080")
}

7.2.5 统计 user 表的数量

Go 复制代码
// Package main 演示在 Gin 中使用 GORM 执行原生 SQL 统计表记录数
package main

import (
        "fmt"
        "net/http"

        "github.com/gin-gonic/gin"
        "gorm.io/driver/postgres"
        "gorm.io/gorm"
)

// User 模型对应数据库中的 user 表
type User struct {
        ID       uint   `gorm:"primarykey"`               // 用户ID
        Username string `gorm:"size:100;column:username"` // 用户名
        Age      int                                       // 年龄
}

// TableName 指定表名为 user(GORM 默认使用复数形式,这里强制使用单数)
func (User) TableName() string {
        return "user"
}

// 全局数据库连接变量
var DB *gorm.DB

// initDB 初始化 PostgreSQL 连接并自动迁移
func initDB() {
        // 根据实际情况修改 DSN 中的用户名、密码和数据库名
        dsn := "host=localhost user=postgres password=123456 dbname=postgres port=5432 sslmode=disable TimeZone=Asia/Shanghai"

        var err error
        DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
        if err != nil {
                panic("数据库连接失败: " + err.Error())
        }

        // 自动迁移创建 user 表(仅用于演示,生产环境建议手动管理表结构)
        if err := DB.AutoMigrate(&User{}); err != nil {
                panic("自动迁移失败: " + err.Error())
        }

        // 插入测试数据,确保表中有数据可查(如果不存在则创建)
        DB.FirstOrCreate(&User{Username: "张三", Age: 25})
        DB.FirstOrCreate(&User{Username: "李四", Age: 30})
        DB.FirstOrCreate(&User{Username: "王五", Age: 28})
}

func main() {
        initDB()
        r := gin.Default()

        // 统计用户数量接口:GET /user/count
        r.GET("/user/count", func(c *gin.Context) {
                // 1. 定义变量用于存储统计结果
                var count int64 // 使用 int64 更合适,因为 count 可能很大

                // 2. 使用原生 SQL 查询记录总数
                // 对应图中的代码(修正了图中的语法错误):
                //   row := models.DB.Raw("SELECT count(1) FROM user").Row()
                //   row.Scan(&count)
                // 注意:图中的 Row(&count) 是错误的写法,正确做法是先获取 *sql.Row,再 Scan。
                row := DB.Raw("SELECT count(1) FROM user").Row()
                if err := row.Scan(&count); err != nil {
                        c.JSON(http.StatusInternalServerError, gin.H{"error": "统计失败: " + err.Error()})
                        return
                }

                // 3. 打印统计结果到控制台(对应图中的 fmt.Println(result),此处打印 count)
                fmt.Printf("用户总数: %d\n", count)

                // 4. 返回 JSON 格式的统计结果
                c.JSON(http.StatusOK, gin.H{
                        "count": count,
                })
        })

        // 可选路由:查询所有用户,便于验证统计结果
        r.GET("/users", func(c *gin.Context) {
                var users []User
                DB.Find(&users)
                c.JSON(http.StatusOK, users)
        })

        // 启动服务,监听 8080 端口
        r.Run(":8080")
}

7.3 Gin中使用 GORM 实现表关联查询

**官网地址:**https://gorm.io/zh_CN/docs/has_many.html

7.3.1 一对一

在 Gin 中处理一对一关系通常指的是在结合 GORM 的模型层定义关联(如 HasOneBelongsTo),然后在控制器中通过 PreloadJoins 方法预加载关联数据,最终以 JSON 格式返回给客户端,从而实现类似"用户及其个人资料"的 API 响应------Gin 作为 Web 层负责接收请求和返回响应,而数据关联的逻辑完全由 GORM 在模型层透明地完成。

如一个文章只有一个分类,article 和 article_cate 之间是 1 对 1 的关系。 文章表中的 cate_id 保存着文章分类的 id。 如果我们想查询文章的时候同时获取文章分类,就涉及到 1 对 1 的关联查询。 foreignkey 指定当前表的外键、references 指定关联表中和外键关联的字段 Article。

Go 复制代码
package models

type Article struct { 
    Id int `json:"id"` 
    Title string `json:"title"` 
    Description int `json:"description"` 
    CateId string `json:"cate_id"` 
    State int `json:"state"` 
    ArticleCate ArticleCate `gorm:"foreignKey:CateId;references:Id"` 
}

func (Article) TableName() string { 
    return "article" 
}

ArticleCate

Go 复制代码
package models

//ArticleCate 的结构体 
type ArticleCate struct { 
    Id int `json:"id"` 
    Title string `json:"title"` 
    State int `json:"state"` 
}

func (ArticleCate) TableName() string { 
    return "article_cate" 
}

1、查询所有文章以及文章对应的分类信息:

Go 复制代码
func (con ArticleController) Index(c *gin.Context) { 
    var articleList []models.Article 
    models.DB.Preload("ArticleCate").Limit(2).Find(&articleList) 
    c.JSON(200, gin.H{ 
        "result": articleList, 
    }) 
}

注意: Preload("ArticleCate")里面的 ArticleCate 为 Article struct 中定义的属性 ArticleCate。返回 JSON 数据:

Go 复制代码
[ 
    { 
        "id": 1, 
        "title": "8 月份 CPI 同比上涨 2.8% 猪肉价格上涨 46.7%", 
        "description": 0, 
        "cate_id": "1", 
        "state": 1, 
        "ArticleCate": { 
            "id": 1, 
            "title": "国内",
            "state": 1 
        } 
    },
    { 
        "id": 2, 
        "title": "中国联通与中国电信共建共享 5G 网络 用户归属不变", 
        "description": 0, 
        "cate_id": "1", 
        "state": 1, 
        "ArticleCate": { 
            "id": 1, 
            "title": "国内", 
            "state": 1 
        } 
}]

2、查询所有文章以及文章对应的分类信息指定条件:

Go 复制代码
func (con ArticleController) Index(c *gin.Context) { 
    var articleList []models.Article 
    models.DB.Preload("ArticleCate").Where("id>=?", 4).Find(&articleList) 
    c.JSON(200, gin.H{ 
        "result": articleList, 
    }) 
}

返回数据:

Go 复制代码
[ 
{ 
    "id": 4, 
    "title": "这些老师的口头禅,想起那些年"被支配的恐惧"了吗", 
    "description": 0, 
    "cate_id": "2", 
    "state": 1, 
    "ArticleCate": { 
        "id": 2, 
        "title": "国际", 
        "state": 1 
    }
},
{ 
    "id": 5, 
    "title": "美国空军一号差点遭雷劈,特朗普惊呼:令人惊奇", 
    "description": 0, 
    "cate_id": "3", 
    "state": 1, 
    "ArticleCate": { 
        "id": 3, 
        "title": "娱乐", 
        "state": 1 
    } 
} 
]

7.3.2 一对多

在 Gin 框架结合 GORM 的场景下,一对多关系指的是一个模型(例如"用户")可以拥有多个子模型(例如"文章"),而每个子模型属于唯一的父模型;通过 GORM 的 HasManyBelongsTo 标签在结构体中定义关联,然后在控制器中使用 PreloadJoins 方法进行预加载,最终在 Gin 的路由处理函数中以 JSON 形式返回嵌套数据,从而构建出如"用户及其文章列表"这样的 RESTful API。

**1 对多**在实际项目中用的非常多比如一个点餐系统:有菜品分类、有菜品。 菜品分类和菜品之间就是一对多的关系订单表和订单商品表:订单表和订单商品表之间也是一对多的关系。

ArticleCate

Go 复制代码
// ArticleCate 的结构体
package models 
type ArticleCate struct { 
    Id int `json:"id"` 
    Title string `json:"title"` 
    State int `json:"state"` 
    Article []Article `gorm:"foreignKey:CateId"` 
}
Go 复制代码
func (ArticleCate) TableName() string { 
    return "article_cate" 
}

Article

Go 复制代码
package models

type Article struct { 
    Id int `json:"id"` 
    Title string `json:"title"` 
    Description int `json:"description"` 
    CateId string `json:"cate_id"` 
    State int `json:"state"` 
}

func (Article) TableName() string { 
    return "article" 
}

1、查找所有分类以及分类下面的文章信息

Go 复制代码
func (con ArticleController) Index(c *gin.Context) { 
    var articleCateList []models.ArticleCate 
    models.DB.Preload("Article").Find(&articleCateList) 
    c.JSON(200, gin.H{ 
        "result": articleCateList, 
    }) 
}

返回数据:

Go 复制代码
[ 
    { 
        "id": 1,
        "title": "国内", 
        "state": 1, 
        "Article": [ 
                        { 
                            "id": 1, 
                            "title": "8 月份 CPI 同比上涨 2.8% 猪肉价格上涨 46.7%", 
                            "description": 0, 
                            "cate_id": "1", 
                            "state": 1 
                        },
                        { 
                            "id": 2, 
                            "title": "中国联通与中国电信共建共享 5G 网络 用户归属不变", 
                            "description": 0, 
                            "cate_id": "1", 
                            "state": 1 
                        } 
                    ] 
    },
    { 
        "id": 2, 
        "title": "国际", 
        "state": 1, 
        "Article": [ 
                        { 
                            "id": 3, 
                            "title": "林郑月娥斥责暴徒破坏港铁:不能因为没生命就肆意破坏", 
                            "description": 0, 
                            "cate_id": "2", 
                            "state": 1 
                        },
                        { 
                            "id": 4, 
                            "title": "这些老师的口头禅,想起那些年"被支配的恐惧"了吗", 
                            "description": 0, 
                            "cate_id": "2", 
                            "state": 1 
                        } 
                    ] 
    },
... 
]

2、查找所有分类以及分类下面的文章信息 指定条件

Go 复制代码
func (con ArticleController) Index(c *gin.Context) { 
    var articleCateList []models.ArticleCate 
    models.DB.Preload("Article").Where("id>0").Offset(1).Limit(1).Find(&articleCateList) 
    c.JSON(200, gin.H{ 
        "result": articleCateList, 
    }) 
}
Go 复制代码
[ 
    { 
    "id": 2, 
    "title": "国际", 
    "state": 1, 
    "Article": [ 
    { 
    "id": 3, 
    "title": "林郑月娥斥责暴徒破坏港铁:不能因为没生命就肆意破坏", 
    "description": 0, 
    "cate_id": "2", 
    "state": 1 
    },
    { 
    "id": 4, 
    "title": "这些老师的口头禅,想起那些年"被支配的恐惧"了吗", 
    "description": 0, 
    "cate_id": "2", 
    "state": 1 
    }
    ]
    }
]

3、更多 1 对多的查询方法

地址: https://github.com/jouyouyun/examples/tree/master/gorm/related

4、如果我们的程序中有如下需求

1、查询文章获取文章分类信息

2、查询文章分类获取文章信息

这个时候可以这样定义 models

Go 复制代码
package models

type Article struct { 
    Id int `json:"id"` 
    Title string `json:"title"` 
    Description int `json:"description"` 
    CateId string `json:"cate_id"` 
    State int `json:"state"` 
    ArticleCate ArticleCate `gorm:"foreignKey:CateId;references:Id"` 
}

func (Article) TableName() string { 
    return "article" 
}
Go 复制代码
package models

//ArticleCate 的结构体 
type ArticleCate struct { 
    Id int `json:"id"` 
    Title string `json:"title"` 
    State int `json:"state"` 
    Article []Article `gorm:"foreignKey:CateId"` 
}

func (ArticleCate) TableName() string { 
    return "article_cate" 
}

7.3.3 多对多

在 Gin 框架结合 GORM 的场景下,多对多关系是指两个模型(如"用户"和"角色")通过一个中间表相互关联,每个用户可以有多个角色,每个角色也可以属于多个用户;通过在 GORM 模型中使用 many2many 标签定义关联字段(例如 Roles []Role gorm:"many2many:user_roles"),并在控制器中利用 Preload` 预加载关联数据,Gin 即可在 API 响应中以嵌套 JSON 形式返回如"用户及其所有角色"的完整数据结构,从而轻松实现复杂业务模型间的查询与展示。

(1)定义学生 课程 学生课程表 model

如果想根据课程获取选学本门课程的学生,这个时候就在 Lesson 里面关联 Student

Lesson

Go 复制代码
package models

type Lesson struct { 
    Id int `json:"id"` 
    Name string `json:"name"` 
    Student []*Student `gorm:"many2many:lesson_student"` 
}

func (Lesson) TableName() string { 
    return "lesson" 
}

Student

Go 复制代码
package models

type Student struct {
    Id int 
    Number string 
    Password string 
    ClassId int 
    Name string 
    Lesson []*Lesson `gorm:"many2many:lesson_student"` 
}

func (Student) TableName() string { 
    return "student" 
}

LessonStudent

Go 复制代码
package models

type LessonStudent struct { 
    LessonId int 
    StudentId int 
}

func (LessonStudent) TableName() string { 
    return "lesson_student" 
}

(2)获取学生信息 以及课程信息

Go 复制代码
studentList := []models.Student{} 
models.DB.Find(&studentList) 
c.JSON(http.StatusOK, studentList) 
lessonList := []models.Lesson{} 
models.DB.Find(&lessonList) 
c.JSON(http.StatusOK, lessonList)

(3)查询学生信息的时候获取学生的选课信息

Go 复制代码
studentList := []models.Student{} 
models.DB.Preload("Lesson").Find(&studentList) 
c.JSON(http.StatusOK, studentList)

(4)查询张三选修了哪些课程

Go 复制代码
studentList := []models.Student{} 
models.DB.Preload("Lesson").Where("id=1").Find(&studentList) 
c.JSON(http.StatusOK, studentList)

(5)课程被哪些学生选修了

Go 复制代码
lessonList := []models.Lesson{} 
models.DB.Preload("Student").Find(&lessonList) 
c.JSON(http.StatusOK, lessonList)

(6)计算机网络被那些学生选修了

Go 复制代码
lessonList := []models.Lesson{} 
models.DB.Preload("Student").Where("id=1").Find(&lessonList) 
c.JSON(http.StatusOK, lessonList)

(7)查询数据指定条件

Go 复制代码
lessonList := []models.Lesson{} 
models.DB.Preload("Student").Offset(1).Limit(2).Find(&lessonList) 
c.JSON(http.StatusOK, lessonList)

(8)关联查询指定子集的筛选条件

如果张三被开除了,查询课程被哪些学生选修的时候要去掉张三。

用法:

Go 复制代码
access := []models.Access{} 
models.DB.Preload("AccessItem", "status=1").Order("sort desc").Where("module_id=?", 0).Find(&access) 
lessonList := []models.Lesson{} 
models.DB.Preload("Student", "id!=1").Find(&lessonList) 
c.JSON(http.StatusOK, lessonList)
lessonList := []models.Lesson{} 
models.DB.Preload("Student", "id not in (1,2)").Find(&lessonList) 
c.JSON(http.StatusOK, lessonList)

(9)自定义 预加载 SQL

预加载 **内容:**https://gorm.io/zh_CN/docs/preload.html

查看课程被哪些学生选修,要求:学生 id 倒叙输出。

**注意:**需要引入 gorm.io/gorm 这个包

Go 复制代码
lessonList := []models.Lesson{}

models.DB.Preload("Student", func(db *gorm.DB) *gorm.DB { 
    return models.DB.Order("id DESC") 
}).Find(&lessonList)

c.JSON(http.StatusOK, lessonList)

essonList := []models.Lesson{} 
models.DB.Preload("Student", func(db *gorm.DB) *gorm.DB { 
    return models.DB.Where("id>3").Order("id DESC") 
}).Find(&lessonList)
c.JSON(http.StatusOK, lessonList)

7.4 GORM 中使用事务

GORM 中的事务用于保证一组数据库操作要么全部成功,要么全部失败,确保数据一致性。它提供了两种使用方式:一是通过 Transaction 函数自动处理,只需将业务逻辑封装在闭包中,任何错误都会自动回滚;二是通过 BeginCommitRollback 方法手动控制事务的边界。此外,GORM 还支持嵌套事务和保存点,允许在复杂场景下部分回滚,结合上下文和回调机制,为高并发和数据敏感型应用(如订单处理、资金转账)提供了可靠的支持。

7.4.1 禁用默认事务

为了确保数据一致性,GORM 会在事务里执行写入操作(创建、更新、删除)。如果没有这方面的要求,您可以在初始化时禁用它,这将获得大约 30%+ 性能提升。

Go 复制代码
package models 
import ( 
    "fmt"
    "gorm.io/driver/mysql" 
    "gorm.io/gorm" 
)

var DB *gorm.DB
var err error

func init() { 
    dsn := "root:123456@tcp(192.168.0.6:3306)/gin?charset=utf8mb4&parseTime=True&loc=Local"
    DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{ 
        SkipDefaultTransaction: true, 
    }) 
    
    DB.Debug() 
    if err != nil { 
        fmt.Println(err) 
    } 
}

GORM 默认会将单个的 create, update, delete 操作封装在事务内进行处理,以确保数据的完整性。

如果你想把多个 create, update, delete 操作作为一个原子操作,Transaction 就是用来完成这个的。

7.4.2 事务

**官网教程:**https://gorm.io/zh_CN/docs/transactions.html

(1)事务执行流程

事务执行流程是指将一组数据库操作视为一个逻辑工作单元的过程:首先使用 Begin 开启事务,随后顺序执行多个数据修改操作(如插入、更新、删除),期间所有操作都在同一数据库会话中临时生效;若所有操作均成功,则调用 Commit 将更改永久写入数据库;若任一操作失败,则调用 Rollback 撤销全部更改,使数据库恢复到事务开始前的状态,从而确保数据的原子性、一致性、隔离性和持久性(ACID)。在 GORM 中,既可以通过 Transaction 函数自动管理此流程,也可以通过 BeginCommitRollback 方法手动控制。

要在事务中执行一系列操作,通常您可以参照下面的流程来执行:

Go 复制代码
db.Transaction(func(tx *gorm.DB) error { 
    // 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db') 
    if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil { 
        // 返回任何错误都会回滚事务 
        return err
    } 
    if err := tx.Create(&Animal{Name: "Lion"}).Error; err != nil { 
        return err
    }
    
    // 返回 nil 提交事务 
    return nil
})

(2)事务(手动控制)

在 GORM 中手动控制事务是指开发者通过显式调用 Begin() 方法开启一个事务,获取事务对象 tx,然后使用该对象执行一系列数据库操作;在执行过程中,需要根据业务逻辑判断是否出现错误,若有错误则调用 tx.Rollback() 回滚事务,撤销所有更改,若全部成功则调用 tx.Commit() 提交事务,将更改持久化到数据库。这种手动控制方式相比自动模式(Transaction 函数)更为灵活,适用于需要跨多个函数传递事务、或根据复杂条件决定提交/回滚的场景,但同时也要求开发者必须正确处理提交和回滚,以避免死锁或数据不一致。

Go 复制代码
// 开启事务 
tx := db.Begin() 
// 在事务中做一些数据库操作 (这里应该使用 'tx' ,而不是 'db') 
tx.Create(...) 
// ... 
// 有错误时,手动调用事务的 Rollback() 
tx.Rollback() 
// 无错误时,手动调用事务的 Commit() 
tx.Commit()

(3)张三给李四转账

Go 复制代码
package admin 
import ( 
  "fmt" 
  "gindemo13/models" 
  "github.com/gin-gonic/gin" 
)

type TransitionController struct { 
    BaseController 
}

func (con TransitionController) Index(c *gin.Context) { 
    tx := models.DB.Begin() 
    defer func() { 
        if r := recover(); r != nil { 
            tx.Rollback() 
            con.error(c) 
        } 
    }()
    
    if err := tx.Error; err != nil { 
        fmt.Println(err) 
        con.error(c) 
    }
    
    // 张三账户减去 100 
    u1 := models.Bank{Id: 1} 
    tx.Find(&u1) 
    u1.Balance = u1.Balance - 100 
    if err := tx.Save(&u1).Error; err != nil { 
        tx.Rollback() 
        con.error(c) 
    }
    // panic("遇到了错误") 
    // 李四账户增加 100 
    u2 := models.Bank{Id: 2} 
    tx.Find(&u2) 
    u2.Balance = u2.Balance + 100 
    // panic("失败") 
    if err := tx.Save(&u2).Error; err != nil { 
        tx.Rollback() 
        con.error(c) 
    } 
    tx.Commit() 
    con.success(c) 
}

第八章:Gin 中使用 go-ini 加载. ini 配置文件

18.1、go-ini 介绍

go-ini 是 Go 语言中一款功能最强大、使用最方便且最流行的 INI 文件操作库 。它支持从文件、字节切片([]byte)等多种数据源加载配置,并提供了将配置值自动转换为 Go 原生类型、将配置直接映射到结构体(struct)等核心特性 。此外,它还具备处理多行值、读写注释、保留配置顺序以及操作父-子节(section)等高级功能 。

✨ 核心特性

  • 灵活加载 :支持从文件、[]byteio.ReadCloser 等多种数据源加载,并允许覆盖和追加配置 。

  • 类型增强 :自动将 INI 文件中的字符串值转换为 intbooltime.Time 等 Go 类型,并提供了 MustInt() 等便捷方法处理默认值 。

  • 结构体映射:可以将配置文件内容映射到预定义的 Go 结构体上,代码操作配置就像操作对象属性一样简单 。

  • 功能强大:支持多行值、读写注释、保持节和键的原始顺序、处理父-子节关系、候选值限制等高级特性 。

  • 易于安装 :使用标准的 go get 命令即可 :

Go 复制代码
# 使用稳定版本
go get gopkg.in/ini.v1

# 或使用最新的开发版本
go get github.com/go-ini/ini

快速上手示例

假设你有一个名为 config.ini 的配置文件:

Go 复制代码
[app]
name = MyWeb3App
port = 8080
debug = true

使用 go-ini 读取并映射到结构体的代码如下:

Go 复制代码
package main

import (
    "fmt"
    "gopkg.in/ini.v1"
)

// 定义与配置文件对应的结构体
type Config struct {
    AppName  string `ini:"name"`
    AppPort  int    `ini:"port"`
    Debug    bool   `ini:"debug"`
}

func main() {
    // 1. 加载配置文件
    cfg, err := ini.Load("config.ini")
    if err != nil {
        panic(err)
    }

    // 2. 方法一:直接获取值
    port := cfg.Section("app").Key("port").MustInt(8080)
    fmt.Printf("Port from direct access: %d\n", port)

    // 3. 方法二:映射到结构体(推荐)
    var config Config
    err = cfg.Section("app").MapTo(&config)
    if err != nil {
        panic(err)
    }
    fmt.Printf("Mapped config: %+v\n", config)
}

Github **地址:**https://github.com/go-ini/ini

**官方文档:**https://ini.unknwon.io/

18.2、go-ini 使用

1、新建 conf/app.ini

现在,我们编辑 my.ini 文件并输入以下内容

Go 复制代码
app_name = itying gin 
# possible values: DEBUG, INFO, WARNING, ERROR, FATAL 
log_level = DEBUG 
[mysql] 
ip = 192.168.0.6 
port = 3306 
user = root 
password = 123456 
database = gin[redis] 
ip = 127.0.0.1 
port = 6379

很好,接下来我们需要编写 main.go 文件来操作刚才创建的配置文件。

Go 复制代码
package main
import ( 
    "fmt" 
    "os" 
    "gopkg.in/ini.v1" 
)
 
func main() { 
    cfg, err := ini.Load("./conf/app.ini")
    
    if err != nil { 
        fmt.Printf("Fail to read file: %v", err) 
        os.Exit(1)
    }
    
    // 典型读取操作,默认分区可以使用空字符串表示
    fmt.Println("App Mode:", cfg.Section("").Key("app_name").String()) 
    fmt.Println("Data Path:", cfg.Section("mysql").Key("ip").String()) 
    
    // 差不多了,修改某个值然后进行保存 
    cfg.Section("").Key("app_name").SetValue("itying gin") 
    cfg.SaveTo("./conf/app.ini") 
}

18.3、从. ini 中读取 mysql 配置

Go 复制代码
package models
//https://gorm.io/zh_CN/docs/connecting_to_the_database.html 
import ( 
    "fmt" 
    "os" 
    "gopkg.in/ini.v1" 
    "gorm.io/driver/mysql" 
    "gorm.io/gorm" 
)

var DB *gorm.DB 
var err error

func init() { 
    cfg, err := ini.Load("./conf/app.ini") 
    if err != nil { 
        fmt.Printf("Fail to read file: %v", err) 
        os.Exit(1) 
    }

    ip := cfg.Section("mysql").Key("ip").String() 
    port := cfg.Section("mysql").Key("port").String() 
    user := cfg.Section("mysql").Key("user").String() 
    password := cfg.Section("mysql").Key("password").String() 
    database := cfg.Section("mysql").Key("database").String()
    dsn := fmt.Sprintf("%v:%v@tcp(%v:%v)/%v?charset=utf8mb4&parseTime=True&loc=Local", user, password, ip, port, database) 
    fmt.Println(dsn)
    
    DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{ 
        QueryFields: true, //打印 sql
        //SkipDefaultTransaction: true, //禁用事务
    })
    
    if err != nil {
        fmt.Println(err)
    } 
}
相关推荐
雷工笔记2 小时前
小笔记|常读常新
笔记
Mr.Cheng.2 小时前
【InternVL2-2B】MLLM内部架构学习笔记
笔记·学习·自然语言处理
悠哉悠哉愿意2 小时前
【物联网学习笔记】串口接收
笔记·单片机·嵌入式硬件·物联网·学习
左左右右左右摇晃2 小时前
TCP三次握手与四次挥手
笔记
AMoon丶2 小时前
Golang--锁
linux·开发语言·数据结构·后端·算法·golang·mutex
是孑然呀2 小时前
【笔记】openclaw+飞书多agent(非群聊方式)
笔记·飞书
鸽子一号2 小时前
c#之常用的字符串操作
笔记
Kiyra2 小时前
[特殊字符] LeetCode 做题笔记(二):678. 有效的括号字符串
笔记·算法·leetcode
LeeeX!3 小时前
玩转 3D 检测和分割(一):MMDetection3D 整体框架介绍
笔记·多模态感知