go-gin框架二次封装go-sword详解(路由+中间件+开启关闭服务篇)

main方法中注册路由
// 5. 注册路由
	run.Run()

run方法中,转到router.Setup文件中,下面具体展开进行讲解,

r := router.Setup(config.ApplicationConfig)

传入存储配置信息的结构体,返回一个引擎。在这一步对其进行配置

// Setup 路由设置
func Setup(cfg *config.Application) *gin.Engine {
}

gin.ReleaseMode:三种mode分别对应了不同的场景。在我们开发调试过程中,使用debug模式就可以了。在上线的时候,一定要选择release模式。而test可以用在测试场景中

	if cfg.Mode == string(utils.ModeProd) {
		gin.SetMode(gin.ReleaseMode)
	}

r := gin.New() // 新建一个没有任何默认中间件的路由
重写下面三个中间件:middleware.Cors(), middleware.GinLogger(), middleware.Sentinel(),中间件的具体内容展示:

	r := gin.New()
	r.Use(middleware.Cors(), middleware.GinLogger(), middleware.Sentinel(200))
	r.Static(cfg.StaticFileUrl, cfg.StaticPath)

Cors()函数解决的问题是跨域请求,返回一个方法。
Access-Control-Allow-Origin:该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求
Access-Control-Allow-Headers:如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。
Access-Control-Allow-Methods:该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。
Access-Control-Expose-Headers:该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定
Access-Control-Allow-Credentials:该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。(接口依赖Cookie来完成一些处理(比如登录态))
Header is a intelligent shortcut for c.Writer.Header().Set(key, value).

// Cors 跨域
func Cors() gin.HandlerFunc {
	return func(c *gin.Context) {
		method := c.Request.Method
		c.Header("Access-Control-Allow-Origin", "*")
		c.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token, Authorization, Token")
		c.Header("Access-Control-Allow-Methods", "PUT, DELETE, POST, GET, OPTIONS")
		c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type")
		c.Header("Access-Control-Allow-Credentials", "true")

		//放行所有OPTIONS方法
		if method == "OPTIONS" {
			c.AbortWithStatus(http.StatusNoContent)
		}
		// 处理请求
		c.Next()
	}
}

GinLogger中间件的作用是接收gin框架默认的日志
记录错误信息:自定义你所要写入的内容。
下面各变量意思极其字面化,没有什么需要特别讲解的。

// GinLogger 接收gin框架默认的日志
func GinLogger() gin.HandlerFunc {
	return func(c *gin.Context) {
		start := time.Now()
		path := c.Request.URL.Path
		query := c.Request.URL.RawQuery
		c.Next()

		cost := time.Since(start)
		status := c.Writer.Status()
		method := c.Request.Method
		ip := c.ClientIP()
		errString := c.Errors.ByType(gin.ErrorTypePrivate).String()
		userAgent := c.Request.UserAgent()
		zap.L().Info(path,
			zap.Int("status", status),
			zap.String("method", method),
			zap.String("path", path),
			zap.String("query", query),
			zap.String("ip", ip),
			zap.String("user-agent", userAgent),
			zap.String("errors", errString),
			zap.Duration("cost", cost),
		)
	}
}

限流作用是:减轻服务器压力
Sentinel Go针对埋点资源配置相应的规则,来达到流量控制的效果。目前 Sentinel Golang 支持流控规则 (FlowRule) 和系统保护规则 (SystemRule)。
strategy字段: 判断的根据是资源自身,还是根据其它关联资源 (refResource),还是根据链路入口 根据资源本身
请求的间隔控制在 1000/200=5 ms
system.LoadRules:LoadRules loads given system rules to the rule manager, while all previous rules will be replaced.
下面这种方式是针对埋点资源配置相应的规则,通过系统保护规则 (SystemRule)来达到流量控制的效果。这种系统自适应算法的效果是一个“兜底”的效果。对于不是应用本身造成的负载高的情况(如其它进程导致的不稳定的情况),效果不明显。

