9-Gin 中自定义 Model --[Gin 框架入门精讲与实战案例]
在 Gin 框架中自定义 Model 通常指的是定义你自己的数据结构,这些结构体(Structs)将用来表示数据库中的表、API 请求的参数或响应的数据格式。下面是如何在 Gin 中创建和使用自定义 Model 的基本步骤。
自定义 Model
定义结构体
首先,你需要定义一个或多个 Go 结构体来表示你的数据模型。例如:
package modelstype User struct {ID uint `json:"id" gorm:"primaryKey"`Name string `json:"name" binding:"required"`Email string `json:"email" binding:"required,email"`CreatedAt time.TimeUpdatedAt time.Time
}
在这个例子中,User
结构体包含了用户的基本信息,并且每个字段都有 JSON 标签用于 API 响应时的序列化,以及 GORM 标签用于数据库操作。binding
标签是用于验证请求数据的。
配置数据库连接
如果你打算将这些模型与数据库一起使用,你需要配置数据库连接。Gin 本身不处理数据库操作,但常常与 GORM 等 ORM 库一起使用。以下是一个简单的例子,说明如何设置 GORM 数据库连接:
package mainimport ("gorm.io/driver/sqlite""gorm.io/gorm""log"
)var db *gorm.DB
var err errorfunc init() {// 连接到 SQLite 数据库 (这里可以替换为其他数据库)db, err = gorm.Open(sqlite.Open("test.db"), &gorm.Config{})if err != nil {log.Fatal(err)}// 自动迁移模式,根据模型自动创建表db.AutoMigrate(&models.User{})
}
使用模型进行 CRUD 操作
接下来,你可以编写函数来进行创建、读取、更新和删除(CRUD)操作。例如,创建一个新的用户记录:
func CreateUser(user *models.User) (*models.User, error) {result := db.Create(user)if result.Error != nil {return nil, result.Error}return user, nil
}
将模型用于 HTTP 请求
最后,你可以将这些模型与 Gin 路由器结合使用,以处理来自客户端的 HTTP 请求。例如:
func RegisterUser(c *gin.Context) {var user models.User// 绑定和验证请求数据if err := c.ShouldBindJSON(&user); err != nil {c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})return}// 创建新用户newUser, err := CreateUser(&user)if err != nil {c.JSON(http.StatusInternalServerError, gin.H{"error": "无法创建用户"})return}// 返回创建的用户信息c.JSON(http.StatusOK, newUser)
}
以上就是如何在 Gin 中自定义 Model 的简要介绍。当然,实际项目可能会更复杂,涉及到更多的业务逻辑、错误处理等。
Model 里面封装公共的方法
在 Gin 中,如果你希望为 Model 封装公共的方法,可以通过定义方法或使用 Go 的组合特性来实现。这里有几个常见的模式可以用来封装模型的公共方法:
方法1:直接在结构体上定义方法
你可以直接在你的模型结构体上定义方法,这些方法可以直接访问和操作结构体的字段。例如:
package modelsimport ("gorm.io/gorm""time" // 引入 time 包以使用时间类型
)// User 代表系统中的用户实体。
// 它包含了用户的 ID、名称、电子邮件地址以及创建和更新的时间戳。
type User struct {ID uint `json:"id" gorm:"primaryKey"` // 用户的唯一标识符(主键)Name string `json:"name" binding:"required"` // 用户的名字,是必填字段Email string `json:"email" binding:"required,email"` // 用户的电子邮件地址,必须是有效的电子邮件格式且为必填CreatedAt time.Time // 用户记录创建的时间戳UpdatedAt time.Time // 用户记录最后更新的时间戳
}// Save 保存当前用户实例到数据库。
// 如果用户已存在,则更新现有记录;如果不存在,则插入新记录。
// 参数:
// - db: GORM 数据库连接实例
// 返回值:
// - error: 如果操作失败则返回错误信息,否则返回 nil 表示成功
func (u *User) Save(db *gorm.DB) error {return db.Save(u).Error // 使用 GORM 的 Save 方法来持久化用户数据,并检查是否有错误发生
}// Delete 删除当前用户实例。
// 参数:
// - db: GORM 数据库连接实例
// 返回值:
// - error: 如果操作失败则返回错误信息,否则返回 nil 表示成功
func (u *User) Delete(db *gorm.DB) error {return db.Delete(u).Error // 使用 GORM 的 Delete 方法来移除用户数据,并检查是否有错误发生
}
方法2:使用服务层
另一种方式是创建一个服务层(Service Layer),其中包含与特定模型相关的业务逻辑。这可以帮助你保持代码的整洁,并且更易于测试。
package servicesimport ("your_project/models""gorm.io/gorm"
)// UserService 提供了对用户模型的一系列操作方法。
// 它依赖于 GORM 数据库连接实例来进行数据库交互。
type UserService struct {DB *gorm.DB // 数据库连接实例,用于执行所有数据库操作
}// NewUserService 创建一个新的 UserService 实例。
// 参数:
// - db: GORM 数据库连接实例
// 返回值:
// - *UserService: 返回一个初始化好的 UserService 实例
func NewUserService(db *gorm.DB) *UserService {return &UserService{DB: db}
}// CreateUser 在数据库中创建一个新的用户记录。
// 参数:
// - user: 指向 models.User 的指针,包含了要保存到数据库的新用户的详情
// 返回值:
// - error: 如果创建过程中出现问题,则返回错误信息;否则返回 nil 表示成功
func (us *UserService) CreateUser(user *models.User) error {return us.DB.Create(user).Error // 使用 GORM 的 Create 方法来插入新的用户数据,并检查是否有错误发生
}// GetUserByID 根据提供的 ID 获取用户信息。
// 参数:
// - id: 用户的唯一标识符(主键)
// 返回值:
// - *models.User: 包含查询结果的 User 结构体指针,如果未找到则为 nil
// - error: 如果查询过程中出现问题,则返回错误信息;否则返回 nil 表示成功
func (us *UserService) GetUserByID(id uint) (*models.User, error) {var user models.User// 使用 GORM 的 First 方法根据主键查找用户,如果找不到或发生错误则返回相应的错误if err := us.DB.First(&user, id).Error; err != nil {return nil, err}return &user, nil // 成功找到用户时,返回该用户的指针
}
方法3:使用接口和组合
如果你想使你的模型更加灵活,你可以定义接口并在其他类型中实现这些接口,或者通过组合来共享行为。这种方法对于需要跨多个模型共享相同行为的情况特别有用。
package modelsimport ("gorm.io/gorm"
)// Entity 定义了一个接口,表示所有实体应该具有的基本方法。
// 这个接口可以被任何需要共享 ID 行为的模型实现。
type Entity interface {// GetID 返回实体的唯一标识符(主键)。GetID() uint// SetID 设置实体的唯一标识符(主键)。SetID(uint)
}// BaseEntity 是一个基础结构体,包含了所有实体共有的字段和方法。
// 它实现了 Entity 接口,并提供了一个默认的 ID 字段。
type BaseEntity struct {ID uint `json:"id" gorm:"primaryKey"` // 实体的唯一标识符(主键),用于数据库中的记录识别
}// GetID 返回当前实体的 ID 值。
func (b *BaseEntity) GetID() uint {return b.ID
}// SetID 设置当前实体的 ID 值。
func (b *BaseEntity) SetID(id uint) {b.ID = id
}// User 继承了 BaseEntity 结构体,因此它自动获得了 ID 字段及其方法。
// 此外,User 结构体还包含额外的字段,如 Name 和 Email,
// 用于存储用户的具体信息。
type User struct {BaseEntity // 匿名字段,使得 User 拥有 BaseEntity 的所有字段和方法Name string `json:"name" binding:"required"` // 用户的名字,是必填字段Email string `json:"email" binding:"required,email"` // 用户的电子邮件地址,必须是有效的电子邮件格式且为必填
}
在这个例子中,BaseEntity
包含了所有实体可能共有的字段和方法,而 User
继承了这些字段和方法。
选择哪种方式取决于你的项目需求和个人偏好。通常来说,将业务逻辑放在服务层是一个不错的选择,因为它使得代码更模块化、可维护和可测试。同时,直接在模型上定义方法也可以简化一些基本的操作。
控制器中调用 Model
在 Gin 框架中,控制器(Controller)是处理 HTTP 请求和响应的地方。通常情况下,控制器会调用 Model 来执行业务逻辑或与数据库进行交互。下面是一个完整的例子,展示了如何在控制器中调用 Model。
假设我们已经有了 User
模型和一个 UserService
服务层,现在我们要创建一个控制器来处理用户的创建和获取请求。
定义路由和控制器
首先,在你的主程序文件(如 main.go
)中设置 Gin 路由,并将这些路由映射到控制器方法:
package mainimport ("your_project/controllers""github.com/gin-gonic/gin"
)func main() {r := gin.Default()// 定义用户相关路由userRoutes := r.Group("/users"){userRoutes.POST("", controllers.CreateUser)userRoutes.GET("/:id", controllers.GetUserByID)}// 启动服务器r.Run(":8080")
}
创建控制器
接下来,在 controllers
包中创建控制器函数,这些函数将调用 UserService
中的方法来处理业务逻辑:
package controllersimport ("net/http""your_project/models""your_project/services""github.com/gin-gonic/gin"
)// CreateUser 控制器用于处理创建新用户的 POST 请求。
// 它解析请求体中的 JSON 数据,调用 UserService 来创建用户,
// 并返回新创建的用户信息或错误。
func CreateUser(c *gin.Context) {var newUser models.Userif err := c.ShouldBindJSON(&newUser); err != nil {c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})return}userService := services.NewUserService(services.DB) // 假设 DB 已经被初始化并赋值给 services.DBif err := userService.CreateUser(&newUser); err != nil {c.JSON(http.StatusInternalServerError, gin.H{"error": "无法创建用户"})return}c.JSON(http.StatusCreated, newUser)
}// GetUserByID 控制器用于处理通过 ID 获取用户的 GET 请求。
// 它从 URL 参数中提取用户 ID,调用 UserService 来获取用户,
// 并返回用户信息或错误。
func GetUserByID(c *gin.Context) {id := c.Param("id")userId, err := strconv.ParseUint(id, 10, 64)if err != nil || userId == 0 {c.JSON(http.StatusBadRequest, gin.H{"error": "无效的用户 ID"})return}userService := services.NewUserService(services.DB) // 假设 DB 已经被初始化并赋值给 services.DBuser, err := userService.GetUserByID(uint(userId))if err != nil {c.JSON(http.StatusNotFound, gin.H{"error": "用户未找到"})return}c.JSON(http.StatusOK, user)
}调用 Model 注册全局模板函数
在这个例子中,我们做了以下几件事情:
- 定义了两个控制器函数:
CreateUser
和GetUserByID
,它们分别处理创建新用户和根据 ID 获取用户的请求。 - 解析了 HTTP 请求:使用
c.ShouldBindJSON
方法解析传入的 JSON 数据。 - 调用了服务层:创建了一个
UserService
实例,并调用了它的方法来执行具体的业务逻辑。 - 处理了响应:根据操作的结果返回适当的 HTTP 状态码和响应体。
确保在实际应用中,你已经正确设置了数据库连接,并且在适当的地方初始化了 services.DB
。这可以通过依赖注入或其他方式来实现,以保持代码的整洁和可测试性。
调用 Model 注册全局模板函数
在 Gin 中,如果你想注册全局模板函数以便可以在所有的 HTML 模板中使用这些函数,你可以通过 gin.Engine
的 HTML
渲染器来实现。通常情况下,你会在应用启动时设置这些全局模板函数,这样它们就可以被所有渲染的模板所访问。
下面是一个例子,展示了如何定义和注册全局模板函数,并在控制器中调用 Model 来传递数据给模板:
1. 定义全局模板函数
首先,在你的主程序文件(如 main.go
)中设置全局模板函数:
package mainimport ("html/template""net/http""your_project/models""github.com/gin-gonic/gin"
)func init() {// 注册全局模板函数gin.DefaultRenderer().(*renderer.Renderer).Funcs(template.FuncMap{"formatDate": func(t time.Time) string {return t.Format("2006-01-02")},"getUserByID": func(id uint) *models.User {// 这里应该有一个适当的数据库连接和服务层来获取用户信息userService := services.NewUserService(services.DB)user, _ := userService.GetUserByID(id)return user},// 可以添加更多的模板函数...})
}
请注意,gin.DefaultRenderer()
可能不是最新的 API 调用方式;具体取决于你使用的 Gin 版本。对于较新的版本,你可能需要直接操作 gin.Engine
的 HTMLRender
属性。
2. 使用自定义渲染器(推荐)
为了确保兼容性和更好的控制,推荐创建一个自定义的渲染器实例并将其配置为 Gin 的默认渲染器。这可以让你更灵活地管理模板路径、布局等。
package mainimport ("html/template""net/http""your_project/models""github.com/gin-gonic/gin""github.com/gin-gonic/gin/render"
)func main() {r := gin.Default()// 创建一个新的自定义渲染器实例renderer := render.HTML{Templates: template.Must(template.New("").Funcs(template.FuncMap{"formatDate": func(t time.Time) string {return t.Format("2006-01-02")},"getUserByID": func(id uint) *models.User {userService := services.NewUserService(services.DB)user, _ := userService.GetUserByID(id)return user},// 可以添加更多的模板函数...}).ParseGlob("templates/*.tmpl")),}// 设置自定义渲染器为默认渲染器r.HTMLRender = renderer// 定义路由和控制器逻辑...r.GET("/user/:id", func(c *gin.Context) {id := c.Param("id")userId, err := strconv.ParseUint(id, 10, 64)if err != nil || userId == 0 {c.JSON(http.StatusBadRequest, gin.H{"error": "无效的用户 ID"})return}c.HTML(http.StatusOK, "user.tmpl", gin.H{"userID": userId,})})// 启动服务器r.Run(":8080")
}
在这个例子中,我们做了以下几件事情:
- 定义了全局模板函数:包括格式化日期和根据 ID 获取用户的函数。
- 创建了自定义渲染器:使用
template.Must
和ParseGlob
来加载所有模板文件,并设置了模板函数。 - 设置了自定义渲染器:将自定义渲染器设置为 Gin 的默认 HTML 渲染器。
- 定义了一个路由:该路由处理
/user/:id
请求,并调用了c.HTML
方法来渲染模板,同时传递了必要的数据。
请确保替换 "templates/*.tmpl"
为实际模板文件的路径模式,并且确保 services.DB
已经被正确初始化。此外,根据你的项目结构和需求调整包名和导入路径。
相关文章:

