系统的完备功能

29 阅读12分钟

系统构建必要功能实现

jwt签名验证

创建签名工具包

// 在token中保存id、username
type UserInfo struct {
	Id       int
	UserName string
}

type MyClaims struct {
	User UserInfo
	// 标准Claims结构体,可传入配置项
	jwt.RegisteredClaims
}

// 返回信息集合
const (
	ISS   = "签发者异常"
	EXP   = "过期时间异常"
	AUD   = "接受者异常"
	IAT   = "签发时间异常"
	NBF   = "该令牌尚未生效"
	JTI   = "身份标识校验错误"
	SUB   = "用户身份错误"
	OTHER = "身份校验失败"
	OK    = "身份校验成功"
)

// 过期时间(这里设置两小时)
const TokenExpireDuration = time.Hour * 2

// 签名
var mySecret = []byte("shuonihao")

// 生成token,传入UserInfo结构体
func GenerateToken(userInfo UserInfo) (string, error) {
	expirationTime := time.Now().Add(TokenExpireDuration) // 两个小时有效期
	claims := MyClaims{
		User: userInfo,
		RegisteredClaims: jwt.RegisteredClaims{
			// 过期时间
			ExpiresAt: jwt.NewNumericDate(expirationTime),
			// 签发时间
			IssuedAt: jwt.NewNumericDate(time.Now()),
			// 生效时间
			NotBefore: jwt.NewNumericDate(time.Now()),
			// 签发者
			Issuer: "dengwenhao",
			// 主题
			Subject: "测试",
			// 用户类型
			Audience: []string{"ok", "no"},
		},
	}
	// 生成Token,指定签名算法和claims
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	// 签名
	if tokenString, err := token.SignedString(mySecret); err != nil {
		return "", err
	} else {
		return tokenString, nil
	}
}

// 获取签名,需要传入解析结构的是一个函数
func Secret() jwt.Keyfunc {
	return func(token *jwt.Token) (interface{}, error) {
		return mySecret, nil
	}
}

// 解析token
func ParseToken(tokenss string) (*MyClaims, string) {
	token, err := jwt.ParseWithClaims(tokenss, &MyClaims{}, Secret())
	if err != nil {
		if ve, ok := err.(*jwt.ValidationError); ok {
			if ve.Errors&jwt.ValidationErrorMalformed != 0 {
				return nil, OTHER
			} else if ve.Errors&jwt.ValidationErrorExpired != 0 {
				return nil, EXP
			} else if ve.Errors&jwt.ValidationErrorNotValidYet != 0 {
				return nil, NBF
			} else {
				return nil, JTI
			}
		}
	}
	if claims, ok := token.Claims.(*MyClaims); ok && token.Valid {
		return claims, OK
	}
	return nil, OTHER
}

在业务中使用

// 签发token
token, _ := utils.GenerateToken(userInfo)
// 解析token(从请求体中获取token字符串)
tokenStr := ctx.Request.Header.Get("Authorization")
// 如果没有token,token==nil
token, msg := utils.ParseToken(tokenStr)

在中间件中使用

// 可以先从控制器获取token传入中间件,或者在中间件中获取token需要使用的时候返回给控制器
func JWTAuth() gin.HandlerFunc {
	return func(ctx *gin.Context) {
		auth := ctx.Request.Header.Get("Authorization")
		if len(auth) == 0 {
			// 没有token直接终止请求
			ctx.Abort()
			ctx.String(http.StatusOK, "未登录无权限")
			return
		}
		// 校验token,只要出错直接拒绝请求
		token, msg := utils.ParseToken(auth)
		if token == nil {
			ctx.Abort()
			ctx.String(http.StatusOK, msg)
			return
		} else {
			// 控制器需要用到token的时候直接发送过去,随时取用
			ctx.Set("token", token)
		}
		ctx.Next()
	}
}