// Sentinel 限流
func Sentinel(triggerCount float64) gin.HandlerFunc {
	if _, err := system.LoadRules([]*system.Rule{
		{
			MetricType:   system.InboundQPS,
			TriggerCount: triggerCount,
			Strategy:     system.BBR,
		},
	}); err != nil {
		zap.L().Fatal("Unexpected error", zap.Error(err))
	}
	return sentinelPlugin.SentinelMiddleware()
}

读取静态文件的封装方法,传入静态文件路由和静态文件目录。

r.Static(cfg.StaticFileUrl, cfg.StaticPath)

c.String():String writes the given string into the response body.

r.GET("/ping", func(c *gin.Context) {
		c.String(http.StatusOK, "ok")
	})

重写Recovery中间件
中间件属于用户自己的钩子(Hook)函数。必须是gin.HandlerFunc类型
recover:
1、内建函数
2、用来控制一个goroutine的panicking行为,捕获panic,从而影响应用的行为
3、一般的调用建议
a). 在defer函数中,通过recever来终止一个gojroutine的panicking过程,从而恢复正常代码的执行
b). 可以获取通过panic传递的error

OpError是经常被net包的函数返回的错误类型。它描述了该错误的操作、网络类型和网络地址
// Op是出现错误的操作,如"read"或"write" Op string
// Net是错误所在的网络类型,如"tcp"或"udp6" Net string
// Addr是出现错误的网络地址 Addr Addr
// Err是操作中出现的错误 Err error
然后判断错误类型:ne.Err.(*os.SyscallError)
strings.ToLower(se.Error()):将错误字母映射为小写
strings.Contains(s, subStr):判断后者是否在前者里面

// GinRecovery recover掉项目可能出现的panic,并使用zap记录相关日志
func GinRecovery(stack bool) gin.HandlerFunc {
	return func(c *gin.Context) {
		defer func() {
			if err := recover(); err != nil {
				// Check for a broken connection, as it is not really a
				// condition that warrants a panic stack trace.
				var brokenPipe bool
				if ne, ok := err.(*net.OpError); ok {
					if se, ok := ne.Err.(*os.SyscallError); ok {
						if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
							brokenPipe = true
						}
					}
				}

				httpRequest, _ := httputil.DumpRequest(c.Request, false)
				if brokenPipe {
					zap.L().Error(c.Request.URL.Path,
						zap.Any("error", err),
						zap.String("request", string(httpRequest)),
					)
					// If the connection is dead, we can't write a status to it.
					c.Error(err.(error)) // nolint: errcheck
					c.Abort()
					return
				}

				if stack {
					zap.L().Error("[Recovery from panic]",
						zap.Any("error", err),
						zap.String("request", string(httpRequest)),
						zap.String("stack", string(debug.Stack())),
					)
				} else {
					zap.L().Error("[Recovery from panic]",
						zap.Any("error", err),
						zap.String("request", string(httpRequest)),
					)
				}
				c.AbortWithStatus(http.StatusInternalServerError)
			}
		}()
		c.Next()
	}
}
中间件返回了一个接收gin.Contnext参数的方法
获取到token之后对其进行判断,如果是空字符串则返回错误,并直接退出同时中断函数调用链然后返回return
拿到正确的token之后按照空格分割,进行判断,如果长度不为二或者第一个元素不为header则终止
拿到tokenString之后去redi数据库
	get(key):返回数据库中名称为key的string的value
	set(key, value):给数据库中名称为key的string赋予值value
	查询缓存较为高级:首先通过config.JwtConfig.RedisHeader之后通过viper获取到配置文件里面的的参数然后使用"-"进行分割,然后加上具体的Token,之后就去数据库查找,如果有错误则说明token解析失败,终止回调链条并返回
	并将用户基本信息放到上下文里,key为CtxUserOnline    = "user_online"
	之后解析token获取用户的最基本信息,这里调用pkg目录下的jwt文件
		首先定义一个结构体用于存储信息,包括:用户id+用户name+jwt.StandardClaims(jwt包自带的jwt.StandardClaims只包含了官方字段)
		生成token:
			创建一个我们自己的	声明
				其中的ExpiresAt:表示过期时间
					time.Duration()表示的是持续时间,我们通过viper获取我们设置的默认值3600然后乘以time.Second,将其转换为纳秒数,在之后需要调用Unix()方法,将这个时间返回为Unix时间(自1970.1.1以来经过的秒数)作为过期时间,即该token一小时后将会过期。
				其中的Issuer:表示签发人	
			然后使用指定的签名算法生成token(通常使用SigningMethodHS256)
			token.SignedString([]byte(config.JwtConfig.Secret))
			加密算法是HS256时,这里的SignedString必须是[]byte()类型,加盐密
			返回出去生成的token
		解析toekn:
			需要token字符串和初始化后的结构体(存储信息)
			然后传入并进行解析,切记要进行有效性检验。