9-Gin 中自定义 Model --[Gin 框架入门精讲与实战案例]
在 Gin 框架中自定义 Model 通常指的是定义你自己的数据结构,这些结构体(Structs)将用来表示数据库中的表、API 请求的参数或响应的数据格式。下面是如何在 Gin 中创建和使用自定义 Model 的基本步骤。 自定义 Model 定义结构体 首先&…...

【VBA】EXCEL - VBA 创建 Sheet 表的 6 种方法,以及注意事项
目录 1. 创建一个新工作表,并将其添加到工作簿的末尾 2. 创建一个新工作表,并命名它 3. 创建一个新工作表,并将其插入到指定位置 4. 检查是否已有同名工作表,避免重复创建 5. 创建多个工作表 6. 基于现有模板创建新工作表 …...

数据库中,group by 和partition by:数据分组和数据分区的区别
数据库中,group by 和partition by:数据分组和数据分区的区别 在大规模数据处理和分析的场景中,对数据进行分区和分组处理是非常常见的场景。 为了实现这一操作,在一些主流的关系型数据库管理系统中,提供了group by 和…...

【linux学习指南】Ext系列文件系统(四)路径分区链接
文章目录 🌠⽬录与⽂件名🌠路径解析🌠路径缓存🌠挂载分区🌉 ⽂件系统总结 🌠软硬连接🌉 硬链接🌉 软链接🌉 软硬连接对⽐🌉软硬连接的⽤途: &…...