// 续签Token
// 普通令牌的时间不宜过长
// 过期不超过额定时间续签token
func JWTAuthPlus() gin.HandlerFunc {
	return func(ctx *gin.Context) {
		auth := ctx.Request.Header.Get("Authorization")
		if len(auth) == 0 {
			ctx.Abort()
			ctx.String(http.StatusOK, "未登录无权限")
			return
		}
		// 校验token
		token, msg := utils.ParseToken(auth)
		fmt.Println(msg)
		if msg != utils.OK {
			if msg == utils.EXP {
				// 判断是否过期,调用续签函数(判断逻辑在续签那里)
				newToken, _ := utils.RenewToken(token)
				// 续签成功给响应头设置一个newtoken字段
				if newToken != "" {
					ctx.Header("newtoken", newToken)
					ctx.Request.Header.Set("Authorization", newToken)
					// 把新的解析给控制器
					lastetToken, _ := utils.ParseToken(newToken)
					ctx.Set("token", lastetToken)
					ctx.Next()
					return
				}
			}
			// Token验证失败或续签失败直接拒绝请求
			ctx.Abort()
			ctx.String(http.StatusOK, msg)
			return
		}
		// token未过期继续执行其他中间件
		ctx.Set("token", token)
		ctx.Next()
	}
}

// 单点登录
// 双token方案
// 一个普通token时间较短,一个refresh_token较长时间,只要refresh_token未过期就再续签一个普通token
// 主要用于第三方登录,第三方提供的token一般时间较短,不能使用户过于频繁地登录第三方,后台操作即可

在控制器中获取token

token, _ := ctx.Get("token")

注册中间件

// 单个路由
userRouter.GET("/auth", middleware.JWTAuth(), user.UserController{}.AuthToken)
// 单组路由
userRouter := r.Group("/user", middleware.JWTAuth())
// 全局使用
r.Use(middleware.JWTAuth())

图形验证码

第三方包推荐

### 图形验证码或者音频验证码,这里我们使用这个包的图形验证码
go get github.com/mojocn/base64Captcha

创建工具包

// 有默认缓存机制,不用额外使用缓存中间件
// 10240个,和两分钟缓存
var store = base64Captcha.NewMemoryStore(base64Captcha.GCLimitNumber, base64Captcha.Expiration/5)

// 生成验证码
func CreateImageCode() (id, b64s, code string, err error) {
	var driver base64Captcha.Driver
	var driverString base64Captcha.DriverString

	// 配置验证码信息
	captchaConfig := base64Captcha.DriverString{
		Height:          60,
		Width:           200,
		NoiseCount:      3,
		ShowLineOptions: 2 | 4,
		Length:          6,
		Source:          "1234567890abcdefghjkmnopqrstuvwxyzABCDEFGHJKLMNOPQRSTUVWXYZ",
		BgColor: &color.RGBA{
			R: 3,
			G: 102,
			B: 214,
			A: 125,
		},
		Fonts: []string{"wqy-microhei.ttc"},
	}

	driverString = captchaConfig
	driver = driverString.ConvertFonts()
	captcha := base64Captcha.NewCaptcha(driver, store)
    // 改一下源码,返回一下这个验证码的值,便于校验
	lid, lb64s, code, lerr := captcha.Generate()
	return lid, lb64s, code, lerr
}

// 验证captcha是否正确
// 需要一个生成验证码的id,id配对的value(也就是用户输入的值)
// 前端携带一个id、一个value
func CaptVerify(id string, val string) bool {
	if id == "" || val == "" {
		return false
	}
	// 校验一次验证码后清除
	return store.Verify(id, val, true)
}

在控制器中使用

// 生成验证码
// 一般只返回id和base64图片就可以了
id, base64Str, code, err := utils.CreateImageCode()
// 校验验证码
flag := utils.CaptVerify(ctx.Param("id"), ctx.Param("code"))

接口文档

swagger

swagger是我接触web应用开发以来最熟悉的一个接口文档框架。

在gin中

官方地址

包安装

# 主要用于生成命令
go get -u github.com/swaggo/swag/cmd/swag
go install github.com/swaggo/swag/cmd/swag@latest
go get -u github.com/swaggo/gin-swagger
go get -u github.com/swaggo/files

在控制器中