回到jwt中间件,拿到token之后。		
	为UserMessage结构体分配空间,之后将取出的信息存到里面,再将用户信息和id分别存到上下文中:
		c.Set(api.CtxUserIdAndName, r)
		c.Set(api.CtxUserIDKey, mc.UserID)

jwt文件

// JWTAuthMiddleware 基于JWT的认证中间件
func JWTAuthMiddleware() func(c *gin.Context) {
	return func(c *gin.Context) {
		// 客户端携带Token有三种方式 1.放在请求头 2.放在请求体 3.放在URI
		// 这里假设Token放在Header的Authorization中,并使用Bearer开头
		// 这里的具体实现方式要依据你的实际业务情况决定
		authHeader := c.Request.Header.Get("Authorization")
		if authHeader == "" {
			app.ResponseError(c, app.CodeLoginExpire)
			c.Abort()
			return
		}
		// 按空格分割
		parts := strings.SplitN(authHeader, " ", 2)
		if !(len(parts) == 2 && parts[0] == config.JwtConfig.Header) {
			app.ResponseError(c, app.CodeInvalidToken)
			c.Abort()
			return
		}
		// parts[1]是获取到的tokenString,我们使用之前定义好的解析JWT的函数来解析它
		res, err := global.Rdb.Get(fmt.Sprintf("%s%s%s", config.JwtConfig.RedisHeader, "-", parts[1])).Result()
		if err != nil {
			zap.L().Error("token解析失败", zap.Error(err))
			app.ResponseError(c, app.CodeInvalidToken)
			c.Abort()
			return
		}
		c.Set(api.CtxUserOnline, res)

		// 将当前请求的user_id信息保存到请求的上下文c上
		mc, err := jwt.ParseToken(parts[1])
		if err != nil {
			c.Abort()
			return
		}
		r := new(api.UserMessage)
		r.UserId = mc.UserID
		r.Username = mc.Username
		c.Set(api.CtxUserIdAndName, r)
		c.Set(api.CtxUserIDKey, mc.UserID)
		c.Next() // 后续的处理函数可以用过c.Get("username")来获取当前请求的用户信息
	}
}
跳转回去 这里是jwt文件内容:生成与解析token
type MyClaims struct {
	UserID   int    `json:"user_id"`
	Username string `json:"username"`
	jwt.StandardClaims
}

// GenToken 生成JWT
func GenToken(userID int, username string) (string, error) {
	// 创建一个我们自己的声明
	c := MyClaims{
		userID,
		username, // 自定义字段
		jwt.StandardClaims{
			ExpiresAt: time.Now().Add(time.Duration(config.JwtConfig.Timeout) * time.Second).Unix(), // 过期时间
			Issuer:    "my-project",                                                                 // 签发人
		},
	}
	// 使用指定的签名方法创建签名对象
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
	// 使用指定的secret签名并获得完整的编码后的字符串token
	return token.SignedString([]byte(config.JwtConfig.Secret))
}