深度学习中的参数初始化
深度学习中的参数初始化主要是指初始化神经网络中的权重和偏置。权重和偏置通常分开初始化,偏置通常初始化为零或较小的常数值。 没有一种万能的初始化技术,因为最佳初始化可能因具体架构和要解决的问题而异。因此,尝试不同的初始化技术以了解…...

wpf 基于Behavior库 的行为模块
Microsoft.Xaml.Behaviors 是一个用于WPF(Windows Presentation Foundation)的行为库,它的主要作用是允许开发者在不修改控件源代码的情况下,为控件添加自定义的行为和交互逻辑。行为库的核心思想是通过定义可重用的行为组件&…...

【每日学点鸿蒙知识】导入cardEmulation、自定义装饰器、CallState状态码顺序、kv配置、签名文件配置
1、HarmonyOS 无法导入cardEmulation? 在工程entry mudule里的index.ets文件里导入cardEmulation失败 可以按照下面方式添加SystemCapability;在src/main/syscap.json(此文件需要手动创建)中添加如下内容 {"devices": {"gen…...

【SpringMVC】REST 风格
REST(Representational State Transfer,表现形式状态转换)是一种访问网络资源的格式。传统的资源描述方式通常如下: http://localhost/user/getById?id1http://localhost/user/saveUser 而 REST 风格的描述则更简洁:…...

IDEA修改编译版本
目录 一、序言 二、修改maven配置 1.修改 2.代码 三、pom文件配置 1.修改 2.代码 3.问题 一、序言 有两种方法可以帮助大家解决IDEA每次刷新maven的pom配置时,会发生发行源版本不正常的报错。个人推荐第二种,原因:第二种你刷新maven后…...

SkyWalking Agent 配置 Spring Cloud Gateway 插件解决日志错误
SkyWalking Agent 配置 Spring Cloud Gateway 插件解决日志错误 IDEA中启动网管时,需要配置VM启动参数,格式如下: # 配置 SkyWalking Agent 启动参数,以便将网关服务的性能数据上报到 SkyWalking 服务器。 -javaagent:/path/to/sk…...

canvas+fabric实现时间刻度尺(一)
前言 需求:显示一个时间刻度尺,鼠标移动会显示当前时间 技术:我们采用canvasfabric进行实现 效果 实现 1.创建canvas(设置宽高)设为全局变量 2.引入fabric包 3.画时间刻度尺(长方形横线) …...

傲雷亮相2024中国时尚体育季(珠海站),展现户外移动照明风采
2024年12月28-29日,2024中国时尚体育季(珠海站)国家级轮滑比赛在珠海金山体育公园成功举办。作为户外创新型移动照明领域的领导品牌,傲雷受邀参加了本次珠海金湾运动生活嘉年华的展览单元,与众多户外运动品牌同台展示。…...

YOLOv10-1.1部分代码阅读笔记-block.py
block.py ultralytics\nn\modules\block.py 目录 block.py 1.所需的库和模块 2.class DFL(nn.Module): 3.class Proto(nn.Module): 4.class HGStem(nn.Module): 5.class HGBlock(nn.Module): 6.class SPP(nn.Module): 7.class SPPF(nn.Module): 8.class C1(nn…...

@RestControllerAdvice注解
RestControllerAdvice 是 Spring 4 引入的一个组合注解,它结合了 ControllerAdvice 和 ResponseBody,专门用于处理 RestController 类型的控制器中的全局异常、全局数据绑定和全局模型属性等问题。在 Spring Boot 中,RestControllerAdvice 通…...

Enum枚举类与静态变量和静态数组的区别
Enum枚举类与静态变量和静态数组的区别 组成结构Enum枚举类静态变量静态数组 组成结构的区别相同之处不同之处 用法使用相同之处不同之处 组成结构 先来看下Enum枚举类,静态变量,静态数组的初始化过程,以下面为例子: public enu…...

uniapp——微信小程序读取bin文件,解析文件的数据内容(三)
微信小程序读取bin文件内容 读取用户选择bin文件,并解析数据内容,分包发送给蓝牙设备; 文章目录 微信小程序读取bin文件内容读取文件读取内容返回格式 API文档: getFileSystemManager 关于App端读取bin文件,请查看&…...

SpringBoot集成ECDH密钥交换
简介 对称加解密算法都需要一把秘钥,但是很多情况下,互联网环境不适合传输这把对称密码,有密钥泄露的风险,为了解决这个问题ECDH密钥交换应运而生 EC:Elliptic Curve——椭圆曲线,生成密钥的方法 DH&…...

python文件操作相关(excel)
python文件操作相关(excel) 1. openpyxl 库openpyxl其他用法创建与删除操作单元格追加数据格式化单元格合并单元格插入图片公式打印设置保护工作表其他功能 2. pandas 库3. xlrd 和 xlwt 库4. xlsxwriter 库5. pyxlsb 库应用场景参考资料 在 Python 中&a…...

探索React与Microi吾码的完美结合:快速搭建项目,低代码便捷开发教程
一、摘要 在当今的数字化时代,软件开发就像是一场探险,每个开发者都是探险家,探索着代码的奥秘。React作为前端开发的领军框架,其组件化和高效的渲染机制为开发者提供了强大的工具。而Microi吾码低代码平台的出现,则为…...

【面试系列】深入浅出 Spring Boot
熟悉SpringBoot,对常用注解、自动装配原理、Jar启动流程、自定义Starter有一定的理解; 面试题 Spring Boot 的核心注解是哪个?它主要由哪几个注解组成的?Spring Boot的自动配置原理是什么?你如何理解 Spring Boot 配置…...

@colyseus/social 模块详解
@colyseus/social 模块介绍 @colyseus/social 是一个适用于 Colyseus 游戏框架的扩展模块,提供了社交功能的支持,帮助开发者在多人游戏中快速实现玩家之间的社交互动。它主要提供了玩家账户管理、好友系统、好友请求、组队和聊天功能等,旨在简化游戏中社交功能的实现。 核心…...

石岩路边理发好去处
周末带娃去罗租公园玩,罗租公园旁边就是百佳华和如意豪庭小区,发现如意豪庭小区对面挺多路边理发摊点 理发摊点聚焦在这里的原因是刚好前面城管来了暂时避避,例如还有一个阿姨剪到一半就跟着过来。这里的城管只是拍了一处没有摊位的地方&…...

ROS 2中的DDS中间件
文章目录 一、简介二、默认支持的 DDS (Data Distribution Service) 实现三、切换DDS实现小结 一、简介 中间件是一个软件层,通常用于连接不同的应用程序、服务或系统,以便它们能够相互通信和交换数据。中间件并不直接向用户暴露,而是在系统…...

「下载」智慧文旅运营综合平台解决方案:整体架构,核心功能设计
智慧文旅运营综合平台,旨在通过集成大数据、云计算、物联网、人工智能等先进技术,为景区、旅游企业及相关管理机构提供一站式的智慧化运营服务。 智慧文旅运营综合平台不仅能够提升游客的游览体验,还能帮助景区管理者实现资源的优化配置和业务…...

NVR小程序接入平台EasyNVR使用FFmpeg取流时提示错误是什么原因呢?
在视频监控系统中,FFmpeg常用于从各种源(如摄像头、文件、网络流等)获取流媒体数据,这个过程通常称为“取流”。 在EasyNVR平台中,使用FFmpeg取流是一种常见的操作。FFmpeg作为一款强大的开源多媒体处理工具ÿ…...

计算机因进程结束导致白屏
问题场景: 计算机卡顿利用(右击计算机桌面底部任务栏->打开任务管理器->结束任务->或进程被意外结束导致白屏) 问题描述 白屏 原因分析: 在结束进程时,导致 文件资源管理器 进程崩溃。 解决方案…...
OpenGL入门最后一章观察矩阵(照相机)
前面的一篇文章笔者向大家介绍了模型变化矩阵,投影矩阵。现在只剩下最后一个观察矩阵没有和大家讲了。此片文章就为大家介绍OpenGL入门篇的最后一个内容。 观察矩阵 前面的篇章当中,我们看到了即使没有观察矩阵,我们也能对绘制出来的模型有一…...

ES6中定义私有属性详解
在ES6中,定义私有属性的方式相对传统的JavaScript有所不同。ES6并没有提供直接的语法来定义私有属性,但可以通过几种方法间接实现私有属性。 1. 使用Symbol来模拟私有属性 Symbol是一种新的数据类型,可以作为对象的键,并且它的值…...

工业5G路由器让无人机数据传输 “飞” 起来
无人机上搭载5G通信模块,该模块与工业5G路由器通过5G网络建立连接。无人机的飞控系统、传感器以及摄像头等设备采集到的数据,如飞行姿态、高度、速度、环境图像、温度湿度等,经过编码、加密、调制等处理后转换为适合5G网络传输的信号形式。 …...

面试经典150题——滑动窗口
文章目录 1、长度最小的子数组1.1 题目链接1.2 题目描述1.3 解题代码1.4 解题思路 2、无重复字符的最长子串2.1 题目链接2.2 题目描述2.3 解题代码2.4 解题思路 3、串联所有单词的子串3.1 题目链接3.2 题目描述3.3 解题代码3.4 解题思路 4、最小覆盖子串4.1 题目链接4.2 题目描…...