一 前言
此项目为冬奥会智能分析与预测可视化平台,这个项目是用于参加中国大学生计算机设计大赛-信息可视化设计赛道的项目,最终获得国赛三等奖的成绩。现在主要介绍一下本项目的后端项目搭建。
二 框架与技术栈
本项目采取的框架为Go中的gin框架:
-
Gin是一个golang的微框架,封装比较优雅,API友好,源码注释比较明确,具有快速灵活,容错方便等特点。
-
对于golang而言,web框架的依赖要远比Python,Java之类的要小。自身的net/http足够简单,性能也非常不错。
-
借助框架开发,不仅可以省去很多常用的封装带来的时间,也有助于团队的编码风格和形成规范。
本项目采取的技术栈包括:
- 使用zap日志库
- 使用配置信息viper
- 使用服务器压测工具测试
- 使用接口文档swagger
- 使用JWT跨域认证
- 使用雪花算法生成ID
- 使用优雅关机和重启
- 使用go-redis
- 使用gorm开发
- 使用pprof进行性能分析
- 使用Air实现文件实时重载
三 项目目录搭建
安装gin框架:
go install github.com/gin-gonic/gin
新建项目 WinterOlympic
进入文件夹,在终端执行
go mod init
go mod init 是使用go module 的管理包,前提是设置做了以下设置
GO111MODULE = on
GOPROXY=https://goproxy.cn,direct
执行 go mod init 后, 将在 WinterOlympic 文件夹下生成 go.mod 文件, 将会包含以下信息:模块名和go 版本
module WinterOlympic
go 1.17
新建以下目录:
- 目录讲解
|- - - conf //项目全局配置
|- - - controller //控制器文件
|- - - dao //数据库和Redis操作
| - - - docs//生成Swagger文档
|- - - logger//生成项目运行日志
| - - - logic//处理功能逻辑函数
| - - - middlewares//使用中间件
|- - - models //模型文件包
| - - - pkg//使用的第三方库
| - - - routes//路由处理
|- - - settings// 项目设置
|- - - static// 静态媒体文件
|- - - templates//页面模板
| - - - util //公共函数
go.mod // go 依赖管理文件
.air.conf // go 项目热加载配置文件
main.go // 主项目入口文件
Makefile // go 自动化编译文件
REMADE.md // 项目阅读文档
WinterOlympic.log // 项目运行日志
四 项目搭建
4.1 main.go主函数:
main.go函数主要包括以下六个步骤:
- 加载配置
- 初始化日志
- 初始化Mysql连接
- 初始化Redis连接
- 注册路由
- 启动服务(优雅关机)
package main
import (
"WinterOlympic/dao/mysql"
"WinterOlympic/dao/redis"
"WinterOlympic/logger"
"WinterOlympic/middlewares"
"WinterOlympic/pkg/Kafka"
"WinterOlympic/routes"
"WinterOlympic/settings"
"fmt"
"go.uber.org/zap"
)
// @title 冬奥会智能分析与预测系统
// @version 1.0
// @description 本系统展示了前八界冬奥会数据和预测了下一届冬奥会各个国家获得的奖牌信息
func main() {
//1.加载配置
if err := settings.Init(); err != nil {
fmt.Printf("Init settings failed, err: %v\n", err)
return
}
//2.初始化日志
if err := logger.Init(); err != nil {
fmt.Printf("Init logger failed, err: %v\n", err)
return
}
defer zap.L().Sync()
zap.L().Debug("logger init success...")
//3.初始化Mysql连接
if err := mysql.Init(); err != nil {
fmt.Printf("Init mysql failed, err: %v\n", err)
return
}
defer mysql.Close()
//4.初始化Redis连接
if err := redis.Init(); err != nil {
fmt.Printf("Init redis failed, err: %v\n", err)
return
}
defer redis.Close()
//5.注册路由
r := routes.Setup()
//6.启动服务(优雅关机)
if err := middlewares.GracefulShutdown(r); err != nil {
fmt.Printf("Graceful Shutdown failed, err: %v\n", err)
return
}
}
4.1 加载配置
在conf中新建config.yaml文件
name: "WinterOlympic"
title: "冬奥会智能分析与预测系统"
mode: "dev"
port: 3600
version: "v0.0.1"
log:
level: "debug"
filename: "WinterOlympic.log"
max_size: 200
max_age: 30
max_backips: 7
mysql:
host: "127.0.0.1"
port: 3306
user: "root"
password: "******"
dbname: "winterolympicgames"
max_open_conns: 200
max_idle_conns: "50"
redis:
host: "127.0.0.1"
port: 6379
password: ""
db: 3
pool_size: 100
email:
user: "xxx@qq.com"
// pass 授权码
pass: "xxxxxx"
host: "smtp.qq.com"
port: 465
rename: "WinterOlympic"
在settings中新建settings.go文件
package settings
import (
"fmt"
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
)
func Init() (err error) {
viper.SetConfigFile("conf/config.yaml") // 指定配置文件
//viper.SetConfigName("config") // 指定配置文件名称(不需要带后缀)
//viper.SetConfigType("yaml") // 指定配置文件类型(专用于从远程获取配置信息时指定配置文件类型)
viper.AddConfigPath(".") // 指定查找配置文件的路径(使用相对路径)
err = viper.ReadInConfig() // 读取配置信息
if err != nil { // 读取配置信息失败
fmt.Printf("viper.ReadInConfig() failed,err:%v\n", err)
return
}
// 监控配置文件变化
viper.WatchConfig()
viper.OnConfigChange(func(in fsnotify.Event) {
fmt.Println("配置文件修改了...")
})
return
}
4.2 初始化日志
在logger中新建logger.go文件
package logger
import (
"github.com/gin-gonic/gin"
"github.com/natefinch/lumberjack"
"github.com/spf13/viper"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"net"
"net/http"
"net/http/httputil"
"os"
"runtime/debug"
"strings"
"time"
)
// Init 初始化Logger
func Init() (err error) {
writeSyncer := getLogWriter(
viper.GetString("log.filename"),
viper.GetInt("log.max_size"),
viper.GetInt("log.max_backups"),
viper.GetInt("log.max_age"),
)
encoder := getEncoder()
var l = new(zapcore.Level)
err = l.UnmarshalText([]byte(viper.GetString("log.level")))
if err != nil {
return
}
core := zapcore.NewCore(encoder, writeSyncer, l)
lg := zap.New(core, zap.AddCaller())
zap.ReplaceGlobals(lg) // 替换zap包中全局的logger实例,后续在其他包中只需使用zap.L()调用即可
return
}
func getEncoder() zapcore.Encoder {
encoderConfig := zap.NewProductionEncoderConfig()
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
encoderConfig.TimeKey = "time"
encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
encoderConfig.EncodeDuration = zapcore.SecondsDurationEncoder
encoderConfig.EncodeCaller = zapcore.ShortCallerEncoder
return zapcore.NewJSONEncoder(encoderConfig)
}
func getLogWriter(filename string, maxSize, maxBackup, maxAge int) zapcore.WriteSyncer {
lumberJackLogger := &lumberjack.Logger{
Filename: filename,
MaxSize: maxSize,
MaxBackups: maxBackup,
MaxAge: maxAge,
}
return zapcore.AddSync(lumberJackLogger)
}
// 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)
zap.L().Info(path,
zap.Int("status", c.Writer.Status()),
zap.String("method", c.Request.Method),
zap.String("path", path),
zap.String("query", query),
zap.String("ip", c.ClientIP()),
zap.String("user-agent", c.Request.UserAgent()),
zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()),
zap.Duration("cost", cost),
)
}
}
// 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()
}
}
4.3 初始化Mysql连接
在dao中新建mysql文件夹再新建mysql.go文件
package mysql
import (
"fmt"
_ "github.com/go-sql-driver/mysql"
"github.com/jinzhu/gorm"
"github.com/spf13/viper"
"go.uber.org/zap"
)
var db *gorm.DB
func Init() (err error) {
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True",
viper.GetString("mysql.user"),
viper.GetString("mysql.password"),
viper.GetString("mysql.host"),
viper.GetInt("mysql.port"),
viper.GetString("mysql.dbname"),
)
db, err = gorm.Open("mysql", dsn)
if err != nil {
zap.L().Error("connect DB failed", zap.Error(err))
return
}
db.DB().SetMaxOpenConns(viper.GetInt("mysql.max_open_conns"))
db.DB().SetMaxIdleConns(viper.GetInt("mysql.max_idle_conns"))
return
}
func Close() {
_ = db.Close()
}
4.4. 初始化Redis连接
在dao中新建redis文件夹再新建redis.go文件
package redis
import (
"fmt"
"github.com/go-redis/redis"
"github.com/spf13/viper"
)
// 声明一个全局的rdb变量
var rdb *redis.Client
// Init 初始化连接
func Init() (err error) {
rdb = redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%d",
viper.GetString("redis.host"),
viper.GetInt("redis.port"),
),
Password: viper.GetString("redis.password"), // no password set
DB: viper.GetInt("redis.db"), // use default DB
PoolSize: viper.GetInt("redis.pool_size"),
})
_, err = rdb.Ping().Result()
return err
}
func Close() {
_ = rdb.Close()
}
4.5 注册路由
在routes中新建routes.go文件
package routes
import (
"WinterOlympic/logger"
"WinterOlympic/pkg/cors"
"github.com/gin-contrib/pprof"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
"github.com/spf13/viper"
"net/http"
"strconv"
)
func Setup() *gin.Engine {
r := gin.New() //创立新的路由
store := cookie.NewStore([]byte("WinterOlympic"))
r.Static("/static", "./static")
r.LoadHTMLFiles("templates/dist/index.html")
r.Use(cors.Cors())
r.Use(logger.GinLogger(), logger.GinRecovery(true), sessions.Sessions("WinterOlympic", store)) //使用日志记录路由信息
pprof.Register(r) //注册pprof相关路由
InitSwagger(r) //注册Swagger文档路由
r.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", gin.H{})
})
AccountGroupRoute(r.Group("/localApi/account/")) //注册账户功能模块路由
SportsGroupRoute(r.Group("/localApi/sports/")) //注册主页面模块路由
CountryGroupRoute(r.Group("/localApi/country/")) //注册国家详细页面模块路由
r.Run(":" + strconv.Itoa(viper.GetInt("port"))) //运行路由
return r
}
4.6 启动服务(优雅关机)
在middlewares中新建graceShutdown.go文件
package middlewares
import (
"context"
"fmt"
"github.com/gin-gonic/gin"
"github.com/spf13/viper"
"go.uber.org/zap"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func GracefulShutdown(r *gin.Engine) (err error) {
srv := &http.Server{
Addr: fmt.Sprintf(":%d", viper.GetInt("app.port")),
Handler: r,
}
go func() {
// 开启一个goroutine启动服务
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
}
}()
// 等待中断信号来优雅地关闭服务器,为关闭服务器操作设置一个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("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("Server Shutdown: ", zap.Error(err))
}
zap.L().Info("Server exiting")
return
}
4.7运行
在终端输入:
go mod tidy
安装好各种依赖库后,再输入:
go run main.go
即可运行项目。
五 总结
以上就是冬奥会智能分析与预测可视化平台后端项目搭建的全部内容啦,在搭建项目目录时可能会有一些不足,请大家多多批评指正。
博主还会更新在主函数中使用到的技术的具体用法,请大家收藏观看不迷路。
本专栏还会继续的更新,希望大家能够多多关注博主的文章,感谢大家的观看。