// ParseToken 解析JWT
func ParseToken(tokenString string) (*MyClaims, error) {
	// 解析token
	var mc = new(MyClaims)
	token, err := jwt.ParseWithClaims(tokenString, mc, func(token *jwt.Token) (i interface{}, err error) {
		return []byte(config.JwtConfig.Secret), nil
	})
	if err != nil {
		return nil, err
	}
	if token.Valid { // 校验token
		return mc, nil
	}
	return nil, errors.New("invalid token")
}
casbin权限检验中间件

首先初始化UserInfo结构体包含了一个用户的岗位/角色/部门信息
然后通过api.GetUserData()方法返回的参数就是UserInfo结构体实例
 步骤:先通过

func AuthCheckRole() gin.HandlerFunc {
	return func(c *gin.Context) {
		data := new(api.UserInfo)
		var role []string
		var err error
		userInfo, err := api.GetUserData(c)
		data = userInfo
		roles := data.Roles
		for _, v := range *roles {
			role = append(role, utils.IntToString(v.ID))
		}
		if err != nil {
			c.Abort()
			return
		}
		e, err := mycasbin.LoadPolicy()
		if err != nil {
			c.Abort()
			return
		}
		//检查权限
		//此处为多角色 要在做处理
		var res bool
		res, err = e.Enforce(role, c.Request.URL.Path, c.Request.Method)
		if err != nil {
			c.Abort()
			return
		}

		if res {
			c.Next()
		} else {
			c.JSON(http.StatusOK, gin.H{
				"code": 403,
				"msg":  "对不起,您没有该接口访问权限,请联系管理员",
			})
			c.Abort()
			return
		}
	}
}
type UserInfo struct {
	Jobs           *[]models.SysJob
	Roles          *[]models.SysRole
	MenuPermission *[]string
	Dept           *models.SysDept
	DataScopes     *[]int
}

// SysJob 岗位实体
type SysJob struct {
	*BaseModel
	Name     string `json:"name"`      //岗位名称
	Enabled  []byte `json:"enabled"`   //状态:1启用(默认)、0禁用
	JobSort  int    `json:"jobSort"`  //排序
	CreateBy int    `json:"create_by"` //创建者id
	UpdateBy int    `json:"update_by"` //更新者id
}

type SysRole struct {
	ID           int    `gorm:"primary_key" json:"id"`                  //ID
	Level        int    `json:"level"`                                  //角色级别(越小越大)
	CreateBy     int    `json:"create_by" gorm:"autoCreateTime:milli"`  //创建者id
	UpdateBy     int    `json:"update_by" gorm:"autoCreateTime:milli"`  //更新者id
	CreateTime   int64  `json:"create_time"`                            //创建日期
	UpdateTime   int64  `json:"update_time"`                            //更新时间
	IsProtection []byte `json:"is_protection" gorm:"default:[]byte{0}"` //是否受保护(内置角色,1为内置角色,默认值为0)
	IsDeleted    []byte `json:"is_deleted"`                             //软删除(默认值为0,1为删除)
	Name         string `json:"name"`                                   //角色名称
	Description  string `json:"description"`                            //描述
	DataScope    string `json:"data_scope"`                             //数据权限
	//Address      string `json:"address"`                                //路由
	//Action       string `json:"action"`                                 //请求方法
}