// @Summary 用户查询
// @Description 只查询自己子集的用户
// @Tags 用户管理
// @Accept json
// @Produce json
// @Success 200 {object} models.ResultCommon[[]models.UserSelect] "查询成功"
// @Router /user/subset [get]
// @Security JWT
func (u UserController) QueryUserSubset(ctx *gin.Context) {
	// 获取id
	token, _ := ctx.Get("token")
	// 断言成那个类型
	id := &token.(*jwt.MyClaims).User.Id
	// 声明承载体
	userList := []models.UserSelect{}
	models.DB.Model(&models.User{}).Where("fid=?", id).Find(&userList)
	ctx.JSON(http.StatusOK, &models.ResultCommon[[]models.UserSelect]{
		Msg:    "查询成功",
		Code:   "200",
		Result: userList,
	})
}

在main中

// @title RBAC权限管理系统
// @version 1.0.0
// @description RBAC权限管理的接口文档
// @host localhost:8002
// @BasePath /api/v1
// @schemes http https
// @securityDefinitions.apikey JWT
// @in header
// @name Authorization
func main() {
	r := gin.Default()
	r.SetTrustedProxies([]string{"127.0.0.1"})

	docs.SwaggerInfo.BasePath = "/api/v1"
	v1 := r.Group("/api/v1")
	{
		routers.UserApi(v1)
		routers.RoleApi(v1)
		routers.MenuApi(v1)
	}
	r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler))
	r.Run("127.0.0.1:8002")
}

日志

内置日志模块(简单使用)

默认是控制台输出 图片转存失败,建议将图片保存下来直接上传

func main() {
	file, _ := os.Create("gin.log")
	// 可以文件、控制台一起
	gin.DefaultWriter = io.MultiWriter(file, os.Stdout)
	router := gin.Default()
	router.GET("/", func(ctx *gin.Context) {
		ctx.String(http.StatusOK, "你好啊")
	})
	router.Run("127.0.0.1:8003")
}

格式化debug

router := gin.Default()
	gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) {
		log.Printf("[logger] %v %v %v %v\n", httpMethod, absolutePath, handlerName, nuHandlers)
	}
fmt.Println(router.Routes())

自定义输出

router := gin.New()
router.Use(gin.LoggerWithFormatter(func(params gin.LogFormatterParams) string {
	// 用户IP、请求时间、请求方法、请求路径、状态码、耗时
	return fmt.Sprintf("[myLogger] %v %v %v %v %v %v", 
		params.TimeStamp.Format("2006-01-02 15:04:05"), 
		params.ClientIP, 
		params.Method, 
		params.Path, 
		params.StatusCode, 
		params.Latency
	)
}))
// [myLogger] 2023-10-10 15:32:52 127.0.0.1 GET / 200 0s

色彩

采用ANSI escape code在终端控制颜色格式 在碰到一个色彩的时候开始,碰到另一个结束 掘金博主文章

// gin中的源码
const (
	green   = "\033[97;42m"
	white   = "\033[90;47m"
	yellow  = "\033[90;43m"
	red     = "\033[97;41m"
	blue    = "\033[97;44m"
	magenta = "\033[97;45m"
	cyan    = "\033[97;46m"
	reset   = "\033[0m"
)
// 256色
for i := 0; i < 16; i++ {
	for j := 0; j < 16; j++ {
		code := i*16 + j
		fmt.Printf("\u001b[38;5;%dm%-4d",code, code)
	}
	fmt.Println("\u001b[0m")
}
// 3开头是前景色,4开头是后景色
fmt.Println("\u001b[30m 黑色 \u001b[0m")
fmt.Println("\u001b[31m 红色 \u001b[0m")
fmt.Println("\u001b[32m 绿色 \u001b[0m")
fmt.Println("\u001b[33m 黄色 \u001b[0m")
fmt.Println("\u001b[34m 蓝色 \u001b[0m")
fmt.Println("\u001b[35m 洋红 \u001b[0m")
fmt.Println("\u001b[36m 青色 \u001b[0m")
fmt.Println("\u001b[37m 白色 \u001b[0m")
return fmt.Sprintf("[myLogger] %v  %v %s %v %s %v %s %v %s %v",
	params.TimeStamp.Format("2006-01-02 15:04:05"),
	params.ClientIP,
	params.MethodColor(), params.Method, params.ResetColor(),
	params.Path,
	params.StatusCodeColor(), params.StatusCode, params.ResetColor(),
	params.Latency,
)

图片转存失败,建议将图片保存下来直接上传

配置式记录日志,用另一个中间件

