1 常用设计
1.1 单点登录:gin+cas认证
01.CAS认证客户端实现
a.背景
在使用Golang对接CAS认证时,发现网上的资料大多使用Golang的CAS客户端包:gopkg.in/cas.v2。
然而,在接入CAS服务器后,出现了不断跳转和重定向的问题,无法继续执行认证成功后的操作。
为了解决该问题,基于CAS认证原理,自行实现了一个CAS认证客户端。
b.定义响应结构体
a.结构体定义
首先,需要定义CAS认证成功后的响应结构体:
// model/cas.go
type CasServiceResponse struct {
XMLName xml.Name `xml:"serviceResponse"`
Data struct {
SFRZH string `xml:"user"`
Attributes struct {
Uid string `xml:"uid"`
UserName string `xml:"userName"`
} `xml:"attributes"`
} `xml:"authenticationSuccess"`
}
c.编写CAS认证逻辑
a.核心逻辑
// utils/cas.go
package utils
import (
"encoding/xml"
"errors"
"fmt"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"io"
"net/http"
"roomlive-go/global"
"roomlive-go/model/cas"
"roomlive-go/model/user"
"strings"
)
func IsAuthentication(w http.ResponseWriter, r *http.Request, casServerUrl string) (bool, *cas.CasServiceResponse) {
if !hasTicket(r) {
redirectToCasServer(w, r, casServerUrl)
return false, nil
}
localUrl := getLocalUrl(r)
ok, err, res := validateTicket(localUrl, casServerUrl)
global.SYSLOG.Debug("cas validateTicket", zap.Bool("ok", ok), zap.Error(err), zap.Any("res", res))
if !ok {
redirectToCasServer(w, r, casServerUrl)
return false, nil
}
global.SYSLOG.Info("user authenticated", zap.String("sfrzh", res.Data.SFRZH))
return true, res
}
func redirectToCasServer(w http.ResponseWriter, r *http.Request, casServerUrl string) {
casServerUrl = casServerUrl + "/login?service=" + getLocalUrl(r)
http.Redirect(w, r, casServerUrl, http.StatusFound)
}
func validateTicket(localUrl, casServerUrl string) (bool, error, *cas.CasServiceResponse) {
casServerUrl = casServerUrl + "/serviceValidate?service=" + localUrl
res, err := http.Get(casServerUrl)
if err != nil {
return false, err, nil
}
defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil {
return false, err, nil
}
casRes, err := ParseCasUserInfo(data)
if err != nil {
return false, err, nil
}
if casRes.Data.SFRZH == "" {
return false, errors.New("authentication failed"), nil
}
return true, nil, casRes
}
func getLocalUrl(r *http.Request) string {
scheme := "http://"
if r.TLS != nil {
scheme = "https://"
}
url := strings.Join([]string{scheme, r.Host, r.RequestURI}, "")
fmt.Printf("url: %v\n", url)
slice := strings.Split(url, "?")
if len(slice) > 1 {
localUrl := slice[0]
urlParamStr := ensureOneTicketParam(slice[1])
url = localUrl + "?" + urlParamStr
}
return url
}
func ensureOneTicketParam(urlParams string) string {
if len(urlParams) == 0 || !strings.Contains(urlParams, "ticket") {
return urlParams
}
sep := "&"
params := strings.Split(urlParams, sep)
newParams := ""
ticket := ""
for _, value := range params {
if strings.Contains(value, "ticket") {
ticket = value
continue
}
if len(newParams) == 0 {
newParams = value
} else {
newParams = newParams + sep + value
}
}
newParams = newParams + sep + ticket
return newParams
}
func getTicket(r *http.Request) string {
return r.FormValue("ticket")
}
func hasTicket(r *http.Request) bool {
t := getTicket(r)
return len(t) != 0
}
func ParseCasUserInfo(data []byte) (*cas.CasServiceResponse, error) {
var casResponse cas.CasServiceResponse
if err := xml.Unmarshal(data, &casResponse); err != nil {
return nil, err
}
return &casResponse, nil
}
func GetUser(c *gin.Context) (*user.User, error) {
if res, exists := c.Get("casResponse"); !exists {
return nil, errors.New("cas authentication failed")
} else {
casRes := res.(*cas.CasServiceResponse)
waitUser := &user.User{
UserName: casRes.Data.Attributes.UserName,
SFRZH: casRes.Data.SFRZH,
}
return waitUser, nil
}
}
d.在Gin中间件中应用
a.中间件应用
将CAS认证逻辑应用到Gin的中间件中:
func CASMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
isAuth, casResponse := utils.IsAuthentication(c.Writer, c.Request, utils.CASServer)
if !isAuth {
c.Abort()
return
}
c.Set("casResponse", casResponse)
c.Next()
return
}
}
e.总结
这里只根据CAS原理实现了一个基本的CAS客户端认证流程,包括了请求检查、重定向处理、票据验证和用户信息解析,并通过Gin中间件集成到了Web应用程序中。
1.3 middleware/recover.go
00.原版
package middleware
import (
"fmt"
"net/http"
"runtime/debug"
"github.com/QuantumNous/new-api/common"
"github.com/gin-gonic/gin"
)
// 变体思路:
// 1) 基础版:保持匿名函数工厂,直接返回 gin.HandlerFunc。
// 2) 命名函数版:提取独立 handler(如 relayPanicRecover),工厂返回命名函数,便于链式或函数式组合。
// 3) 配置版:工厂接受可选参数(日志输出函数、错误响应构造器),返回定制化 handler。
// 4) 链式适配版:作为中间件适配器,接受/返回下游 handler 或 builder,方便在自定义链路中插入。
// 5) 无状态纯函数版:移除包级依赖,纯输入输出,便于测试与函数式管线组合。
// 6) 钩子注入版:支持注入日志/报警/指标回调或响应格式化器,实现可插拔扩展。
func RelayPanicRecover() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
common.SysLog(fmt.Sprintf("panic detected: %v", err))
common.SysLog(fmt.Sprintf("stacktrace from panic: %s", string(debug.Stack())))
c.JSON(http.StatusInternalServerError, gin.H{
"error": gin.H{
"message": fmt.Sprintf("Panic detected, error: %v. Please submit a issue here: https://github.com/Calcium-Ion/new-api", err),
"type": "new_api_panic",
},
})
c.Abort()
}
}()
c.Next()
}
}
01.变体1: 基础版(匿名函数工厂),这是你提供的原始版本,也是最常见和直接的实现方式。
a.代码示例:
---
package middleware
import (
"fmt"
"net/http"
"runtime/debug"
"github.com/gin-gonic/gin"
// 假设 common 包存在,并提供了 SysLog 函数
// "your_project/pkg/common"
)
// 为了示例能独立运行,我们模拟一个 common.SysLog
var common = new(mockCommon)
type mockCommon struct{}
func (m *mockCommon) SysLog(msg string) {
fmt.Println(msg)
}
// RelayPanicRecover 是一个中间件工厂,返回一个用于恢复 panic 的 gin.HandlerFunc
func RelayPanicRecover() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
// 记录 panic 信息和堆栈
common.SysLog(fmt.Sprintf("panic detected: %v", err))
common.SysLog(fmt.Sprintf("stacktrace from panic: %s", string(debug.Stack())))
// 返回统一的错误响应
c.JSON(http.StatusInternalServerError, gin.H{
"error": gin.H{
"message": fmt.Sprintf("Panic detected, error: %v. Please submit a issue here: https://github.com/Calcium-Ion/new-api", err),
"type": "new_api_panic",
},
})
// 中断请求链
c.Abort()
}
}()
// 继续处理请求
c.Next()
}
}
// 使用示例
func main_variant1() {
router := gin.Default()
router.Use(RelayPanicRecover())
// ... 定义你的路由
}
---
b.代码解说:
a.优点
简洁明了,将逻辑封装在闭包内,易于理解。
b.缺点
恢复逻辑(func(c *gin.Context))是匿名的,不易在其他地方直接复用或测试。同时,硬编码了对 common.SysLog 的依赖。
02.变体2: 命名函数版,将处理逻辑提取到一个命名的函数中,使代码结构更清晰。
a.代码示例:
---
package middleware
import (
"fmt"
"net/http"
"runtime/debug"
"github.com/gin-gonic/gin"
)
// relayPanicRecoverHandler 是具体的中间件处理函数
func relayPanicRecoverHandler(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
common.SysLog(fmt.Sprintf("panic detected: %v", err))
common.SysLog(fmt.Sprintf("stacktrace from panic: %s", string(debug.Stack())))
c.JSON(http.StatusInternalServerError, gin.H{
"error": gin.H{
"message": fmt.Sprintf("Panic detected, error: %v. Please submit a issue here: https://github.com/Calcium-Ion/new-api", err),
"type": "new_api_panic",
},
})
c.Abort()
}
}()
c.Next()
}
// RelayPanicRecoverFactory 工厂函数现在只返回命名函数
func RelayPanicRecoverFactory() gin.HandlerFunc {
return relayPanicRecoverHandler
}
// 使用示例
func main_variant2() {
router := gin.Default()
// 使用工厂函数
router.Use(RelayPanicRecoverFactory())
// 或者直接使用命名函数(如果它符合 gin.HandlerFunc 类型)
// router.Use(relayPanicRecoverHandler)
}
---
b.代码解说:
a.优点
relayPanicRecoverHandler 是一个独立的、可导出的函数(如果需要),便于单元测试和直接引用。代码职责分离更清晰:工厂负责创建,处理函数负责执行。
b.缺点
仍然存在对 common.SysLog 的硬编码依赖。
03.变体3: 配置版,通过传递配置对象,使中间件的行为可定制。
a.代码示例:
---
package middleware
import (
"fmt"
"net/http"
"runtime/debug"
"github.com/gin-gonic/gin"
)
// Logger 定义了一个简单的日志接口
type Logger interface {
Printf(format string, v ...interface{})
}
// ErrorResponseBuilder 定义了如何根据错误构建响应
type ErrorResponseBuilder func(c *gin.Context, err interface{})
// PanicRecoverConfig 用于配置 panic 恢复中间件
type PanicRecoverConfig struct {
Logger Logger
ResponseBuilder ErrorResponseBuilder
}
// defaultResponseBuilder 是一个默认的响应构造器
func defaultResponseBuilder(c *gin.Context, err interface{}) {
c.JSON(http.StatusInternalServerError, gin.H{
"error": gin.H{
"message": fmt.Sprintf("Internal server error: %v", err),
"type": "server_panic",
},
})
}
// NewConfigurablePanicRecover 创建一个可配置的 panic 恢复中间件
func NewConfigurablePanicRecover(config PanicRecoverConfig) gin.HandlerFunc {
// 如果未提供 Logger,使用默认的 fmt.Printf
if config.Logger == nil {
config.Logger = &defaultLogger{}
}
// 如果未提供 ResponseBuilder,使用默认的
if config.ResponseBuilder == nil {
config.ResponseBuilder = defaultResponseBuilder
}
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
config.Logger.Printf("panic detected: %v\nstacktrace: %s", err, string(debug.Stack()))
config.ResponseBuilder(c, err)
c.Abort()
}
}()
c.Next()
}
}
// defaultLogger 实现了 Logger 接口
type defaultLogger struct{}
func (l *defaultLogger) Printf(format string, v ...interface{}) {
fmt.Printf(format+"\n", v...)
}
// 使用示例
func main_variant3() {
router := gin.Default()
// 使用自定义配置
config := PanicRecoverConfig{
Logger: &defaultLogger{},
ResponseBuilder: func(c *gin.Context, err interface{}) {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "msg": "系统出现意外,请联系管理员"})
},
}
router.Use(NewConfigurablePanicRecover(config))
}
---
b.代码解说:
a.优点
高度灵活。用户可以轻松替换日志实现(例如,从 fmt 换成 logrus 或 zap),并完全自定义错误响应的 JSON 结构,而无需修改中间件的源代码。
b.缺点
实现稍微复杂一些,需要定义额外的 Config 结构和接口。
04.变体4: 链式适配版(装饰器模式),创建一个函数,用于“装饰”或“包裹”另一个 gin.HandlerFunc,为其添加 panic 恢复能力。
a.代码示例:
---
package middleware
import (
"fmt"
"net/http"
"runtime/debug"
"github.com/gin-gonic/gin"
)
// AdaptWithPanicRecovery 是一个适配器,它接收一个 handler 并为其添加 panic 恢复功能
func AdaptWithPanicRecovery(handler gin.HandlerFunc) gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
common.SysLog(fmt.Sprintf("panic in handler detected: %v", err))
common.SysLog(fmt.Sprintf("stacktrace from panic: %s", string(debug.Stack())))
c.JSON(http.StatusInternalServerError, gin.H{"error": "panic occurred"})
c.Abort()
}
}()
// 调用被包裹的原始 handler
handler(c)
}
}
// 使用示例
func main_variant4() {
router := gin.Default()
// 假设我们有一个可能 panic 的 handler
mightPanicHandler := func(c *gin.Context) {
panic("something went wrong!")
}
// 使用适配器包裹这个 handler
safeHandler := AdaptWithPanicRecovery(mightPanicHandler)
// 注册包裹后的安全 handler
router.GET("/test-panic", safeHandler)
// 注意:这种模式通常用于单个路由,而不是全局 router.Use()。
// 在 router.Use() 中,panic 恢复中间件应该调用 c.Next() 而不是特定的 handler。
// 此处为了展示“装饰”特定函数的概念。
}
---
b.代码解说:
a.优点
清晰地体现了装饰器模式,非常适合在函数式编程风格中对特定处理函数进行功能增强,而不是全局应用。
b.缺点
c.Next() 不再被调用,这意味着这个适配器本身就是一个终点处理器(或者说它内部处理了调用链)。这与 Gin 的 router.Use() 期望的中间件行为(调用 c.Next() 以传递给下一个)不同,更适用于包裹独立的 HandlerFunc。
05.变体5: 无状态纯函数版,移除对全局或包级变量(如 common.SysLog)的依赖,所有依赖都通过参数传入。
a.代码示例:
---
package middleware
import (
"fmt"
"net/http"
"runtime/debug"
"github.com/gin-gonic/gin"
)
// LogFunc 定义了日志函数的签名
type LogFunc func(format string, args ...interface{})
// NewStatelessPanicRecover 创建一个无状态的 panic 恢复中间件
// 它显式地接收所有外部依赖(这里是日志函数)。
func NewStatelessPanicRecover(log LogFunc) gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
log("panic detected: %v", err)
log("stacktrace from panic: %s", string(debug.Stack()))
c.JSON(http.StatusInternalServerError, gin.H{
"error": gin.H{
"message": fmt.Sprintf("Panic detected, error: %v", err),
"type": "stateless_panic",
},
})
c.Abort()
}
}()
c.Next()
}
}
// 使用示例
func main_variant5() {
router := gin.Default()
// 准备一个日志函数,可以是任何符合 LogFunc 签名的函数
myLogger := func(format string, args ...interface{}) {
fmt.Printf("[MyCustomLogger] "+format+"\n", args...)
}
// 将日志函数作为依赖注入
router.Use(NewStatelessPanicRecover(myLogger))
}
---
b.代码解说:
a.优点
极易测试和复用。由于没有隐藏的依赖,你可以轻松地在单元测试中提供一个 mock 的日志函数来验证其行为。函数的行为完全由其输入决定,符合纯函数的思想。
b.缺点
如果依赖项很多,函数签名可能会变得很长。但对于只有一两个依赖项的情况,这是非常清晰的模式。
06.变体6: 钩子注入版,这是配置版的扩展,允许在 panic 发生时执行一系列自定义操作(钩子)。
a.代码示例:
---
package middleware
import (
"fmt"
"net/http"
"runtime/debug"
"github.com/gin-gonic/gin"
)
// OnPanicHook 定义了 panic 发生时触发的钩子函数签名
type OnPanicHook func(c *gin.Context, err interface{}, stack []byte)
// PanicRecoverHooks 包含了一系列可注入的钩子
type PanicRecoverHooks struct {
Hooks []OnPanicHook
}
// NewPanicRecoverWithHooks 创建一个支持钩子注入的 panic 恢复中间件
func NewPanicRecoverWithHooks(hooks PanicRecoverHooks) gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
stack := debug.Stack()
// 依次执行所有注入的钩子
for _, hook := range hooks.Hooks {
hook(c, err, stack)
}
// 默认响应
if !c.IsAborted() {
c.JSON(http.StatusInternalServerError, gin.H{"error": "server panic"})
c.Abort()
}
}
}()
c.Next()
}
}
// 使用示例
func main_variant6() {
router := gin.Default()
// 1. 定义一个日志钩子
logHook := func(c *gin.Context, err interface{}, stack []byte) {
fmt.Printf("[LogHook] Panic: %v\nStack: %s\n", err, string(stack))
}
// 2. 定义一个告警钩子(例如,发送到 Sentry 或企业微信)
alertHook := func(c *gin.Context, err interface{}, stack []byte) {
// 在这里实现发送告警的逻辑
fmt.Printf("[AlertHook] Sending alert for panic: %v\n", err)
}
// 3. 定义一个指标上报钩子
metricsHook := func(c *gin.Context, err interface{}, stack []byte) {
// 在这里实现 panic 次数+1 的监控指标上报
fmt.Println("[MetricsHook] Incrementing panic counter.")
}
// 创建并注入钩子
recoveryHooks := PanicRecoverHooks{
Hooks: []OnPanicHook{logHook, alertHook, metricsHook},
}
router.Use(NewPanicRecoverWithHooks(recoveryHooks))
}
---
b.代码解说:
a.优点
极高的可扩展性。你可以将日志、告警、指标、自定义响应等不同职责的逻辑完全解耦,实现为独立的钩子函数。在需要添加新功能(如新的告警渠道)时,只需添加一个新的钩子,而无需修改中间件核心代码,符合“开闭原则”。
b.缺点
实现最为复杂,但对于大型、高可维护性要求的项目来说,这种模式的价值非常高。
1.2 internal/web/middleware/logger.md
01.代码
a.middleware/logger.go
package middleware
import (
"fmt"
"time"
"github.com/gin-gonic/gin"
)
// Logger 日志中间件
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
// 开始时间
startTime := time.Now()
// 处理请求
c.Next()
// 结束时间
endTime := time.Now()
// 执行时间
latencyTime := endTime.Sub(startTime)
// 请求方式
reqMethod := c.Request.Method
// 请求路由
reqUri := c.Request.RequestURI
// 状态码
statusCode := c.Writer.Status()
// 请求IP
clientIP := c.ClientIP()
// 日志格式
fmt.Printf("[GIN] %s | %3d | %13v | %15s | %-7s %s\n",
endTime.Format("2006-01-02 15:04:05"),
statusCode,
latencyTime,
clientIP,
reqMethod,
reqUri,
)
}
}
02.Gin中间件开发笔记
a.中间件的标准签名
a.说明
Gin 中间件本质上是一个 gin.HandlerFunc 类型的函数。一个标准的、可复用的中间件通常被包裹在一个返回 gin.HandlerFunc 的函数中。
b.示例
// Logger() 是一个工厂函数,返回真正的中间件处理函数
func Logger() gin.HandlerFunc {
// 这个匿名函数才是中间件的核心
return func(c *gin.Context) {
// ... 逻辑
}
}
b.c.Next():核心流程控制
a.说明
c.Next() 是中间件的“分水岭”。
b.说明
c.Next() 之前的代码:在请求到达业务处理函数(Handler)之前执行。适合做身份验证、参数预处理等。
c.Next() 之后的代码:在业务处理函数执行完毕之后执行。适合做日志记录、响应数据处理、异常捕获等。
这个机制构成了 Gin 的“洋葱模型”,请求一层层进入,响应一层层返回。
c.gin.Context:数据和操作的载体
a.说明
*gin.Context (通常简写为 c) 是中间件中最重要的对象,它包含了所有与当前 HTTP 请求和响应相关的信息和方法。
b.获取请求信息:
1.HTTP方法: c.Request.Method
2.请求URI: c.Request.RequestURI
3.客户端IP: c.ClientIP()
c.获取响应信息:
1.HTTP状态码: c.Writer.Status() (注意:必须在 c.Next() 之后才能获取到最终状态)
d.控制流程:
1.c.Next(): 继续处理链。
2.c.Abort(): 中断处理链。
d.计算请求耗时
a.说明
通过在 c.Next() 前后分别记录时间,可以精确计算出整个请求(包括业务处理)的耗时。
b.示例
startTime := time.Now()
c.Next() // 执行业务逻辑
endTime := time.Now()
latencyTime := endTime.Sub(startTime) // 计算差值
e.如何使用中间件
a.说明
编写好的中间件需要注册到 Gin 引擎(Engine)或路由组(RouterGroup)中才能生效。
b.示例
// 创建一个 Gin 引擎
router := gin.Default()
// 注册为全局中间件,对所有路由生效
router.Use(middleware.Logger())
// 路由定义
router.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "pong"})
})
03.Gin基础语法开发笔记
a.包声明与导入
a.package: 每个 Go 文件都必须在开头使用 package 声明其所属的包。它是 Go 组织代码的基本单元。
package middleware
b.import: 用于导入其他包。可以使用圆括号 () 将多个导入语句组合在一起,提高可读性。
import (
"fmt" // 用于格式化 I/O
"time" // 提供时间相关功能
)
b.函数:定义、返回与高阶用法
a.函数定义
使用 func 关键字,格式为 func 函数名(参数列表) 返回值类型 { ... }。
b.高阶函数
Go 中的函数是“一等公民”,可以作为参数传递,也可以作为另一个函数的返回值。下面的 Logger 函数就返回了一个函数类型 (gin.HandlerFunc)。
-------------------------------------------------------------------------------------------------
// Logger 函数返回一个函数
func Logger() gin.HandlerFunc {
// ...
}
c.匿名函数与闭包
可以在代码中直接定义一个没有名字的函数,即匿名函数。当匿名函数被返回时,它可以访问并持有其外部作用域的变量,形成闭包。
-------------------------------------------------------------------------------------------------
return func(c *gin.Context) { // 这是一个匿名函数
// 函数体
}
c.变量声明与赋值
a.短变量声明 :=
这是在函数内部最常用的变量声明和初始化方式。它会根据右侧的值自动推断变量类型。
-------------------------------------------------------------------------------------------------
// Go 自动推断 startTime 的类型为 time.Time
startTime := time.Now()
b.限制
:= 只能在函数内部使用,不能用于包级别的变量声明。
d.结构体 (Struct) 的字段与方法调用
a.说明
Go 使用 . 操作符来访问结构体的字段或调用其方法。
即使变量是指针类型,Go 也允许直接使用 . 来访问,它会自动进行解引用。
b.示例
// 访问字段 (c 是 *gin.Context 指针)
reqMethod := c.Request.Method
// 调用方法 (endTime 是 time.Time 结构体)
latencyTime := endTime.Sub(startTime)
e.fmt.Printf 格式化输出
a.说明
fmt.Printf 是一个非常强大的格式化输出函数,它使用占位符(Verb)来格式化数据。
b.示例
%s: 输出字符串。
%d: 输出十进制整数。
%v: 按默认格式输出值。
可以在占位符中添加数字来控制宽度和对齐,如 %3d (宽度为3的整数)、%-7s (宽度为7,左对齐的字符串)。
f.Go 特有的时间格式化
a.说明
在 Go 中,格式化或解析时间必须使用一个固定的参考时间点:2006-01-02 15:04:05 (可以记忆为:1月2日3点4分5秒,2006年,时区-0700)。
你通过调整这个“模板”的布局来得到你想要的格式。
b.示例
// 将时间格式化为 "年-月-日 时:分:秒"
endTime.Format("2006-01-02 15:04:05")