type SysDept struct {
	BaseModel
	Name     string `json:"name"`                              //名称
	Pid      int    `json:"pid"`                               //上级部门(顶级部门为0,默认为0)
	SubCount int    `json:"sub_count" gorm:"default:0"`        //子部门数目
	DeptSort int    `json:"deptSort"`                          //排序
	CreateBy int    `json:"create_by"`                         //创建者
	UpdateBy int    `json:"update_by"`                         //更新者
	Enabled  []byte `json:"enabled"  gorm:"default:[]byte{0}"` //状态:1启用(默认)、0禁用
}
// 获取用户完整信息
func GetUserData(c *gin.Context) (user *UserInfo, err error) {

	userId, err := GetCurrentUserId(c)
	if err != nil {
		return
	}

	keys := new([]string)
	*keys = append(*keys, cache.KeyUserJob, cache.KeyUserRole, cache.KeyUserMenu, cache.KeyUserDept, cache.KeyUserDataScope)
	cacheMap := cache.GetUserCache(keys, userId)

	cacheJob, jobErr := cacheMap[cache.KeyUserJob].Result()
	cacheRole, rolesErr := cacheMap[cache.KeyUserRole].Result()
	cacheMenu, menuErr := cacheMap[cache.KeyUserMenu].Result()
	cacheDept, deptErr := cacheMap[cache.KeyUserDept].Result()
	cacheDataScopes, dataScopesErr := cacheMap[cache.KeyUserDataScope].Result()
	jobs := new([]models.SysJob)
	if err = service.GetUserJobData(cacheJob, jobErr, jobs, userId); err != nil {
		return nil, err
	}

	roles := new([]models.SysRole)
	if err = service.GetUserRoleData(cacheRole, rolesErr, roles, userId); err != nil {
		return nil, err
	}

	menuPermission := new([]string)
	if err = service.GetUserMenuData(cacheMenu, menuErr, userId, menuPermission, roles); err != nil {
		return nil, err
	}

	dept := new(models.SysDept)
	if err = service.GetUserDeptData(cacheDept, deptErr, dept, userId); err != nil {
		return nil, err
	}

	dataScopes := new([]int)
	if err = service.GetUserDataScopes(cacheDataScopes, dataScopesErr, dataScopes, userId, dept.ID, roles); err != nil {
		return nil, err
	}

	user = new(UserInfo)
	user.Jobs = jobs
	user.Roles = roles
	user.MenuPermission = menuPermission
	user.Dept = dept
	user.DataScopes = dataScopes
	return
}

注册swagger api相关路由
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))

注册业务路由放到最后来讲
下面这个路由用来测试服务是否开启

r.GET("/", func(c *gin.Context) {
		c.String(http.StatusOK, "ok")
	})

下面这个写法是使用pprof对程序服务进行性能分析调优。对于gin框架只用写下面这一句话即可:

pprof.Register(r) // 注册pprof相关路由

回到Run文件中
传入两个值,Addr接收服务器id和端口号 Addr optionally specifies the TCP address for the server to listen on
A Server defines parameters for running an HTTP server.

// 启动服务(优雅关机)
	srv := &http.Server{
		Addr:    fmt.Sprintf("%s:%s", config.ApplicationConfig.Host, config.ApplicationConfig.Port),
		Handler: r,
	}

然后启用一个goroutine来启动服务:
ListenAndSreve:内部创建一个Server结构体,调用该结构体的ListenAndServer方法然后返回:
从传递的参数来看,这些参数只是为了创建一个Service结构体实例;如果不传具体参数,那么则会以":http"(等价于":80")和DefaulServeMux作为参数来创建Server结构体实例
如果Server已关闭,会报错:ErrServerClosed

 	// ListenAndServe listens on the TCP network address srv.Addr and then calls Serve to handle requests on incoming connections.
 go func() {
		// 开启一个goroutine启动服务
		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			zap.L().Fatal("listen: ", zap.Error(err))
		}
	}()

进入Service.ListenAndServe
创建了一个服务器Listener,在返回时把它传给了Server.Serve()方法并调用Server.Serve()

func (srv *Server) ListenAndServe() error {
	addr := srv.Addr
	if addr == "" {
		addr = ":http"  // 如果不指定服务器地址信息,默认以":http"作为地址信息
	}
	ln, err := net.Listen("tcp", addr)    // 这里创建了一个TCP Listener,之后用于接收客户端的连接请求
	if err != nil {
		return err
	}
	return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})  // 调用Server.Serve()函数并返回
}

继续分析Server.Serve