// file, _ := os.Create("gin.log")
file, _ := os.OpenFile("./gin.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
defer file.Close()
fileWriter := io.MultiWriter(file)
router.Use(gin.LoggerWithConfig(gin.LoggerConfig{
	Formatter: func(params gin.LogFormatterParams) string {
		// 用户IP、请求时间、请求方法、请求路径、状态码、耗时
		return fmt.Sprintf("[myLogger] %v  %v  %v  %v  %v  %v\n",
			params.TimeStamp.Format("2006-01-02 15:04:05"),
			params.ClientIP,
			params.Method,
			params.Path,
			params.StatusCode,
			params.Latency,
		)
	},
	// 输出的地方
	Output: fileWriter,
	// 要排除记录日志的路由
	SkipPaths: []string{"/r1"},
}))

第三方库推荐Logrus

这是go社区最流行的日志库

使用内置设置

自定义格式

type MyFomatter struct {
}

// 日志颜色
const (
	green   = "\033[97;32m"
	white   = "\033[90;37m"
	yellow  = "\033[90;33m"
	red     = "\033[97;31m"
	blue    = "\033[97;34m"
	magenta = "\033[97;35m"
	cyan    = "\033[97;36m"
	reset   = "\033[0m"
)

func (m MyFomatter) Format(entry *logrus.Entry) ([]byte, error) {
	// 生成颜色
	var color string
	switch entry.Level {
	case logrus.ErrorLevel:
		color = red
	case logrus.WarnLevel:
		color = yellow
	case logrus.InfoLevel:
		color = blue
	case logrus.DebugLevel:
		color = magenta
	default:
		color = white
	}
	// 设置buffer
	var buffer *bytes.Buffer
	if entry.Buffer == nil {
		buffer = &bytes.Buffer{}
	} else {
		buffer = entry.Buffer
	}
	// 设置时间
	time := entry.Time.Format("2006/01/02 15:04:05")
	// 合成buffer
	fmt.Fprintf(buffer, "%s[%s]%s [%s] %s\n", color, entry.Level, reset, time, entry.Message)
	return buffer.Bytes(), nil
}

自定义hook时间分割

type FileWriter struct {
}

func (fw FileWriter) Levels() []logrus.Level {
	return logrus.AllLevels
}

func (fw FileWriter) Fire(entry *logrus.Entry) error {
	// 获得时间
	filePath := fmt.Sprintf("./logs/%s.log", time.Now().Format("01020304"))
	file, _ := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
	line, _ := entry.String()
	file.Write([]byte(line))
	defer file.Close()
	return nil
}
func test() {
	for i := 0; i < 10; i++ {
		logrus.Warn("okkkk")
		time.Sleep(time.Second * 10)
		logrus.Error("okkkk")
		time.Sleep(time.Second * 10)
	}
	wg.Done()
}

var wg sync.WaitGroup

func main() {
	router := gin.New()
	logrus.AddHook(&FileWriter{})
	logrus.SetFormatter(MyFomatter{Prefix: "GinApp"})
	wg.Add(1)
	go test()
	router.Run("127.0.0.1:8003")
	wg.Wait()
}

在gin中

type MyFomatter struct {
	// 日志前缀
	Prefix string
}

func (m MyFomatter) Format(entry *logrus.Entry) ([]byte, error) {
	// 设置buffer
	var buffer *bytes.Buffer
	if entry.Buffer == nil {
		buffer = &bytes.Buffer{}
	} else {
		buffer = entry.Buffer
	}
	fmt.Fprintf(buffer, "[%s] %s\n", m.Prefix, entry.Message)
	return buffer.Bytes(), nil
}
func LogMiddleWare() gin.HandlerFunc {
	return func(ctx *gin.Context) {
		// 开始时间
		startTime := time.Now()
		path := ctx.Request.URL.Path
		if ctx.Request.URL.RawQuery != "" {
			path = path + "?" + ctx.Request.URL.RawQuery
		}
		ctx.Next()
		timeStr := startTime.Format("2006/01/02 15:04:05")
		clientIP := ctx.ClientIP()
		method := ctx.Request.Method
		statusCode := ctx.Writer.Status()
		endTime := time.Now()
		latency := endTime.Sub(startTime)
		logrus.Infof("%s [%s] %s | %d |  %s |   %s", timeStr, method, clientIP, statusCode, latency, path)
	}
}

type FileWriter struct {
}

func (fw FileWriter) Levels() []logrus.Level {
	return logrus.AllLevels
}

func (fw FileWriter) Fire(entry *logrus.Entry) error {
	level := entry.Level
	// 获得等级
	filePath := fmt.Sprintf("./logs/%s.log", level)
	file, _ := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
	line, _ := entry.String()
	file.Write([]byte(line))
	defer file.Close()
	return nil
}
func main() {
	router := gin.New()
	router.Use(LogMiddleWare())
	logrus.AddHook(FileWriter{})
	logrus.SetLevel(logrus.DebugLevel)
	logrus.SetFormatter(MyFomatter{Prefix: "GinApp"})

	router.SetTrustedProxies([]string{"127.0.0.1"})
	router.GET("/r2", func(ctx *gin.Context) {
		ctx.String(http.StatusOK, "你好啊")
	})
	router.Run("127.0.0.1:8003")
}

即时通讯WebScoket

流程

  1. 发起握手
  2. 从客户端接收数据帧
  3. 发送数据帧给客户端
  4. 关闭握手

内置http模块的使用

太复杂,跳过

gorilla/websocket包的使用

简单使用

var upgrader = websocket.Upgrader{
	ReadBufferSize:  1024,
	WriteBufferSize: 1024,
}

// 服务端处理程序
func EchoMessage(w http.ResponseWriter, r *http.Request) {
	conn, _ := upgrader.Upgrade(w, r, nil)
	for {
		// 读取客户端的消息
		msgType, msg, err := conn.ReadMessage()
		if err != nil {
			return
		}
		fmt.Printf("%s sent: %s\n", conn.RemoteAddr(), string(msg))

		// 把消息写回客户端,做一个回音处理
		if err = conn.WriteMessage(msgType, msg); err != nil {
			return
		}
	}
}

// 客户端处理程序
func DisplayEcho(w http.ResponseWriter, r *http.Request) {
	http.ServeFile(w, r, "./websocket/index.html")
}
func main(){
	// 注册两个路由
	http.HandleFunc("/echo", websocket.EchoMessage)
	http.HandleFunc("/echo_display", websocket.DisplayEcho)

	// 启动HTTP服务,监听端口8080
	http.ListenAndServe("127.0.0.1:8080", nil)
}

使用postman或者客户端页面可以进行测试 图片转存失败,建议将图片保存下来直接上传

在gin中集成一个

协程和互斥锁常量

// Index.go
var Wg sync.WaitGroup
var Mutex sync.Mutex

消息结构体模块

// Message.go
const (
	// 系统消息
	SYSTEM = 1
	// 用户消息
	USER = 2
)
const (
	// 用户进入聊天室
	Register = 1
	// 用户离开聊天室
	UnRegister = 2
	// 普通消息
	Common = 3
)

type Message struct {
	// 消息简讯
	Sms int
	// 消息具体
	Text string
	// 在线人数
	Total int
}

type WsMessage struct {
	Type int      `json:"type"`
	Data *Message `json:"data"`
}

// 接收用户的消息
type ClientMessage struct {
	// 后续可以根据Type来实现对应处理
	Type int `json:"type"`
	// 消息正文
	Msg string `json:"msg"`
}

服务端模块

// Hub.go
// 在线客户端结构体
type ClientManager struct {
	Clients    map[string]*Client // 记录在线用户
	Broadcast  chan []byte        //触发消息广播
	Register   chan *Client       // 触发新用户登陆
	UnRegister chan *Client       // 触发用户退出
}

func NewClientManager() *ClientManager {
	return &ClientManager{
		Clients:    make(map[string]*Client), // 初始化在线用户列表
		Broadcast:  make(chan []byte),        // 创建广播通道
		Register:   make(chan *Client),       // 创建新用户登陆通道
		UnRegister: make(chan *Client),       // 创建用户退出通道
	}
}

func (manager *ClientManager) InitSend(cur *Client, count int) {
	defer Wg.Done()
	// 广播有人进入
	resp, _ := json.Marshal(&WsMessage{Type: SYSTEM, Data: &Message{Sms: Register, Text: fmt.Sprintf("%s进入聊天室", cur.UserId), Total: count}})
	manager.Broadcast <- resp

	// 再给进入的人单独发送聊天记录
	// 这个暂时不做
}

func (manager *ClientManager) Run() {
	defer Wg.Done()
	for {
		select {
		// 消息广播
		case msg := <-manager.Broadcast:
			for _, conn := range manager.Clients {
				Mutex.Lock()
				conn.Send <- msg
				Mutex.Unlock()
			}
		// 新用户来到
		case conn := <-manager.Register:
			manager.Clients[conn.ID] = conn
			// 如果有新用户连接则发送最近聊天记录和在线人数给他
			count := len(manager.Clients)
			// 广播有人进入
			Wg.Add(1)
			go manager.InitSend(conn, count)
		// 用户离线
		case conn2 := <-manager.UnRegister:
			delete(manager.Clients, conn2.ID)
			// 给客户端刷新在线人数
			Wg.Add(1)
			go func() {
				defer Wg.Done()
				resp, _ := json.Marshal(&WsMessage{Type: SYSTEM, Data: &Message{Sms: UnRegister, Text: fmt.Sprintf("%s离开聊天室", conn2.UserId), Total: len(manager.Clients)}})
				manager.Broadcast <- resp
			}()
		}
	}
}

客户端模块

// 客户端结构体
type Client struct {
	ID         string
	IpAddress  string
	IpSource   string
	UserId     string
	Socket     *websocket.Conn
	Send       chan []byte
	Start      time.Time
	ExpireTime time.Duration // 一段时间没有接收到心跳则过期
	Manager    *ClientManager
}

// Read 读取客户端发送过来的消息
func (c *Client) Read() {
	// 出现故障后把当前客户端注销
	defer func() {
		Wg.Done()
		_ = c.Socket.Close()
		c.Manager.UnRegister <- c
	}()
	for {
		_, data, err := c.Socket.ReadMessage()
		if err != nil {
			fmt.Println("读取消息失败", err.Error())
			break
		}
		var msg ClientMessage
		err = json.Unmarshal(data, &msg)
		if err != nil {
			fmt.Println(err.Error())
			break
		}
		resp, _ := json.Marshal(&WsMessage{Type: USER, Data: &Message{Sms: Common, Text: msg.Msg, Total: len(c.Manager.Clients)}})
		Mutex.Lock()
		c.Manager.Broadcast <- resp
		Mutex.Unlock()
	}
}

// 根据send管道,把消息发到客户端
func (c *Client) Write() {
	defer func() {
		Wg.Done()
		_ = c.Socket.Close()
		Mutex.Lock()
		c.Manager.UnRegister <- c
		Mutex.Unlock()
	}()
	for msg := range c.Send {
		if len(msg) == 0 {
			// 没有消息则发送空响应
			err := c.Socket.WriteMessage(websocket.CloseMessage, []byte{})
			if err != nil {
				fmt.Println(err.Error())
				return
			}
			return
		}
		err := c.Socket.WriteMessage(websocket.TextMessage, msg)
		if err != nil {
			fmt.Println(err.Error())
			return
		}
	}
}

// Check 实时监测过期
func (c *Client) Check() {
	for {
		now := time.Now()
		var duration = now.Sub(c.Start)
		if duration >= c.ExpireTime {
			c.Manager.UnRegister <- c
			break
		}
	}
}

控制器模块

func WsHandler(ctx *gin.Context, Manager *models.ClientManager) {
	conn, err := (&websocket.Upgrader{
		// 决解跨域问题
		CheckOrigin: func(r *http.Request) bool { return true },
	}).Upgrade(ctx.Writer, ctx.Request, nil)
	if err != nil {
		http.NotFound(ctx.Writer, ctx.Request)
		fmt.Println(err.Error())
		return
	}
	// 可以使用session存储在线
	ip := ctx.ClientIP()
	// 在这里可以解析IP归属地
	id := generateClientID()
	client := &models.Client{
		ID:         id,
		Socket:     conn,
		Send:       make(chan []byte),
		IpAddress:  ip,
		IpSource:   "江西",
		UserId:     ctx.Query("username"),
		Start:      time.Now(),
		ExpireTime: time.Minute * 10,
		Manager:    Manager,
	}
	models.Mutex.Lock()
	client.Manager.Register <- client
	models.Mutex.Unlock()
	models.Wg.Add(1)
	go client.Read()
	models.Wg.Add(1)
	go client.Write()
	// go client.Check()
}

// // 获取uuid
func generateClientID() string {
	return uuid.New().String()
}

路由模块、主函数

func main() {
	manager := models.NewClientManager()
	router := gin.Default()
	models.Wg.Add(1)
	go manager.Run()
	router.GET("/ws", func(ctx *gin.Context) {
		controllers.WsHandler(ctx, manager)
	})
	router.Run("127.0.0.1:8080")
}

使用postman测试或者自编客户端测试

定时任务

内置time包提供功能

基本使用

// 时间结构体
// type Timer struct {
// 	C <-chan Time
// 	r runtimeTimer
// }

// 最基本的定时器
mtimer := time.NewTimer(time.Second)
fmt.Println("time start!")
<-mtimer.C
fmt.Println("time over!")

延迟执行

// 延时执行
fmt.Println("两秒延时计划开始")
<-time.After(time.Second * 2)
fmt.Println("两秒延时计划结束")

// 延时执行函数
fmt.Println("两秒延时执行函数计划开始")
chan1 := make(chan int)
time.AfterFunc(time.Second*2, func() {
	fmt.Println("两秒延时计划Ing")
	chan1 <- 1
})
// AfterFunc并没有传入C,以下报错
// <-myTimer.C
for {
	// chan多路复用,获取管道的值
	select {
	case n := <-chan1:
		fmt.Println("两秒后我收到了响应,值是:" + fmt.Sprint(n))
		fmt.Println("两秒延时执行函数计划结束")
		return
	default:
		fmt.Println("等待Ing~")
	}
}

停止执行

mtimer3 := time.NewTimer(time.Second * 2)
fmt.Println("下一步操作等待两秒执行")
time.Sleep(time.Second)
mtimer3.Stop()
fmt.Println("不,我只需要等待一秒就可以执行")

周期性

// 周期性定时器Ticker
mTicker := time.NewTicker(time.Minute * 30)
for range mTicker.C {
	fmt.Println("三秒后到我")
	tool.MailMsg()
}

//使用自定义的Ticker
type MyTicker struct {
	MyTick *time.Ticker
	Runner func()
}

// 创建一个定时任务
func NewMyTicker(interval int, f func()) *MyTicker {
	return &MyTicker{
		MyTick: time.NewTicker(time.Duration(interval) * time.Second),
		Runner: f,
	}
}

// 启动定时器需要执行的任务
func (t *MyTicker) Start() {
	for range t.MyTick.C {
		t.Runner()
	}
}
func TestMyTicker() {
	fmt.Println("【定时任务】启动!当前时间:" + time.Now().Format("2006/01/02 15:04:05"))
}
//创建一个定时任务
ticker := tool.NewMyTicker(3, tool.TestMyTicker)
ticker.Start()

cron,强大的定时任务包

在egg.js篇有介绍时间表达式的使用

基本使用

// 设置一个定时任务
// spec:时间表达式
// fn:执行函数
func TimeTask(spec string, fn func()) {
	c := cron.New()
	err := c.AddFunc(spec, fn)
	if err != nil {
		fmt.Println(err)
	}
	c.Start()
	defer c.Stop()
	select {}
}

使用实例

// 每天下午三点清理七天前的日志文件
func RemoveLog() {
	TimeTask("0 25 15 * * ?", func() {
		dirName := time.Now().AddDate(0, 0, -7).Format("20060102")
		// 文件夹路径
		folderPath := "./logs/" + dirName

		// 使用Stat函数检查文件夹是否存在
		_, err := os.Stat(folderPath)
		if os.IsNotExist(err) {
			fmt.Println("日志文件夹不存在")
		} else {
			// 文件夹存在,尝试删除
			err = os.RemoveAll(folderPath)
			if err != nil {
				fmt.Println("删除日志文件夹时发生错误:", err)
			} else {
				fmt.Println("日志文件夹已成功删除")
			}
		}
	})
}

邮件、短信功能