func (srv *Server) Serve(l net.Listener) error {
	defer l.Close()
	var tempDelay time.Duration 
	// 这个循环就是服务器的主循环了,通过传进来的listener接收来自客户端的请求并建立连接,
	// 然后为每一个连接创建routine执行c.serve(),这个c.serve就是具体的服务处理了
	for {
		rw, e := l.Accept()
		if e != nil {
			if ne, ok := e.(net.Error); ok && ne.Temporary() {
				if tempDelay == 0 {
					tempDelay = 5 * time.Millisecond
				} else {
					tempDelay *= 2
				}
				if max := 1 * time.Second; tempDelay > max {
					tempDelay = max
				}
				srv.logf("http: Accept error: %v; retrying in %v", e, tempDelay)
				time.Sleep(tempDelay)
				continue
			}
			return e
		}
		tempDelay = 0
		c, err := srv.newConn(rw)
		if err != nil {
			continue
		}
		c.setState(c.rwc, StateNew) // before Serve can return
		go c.serve() // <-这里为每一个建立的连接创建routine之后进行服务
	}
}

接着看conn.serve()里面是怎么进行服务的

func (c *conn) serve() {



	origConn := c.rwc // copy it before it's set nil on Close or Hijack
 
	// 这里做了一些延迟释放和TLS相关的处理...
	
	// 前面的部分都可以忽略,这里才是主要的循环
	for {
		w, err := c.readRequest()  // 读取客户端的请求
		// ...
		serverHandler{c.server}.ServeHTTP(w, w.req) //这里对请求进行处理
		if c.hijacked() {
			return
		}
		w.finishRequest()
		if w.closeAfterReply {
			if w.requestBodyLimitHit {
				c.closeWriteAndWait()
			}
			break
		}
		c.setState(c.rwc, StateIdle)
	}
}
生成一张流程图:

ListenAndServer服务流程

	fmt.Println(utils.Red(string(LogoContent)))
	tip()
	fmt.Println(utils.Green("Server run at:"))
	fmt.Printf("-  Local:   http://localhost:%s/ \r\n", config.ApplicationConfig.Port)
	fmt.Printf("-  Network: http://%s:%s/ \r\n", utils.GetLocaHonst(), config.ApplicationConfig.Port)
	fmt.Println(utils.Green("Swagger run at:"))
	fmt.Printf("-  Local:   http://localhost:%s/swagger/index.html \r\n", config.ApplicationConfig.Port)
	fmt.Printf("-  Network: http://%s:%s/swagger/index.html \r\n", utils.GetLocaHonst(), config.ApplicationConfig.Port)
	fmt.Printf(utils.Red("%s Enter Control + C Shutdown Server \r\n"), utils.GetCurrentTimeStr())

回归项目,上面这些代码调用的是如下:
主要通过SetColor这个方法实现彩色打印:
这个方法内部是通过fmt.Sprintf("%c[%d;%d;%dm%s%c[0m", 0x1B, conf, bg, text, msg, 0x1B)这个方法来实现彩色打印滴

// 前景 背景 颜色
// ---------------------------------------
// 30  40  黑色
// 31  41  红色
// 32  42  绿色
// 33  43  黄色
// 34  44  蓝色
// 35  45  紫红色
// 36  46  青蓝色
// 37  47  白色
//
// 代码 意义
// -------------------------
//  0  终端默认设置
//  1  高亮显示
//  4  使用下划线
//  5  闪烁
//  7  反白显示
//  8  不可见

const (
	TextBlack = iota + 30
	TextRed
	TextGreen
	TextYellow
	TextBlue
	TextMagenta
	TextCyan
	TextWhite
)

func Black(msg string) string {
	return SetColor(msg, 0, 0, TextBlack)
}

func Red(msg string) string {
	return SetColor(msg, 0, 0, TextRed)
}

func Green(msg string) string {
	return SetColor(msg, 0, 0, TextGreen)
}

func Yellow(msg string) string {
	return SetColor(msg, 0, 0, TextYellow)
}

func Blue(msg string) string {
	return SetColor(msg, 0, 0, TextBlue)
}

func Magenta(msg string) string {
	return SetColor(msg, 0, 0, TextMagenta)
}

func Cyan(msg string) string {
	return SetColor(msg, 0, 0, TextCyan)
}

func White(msg string) string {
	return SetColor(msg, 0, 0, TextWhite)
}

func SetColor(msg string, conf, bg, text int) string {
	return fmt.Sprintf("%c[%d;%d;%dm%s%c[0m", 0x1B, conf, bg, text, msg, 0x1B)
}

至于tip方法,需要说明的是config.ApplicationConfig.Version这一块,

	func tip() {
	usageStr := `欢迎使用 ` + utils.Green(`GoSword `+config.ApplicationConfig.Version) + ` 可以使用 ` + utils.Red(`-help`) + ` 查看命令`
	fmt.Printf("%s \n\n", usageStr)
}
cfgApplication = viper.Sub("settings.application")
	if cfgApplication == nil {
		panic("No found settings.application in the configuration")
	}
	ApplicationConfig = InitApplication(cfgApplication)

这个写法就很值得借鉴。先是写了配置结构体,然后写了一个初始化配置的方法,返回值是一个指针类型的结构体。传进来的是viper
这个方法在config文件(上面)中使用:
通过viper.sub拿到具体位置然后将这些数据传给InitApplication方法。将其一一赋值给返回值。返回的是指针,如果要使用指针,首先就是要初始化,在当前文件中初始化,并将变量名首字母大写。在需要的地方直接调用即可。

type Application struct {
	ReadTimeout   int
	WriterTimeout int
	Host          string
	Port          string
	Name          string
	Mode          string
	StaticFileUrl string
	StaticPath    string
	Version       string
	EnableDP      bool
}
// InitApplication 初始化app配置
func InitApplication(cfg *viper.Viper) *Application {
	return &Application{
		ReadTimeout:   cfg.GetInt("readTimeout"),
		WriterTimeout: cfg.GetInt("writerTimeout"),
		Host:          cfg.GetString("host"),
		Port:          cfg.GetString("port"),
		Name:          cfg.GetString("name"),
		Mode:          cfg.GetString("mode"),
		StaticFileUrl: cfg.GetString("staticfileurl"),
		StaticPath:    cfg.GetString("staticpath"),
		Version:       cfg.GetString("version"),
		EnableDP:      cfg.GetBool("enabledp"),
	}
}

var ApplicationConfig = new(Application)

优雅的关闭服务器
shutdown将无中断的关闭正在活跃的连接,然后平滑的停止服务,处理流程如下:
首先关闭所有监听
然后关闭所有空闲连接
然后无期限等待连接处理完毕转为空闲,并返回
执行 Shutdown 时如果传 nil,并且有未完成的请求,会报错 panic: runtime error: invalid memory address or nil pointer dereference。
正确的方式是执行 Shutdown 时传入一个非 nil 的 context.Context。

注意,虽然到截至时间会自动cancel,但cancel代码仍建议加上。
到截至时间而被取消还是被cancel代码所取消,取决于哪个信号发送的早。

context包提供从已有Context衍生新的Context的能力。
这样即可形成一个Context树。
当父Context取消时,所有从其衍生出来的子Context亦会被取消。
Background是所有Context树的根,其永远不会被取消。

使用WithCancel及WithTimeout可以创建衍生的Context
WithCancel可用来取消一组从其衍生的goroutine
WithTimeout可用来设置截至时间
WithValue提供给Context赋予请求域数据的能力。

	// 等待中断信号来优雅地关闭服务器,为关闭服务器操作设置一个5秒的超时
	quit := make(chan os.Signal, 1) // 创建一个接收信号的通道
	// kill 默认会发送 syscall.SIGTERM 信号
	// kill -2 发送 syscall.SIGINT 信号,我们常用的Ctrl+C就是触发系统SIGINT信号
	// kill -9 发送 syscall.SIGKILL 信号,但是不能被捕获,所以不需要添加它
	// signal.Notify把收到的 syscall.SIGINT或syscall.SIGTERM 信号转发给quit
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) // 此处不会阻塞
	<-quit                                               // 阻塞在此,当接收到上述两种信号时才会往下执行
	zap.L().Info(utils.Red("Shutdown Server ..."))
	// 创建一个5秒超时的context
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	// 5秒内优雅关闭服务(将未处理完的请求处理完再关闭服务),超过5秒就超时退出
	if err := srv.Shutdown(ctx); err != nil {
		zap.L().Fatal(utils.Red("Server Shutdown"), zap.Error(err))
	}
	zap.L().Info(utils.Red("Server exiting"))
接下来是项目业务路由。这一块儿的写法的值得借鉴学习。
	先在Setup方法中初始化engine,然后注册业务路由
	在common模块中注册主体部分,调用admin里面的路由注册方法。
	admin "project/app/admin/router"
	admin.InitAdminRouter(r)
下面的写法较为高级
先定义两个方法(一个是需要身份认证另一个不需要)切片,长度为0
然后是InitAdminRouter方法注册路由
	包含两个方法,该方法只起分流作用,使代码更清晰
	adminNoCheckRoleRouter方法:
		通过for range获取切片里面的每一个方法,并将*RouterGroup类型的参数v1传进去
	下面那个方法也同理
	这两个方法有意思。如果顺着流程走的话,你就会觉得有疑惑:空空的切片怎么遍历,但具体每个模块儿文件都使用了init方法,在包第一次加载的时候就已经把每个模块儿的路由加载到切片里面了。
var (
	routerNoCheckRole = make([]func(*gin.RouterGroup), 0)
	routerCheckRole   = make([]func(v1 *gin.RouterGroup), 0)
)

//InitAdminRouter 后台模块路由
func InitAdminRouter(r *gin.Engine) *gin.Engine {

	// 无需认证的路由
	adminNoCheckRoleRouter(r)
	// 需要认证的路由
	adminCheckRoleRouter(r)

	return r
}

func adminNoCheckRoleRouter(r *gin.Engine) {
	// 可根据业务需求来设置接口版本
	v1 := r.Group("/api")
	// 空接口防止v1定义无使用报错
	v1.GET("/nilcheckrole", nil)

	for _, f := range routerNoCheckRole {
		f(v1)
	}
}

func adminCheckRoleRouter(r *gin.Engine) {
	// 可根据业务需求来设置接口版本
	v1 := r.Group("/api", middleware.JWTAuthMiddleware())
	// 空接口防止v1定义无使用报错
	v1.GET("/checkrole", nil)

	for _, f := range routerCheckRole {
		f(v1)
	}
}
上面这些都是写在common模块里的,与项目有关的具体业务逻辑则是写在和它平级的app文件夹里。分为前台和后台。后台一般叫做admin:
找到router文件夹。
这次我们只看这文件夹里面的路由实现:
	通过init方法导包时调用,使用append方法对切片进行追加,该切片存储的是参数是*gin.RouterGroup的方法,并在此按照项目模块进行路由分组。
func init()  {
	routerNoCheckRole = append(routerNoCheckRole, getCaptchaRouter)
}

// 无需认证的路由代码
func getCaptchaRouter(v1 *gin.RouterGroup) {
	r := v1.Group("/auth")
	{
		r.GET("code", apis.Captcha)
	}
}
需要注意和学习的是:Restful风格接口。
请求参数极为简单。
ping方法只是用来测试,
func init() {
	routerNoCheckRole = append(routerNoCheckRole, deptRouter)
	routerCheckRole = append(routerCheckRole, deptAuthRouter)
}

// 无需认证的路由代码
func deptRouter(v1 *gin.RouterGroup) {
	r := v1.Group("/dept")
	{
		r.GET("ping", func(c *gin.Context) {
			c.String(int(app.CodeSuccess), "ok")
		})
	}
}

// 需认证的路由代码
func deptAuthRouter(v1 *gin.RouterGroup) {
	r := v1.Group("/dept")
	{
		r.GET("/download", apis.DownloadDeptHandler)
		r.GET("/", apis.SelectDeptHandler)
		r.POST("/", apis.InsertDeptHandler)
		r.DELETE("/", apis.DeleteDeptHandle)
		r.PUT("/", apis.UpdateDeptHandler)
		r.POST("/superior", apis.SuperiorDeptHandler)
	}
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值