go-web项目通用脚手架

前言

构建go-web项目时,在编写业务逻辑代码之前,还需搭建脚手架(框架):进行编写配置文件、整合web框架(gin框架)和数据库框架(sqlx)等,搭建脚手架的过程就类似于Java中的SSM整合

编写配置文件

采用yaml格式编写配置文件,配置文件中规定项目要用到的一些重要配置属性(例如此项目的名称版本号等、mysql和redis的用户名密码等、配置文件的文件大小和存储时间等)。

在项目中使用Viper中间件,从配置文件读取配置信息,使得项目运行时可以获取到配置文件中的信息。Viper还可以监视配置文件,每当配置文件发生改变时,会执行自定义的回调函数。

配置文件示例如下:

app:name: "web_app"mode: "dev"port: 8081version: "v0.0.1"start_time: 2020-07-01machine_id: 1
log:level: "debug"filename: "web_app.log"max_size: 1000 #文件大小max_backups: 3600 #备份数量max_age: 5 #存储时间
mysql:host: "127.0.0.1"port: 3306user: "root"password: "325523"db: "bluebell" #数据库名称max_open_conns: 200 #最大连接数max_idle_conns: 50 #最大空闲连接数
redis:host: "127.0.0.1"password: ""port: 6379db: 0pool_size: 100min_idle_conns: 50

我们通常使用结构体来对应配置文件中的信息,这样使用Viper读取配置文件后,可以将对应的信息映射到结构体中,在项目中直接访问结构体来获取配置文件的信息

示例的配置文件对应的结构体如下:

同时编写初始化Viper的函数,用来读取配置文件信息并将其反序列化到结构体中

package settingsimport ("fmt""github.com/fsnotify/fsnotify""github.com/spf13/viper"
)var Conf = new(AppConfig)type AppConfig struct {Name         string `mapstructure:"verdion"`Mode         string `mapstructure:"mode"`Version      string `mapstructure:"version"`StartTime    string `mapstructure:"start_time"`MachineID    int64  `mapstructure:"machine_id"`Port         int    `mapstructure:"port"`*LogConfig   `mapstructure:"log"`*MySQLConfig `mapstructure:"mysql"`*RedisConfig `mapstructure:"redis"`
}type MySQLConfig struct {Host         string `mapstructure:"host"`User         string `mapstructure:"user"`Password     string `mapstructure:"password"`DB           string `mapstructure:"db"`Port         int    `mapstructure:"port"`MaxOpenConns int    `mapstructure:"max_open_conns"`MaxIdleConns int    `mapstructure:"max_idle_conns"`
}type RedisConfig struct {Host         string `mapstructure:"host"`Password     string `mapstructure:"password"`Port         int    `mapstructure:"port"`DB           int    `mapstructure:"db"`PoolSize     int    `mapstructure:"pool_size"`MinIdleConns int    `mapstructure:"min_idle_conns"`
}type LogConfig struct {Level      string `mapstructure:"level"`Filename   string `mapstructure:"filename"`MaxSize    int    `mapstructure:"max_size"`MaxAge     int    `mapstructure:"max_age"`MaxBackups int    `mapstructure:"max_backups"`
}// 初始化viper,读取配置文件并监视更改
func Init() error {viper.SetConfigName("config")  // 配置文件名称(无扩展名)viper.SetConfigType("yaml")    // 如果配置文件的名称中没有扩展名,则需要配置此项viper.AddConfigPath("./conf/") // 查找配置文件所在的路径err := viper.ReadInConfig() // 查找并读取配置文件if err != nil {             // 处理读取配置文件的错误//读取配置信息失败panic(fmt.Errorf("ReadInConfig failed, err: %v", err))}err = viper.Unmarshal(&Conf) // 将配置文件信息反序列化到Conf结构体中if err != nil {panic(fmt.Errorf("unmarshal to Conf failed, err:%v", err))}//监视配置文件的更改viper.WatchConfig()viper.OnConfigChange(func(in fsnotify.Event) {fmt.Printf("配置文件修改了...")err = viper.Unmarshal(&Conf)if err != nil {panic(fmt.Errorf("unmarshal to Conf failed, err:%v", err))}})return err
}

初始化日志配置

gin框架配置ZAP

介绍在基于gin框架开发的项目中如何配置并使用zap来接收并记录gin框架默认的日志和如何配置日志归档。

gin默认的中间件

首先我们来看一个最简单的gin项目:

func main() {r := gin.Default()r.GET("/hello", func(c *gin.Context) {c.String("hello liwenzhou.com!")})r.Run(
}

接下来我们看一下gin.Default()的源码:

func Default() *Engine {debugPrintWARNINGDefault()engine := New()engine.Use(Logger(), Recovery())return engine
}

也就是我们在使用gin.Default()的同时是用到了gin框架内的两个默认中间件Logger()和Recovery()。

其中Logger()是把gin框架本身的日志输出到标准输出(我们本地开发调试时在终端输出的那些日志就是它的功劳),而Recovery()是在程序出现panic的时候恢复现场并写入500响应的。

基于zap的中间件

我们可以模仿Logger()和Recovery()的实现,使用我们的日志库来接收gin框架默认输出的日志。

这里以zap为例,我们实现两个中间件如下:

// GinLogger 接收gin框架默认的日志
func GinLogger(logger *zap.Logger) gin.HandlerFunc {return func(c *gin.Context) {start := time.Now()path := c.Request.URL.Pathquery := c.Request.URL.RawQueryc.Next()cost := time.Since(start)logger.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
func GinRecovery(logger *zap.Logger, 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 boolif 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 {logger.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: errcheckc.Abort()return}if stack {logger.Error("[Recovery from panic]",zap.Any("error", err),zap.String("request", string(httpRequest)),zap.String("stack", string(debug.Stack())),)} else {logger.Error("[Recovery from panic]",zap.Any("error", err),zap.String("request", string(httpRequest)),)}c.AbortWithStatus(http.StatusInternalServerError)}}()c.Next()}
}

如果不想自己实现,可以使用github上有别人封装好的https://github.com/gin-contrib/zap。

这样我们就可以在gin框架中使用我们上面定义好的两个中间件来代替gin框架默认的Logger()和Recovery()了。

r := gin.New()
r.Use(GinLogger(), GinRecovery())

在gin项目中使用zap

最后我们再加入我们项目中常用的日志切割,完整版的logger.go代码如下:

package loggerimport ("net""net/http""net/http/httputil""os""runtime/debug""strings""time""web_app/settings""github.com/gin-gonic/gin""github.com/natefinch/lumberjack""go.uber.org/zap""go.uber.org/zap/zapcore"
)var lg *zap.Logger// 初始化Logger,用来记录日志
func Init(cfg *settings.LogConfig, mode string) (err error) {writeSyncer := getLogWriter(cfg.Filename, cfg.MaxSize, cfg.MaxBackups, cfg.MaxAge)encoder := getEncoder()var l = new(zapcore.Level)err = l.UnmarshalText([]byte(cfg.Level))if err != nil {return}var core zapcore.Coreif mode == "dev" {// 进入开发模式,日志输出到终端consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig())core = zapcore.NewTee(zapcore.NewCore(encoder, writeSyncer, l),zapcore.NewCore(consoleEncoder, zapcore.Lock(os.Stdout), zapcore.DebugLevel),)} else {core = zapcore.NewCore(encoder, writeSyncer, l)}lg := zap.New(core, zap.AddCaller())zap.ReplaceGlobals(lg) // 替换zap包中全局的logger实例,后续在其他包中只需使用zap.L()调用即可zap.L().Info("init logger success")return
}func getEncoder() zapcore.Encoder {encoderConfig := zap.NewProductionEncoderConfig()encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoderencoderConfig.TimeKey = "time"encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoderencoderConfig.EncodeDuration = zapcore.SecondsDurationEncoderencoderConfig.EncodeCaller = zapcore.ShortCallerEncoderreturn 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.Pathquery := c.Request.URL.RawQueryc.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 boolif 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: errcheckc.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()}
}

在项目中先从配置文件加载配置信息,再调用 logger.InitLogger(config.Conf.LogConfig) 即可完成logger实例的初识化。其中,通过 r.Use(logger.GinLogger(), logger.GinRecovery(true)) 注册我们的中间件来使用zap接收gin框架自身的日志,在项目中需要的地方通过使用 zap.L().Xxx() 方法来记录自定义日志信息。

初始化MySQL和Redis配置

分别编写MySQL和Redis的初始化函数和关闭函数

package mysqlimport ("fmt""web_app/settings"_ "github.com/go-sql-driver/mysql" // 导入数据库驱动"github.com/jmoiron/sqlx""go.uber.org/zap"
)var db *sqlx.DBfunc Init(cfg *settings.MySQLConfig) (err error) {dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?parseTime=true&loc=Local",cfg.User,cfg.Password,cfg.Host,cfg.Port,cfg.DB,)// 也可以使用MustConnect连接不成功就panicdb, err = sqlx.Connect("mysql", dsn)if err != nil {zap.L().Error("connect DB failed", zap.Error(err))return}db.SetMaxOpenConns(cfg.MaxOpenConns)db.SetMaxIdleConns(cfg.MaxIdleConns)return
}func Close() {_ = db.Close()
}
package redisimport ("fmt""web_app/settings""github.com/go-redis/redis"
)var client *redis.Client// 初始化连接
func Init(cfg *settings.RedisConfig) (err error) {client = redis.NewClient(&redis.Options{Addr:         fmt.Sprintf("%s:%d", cfg.Host, cfg.Port),Password:     cfg.Password, // no password setDB:           cfg.DB,       // use default DBPoolSize:     cfg.PoolSize,MinIdleConns: cfg.MinIdleConns,})_, err = client.Ping().Result()if err != nil {return}return
}func Close() {_ = client.Close()
}

初始化路由配置

此文件用来初始化gin框架路由,并将自定义的Zap日志库中的两个中间件加入路由

package routesimport ("web_app/logger""github.com/gin-gonic/gin"
)func Setup() *gin.Engine {r := gin.New()                                      //没有任何中间件r.Use(logger.GinLogger(), logger.GinRecovery(true)) //添加自定义的中间件return r
}

main函数

前面我们已经编写好了初始化配置文件、初始化日志、初始化MySQL和Redis、初始化路由的相关函数,只需在main函数中依次调用,即可进行各项配置的初始化。最后加入优雅关闭的HTTP请求的代码,能够平滑处理关闭请求而不丢失正在处理的任务。

package mainimport ("context""fmt""net/http""os""os/signal""syscall""time""web_app/dao/mysql""web_app/dao/redis""web_app/logger""web_app/routes""web_app/settings""github.com/spf13/viper""go.uber.org/zap"
)// GoWeb开发较通用脚手架模版
func main() {//1.加载配置if err := settings.Init(); err != nil {fmt.Printf("init settings failed, err:%v\n", err)return}//2.初始化日志if err := logger.Init(settings.Conf.LogConfig, settings.Conf.Mode); err != nil {fmt.Printf("init logger failed, err:%v\n", err)}defer zap.L().Sync() //延迟注册,将缓冲区日志追加到文件中//3.初始化MySQL连接if err := mysql.Init(settings.Conf.MySQLConfig); err != nil {fmt.Printf("init mysql failed, err:%v\n", err)}defer mysql.Close()//4.初始化Redis连接if err := redis.Init(settings.Conf.RedisConfig); err != nil {fmt.Printf("init redis failed, err:%v\n", err)}defer redis.Close()//5.注册路由r := routes.Setup()//6.启动服务(优雅关机)srv := &http.Server{Addr:    fmt.Sprintf(":%d", viper.GetInt("a")),Handler: r,}go func() {// 开启一个goroutine启动服务if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {zap.L().Fatal("listen: ", zap.Error(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 信号转发给quitsignal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) // 此处不会阻塞<-quit                                               // 阻塞在此,当接收到上述两种信号时才会往下执行zap.L().Info("Shutdown Server ...")// 创建一个5秒超时的contextctx, 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")
}

至此,web项目脚手架搭建完毕,可以开始进行业务代码的编写

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/61895.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

深度学习图像视觉 RKNN Toolkit2 部署 RK3588S边缘端 过程全记录

深度学习图像视觉 RKNN Toolkit2 部署 RK3588S边缘端 过程全记录 认识RKNN Toolkit2 工程文件学习路线&#xff1a; Anaconda Miniconda安装.condarc 文件配置镜像源自定义conda虚拟环境路径创建Conda虚拟环境 本地训练环境本地转换环境安装 RKNN-Toolkit2&#xff1a;添加 lin…

论文模型设置与实验数据:scBERT

Yang, F., Wang, W., Wang, F. et al. scBERT as a large-scale pretrained deep language model for cell type annotation of single-cell RNA-seq data. Nat Mach Intell 4, 852–866 (2022). https://doi.org/10.1038/s42256-022-00534-z 论文地址&#xff1a;scBERT as a…

Jenkins的环境部署

day22 回顾 Jenkins 简介 官网Jenkins Jenkins Build great things at any scale The leading open source automation server, Jenkins provides hundreds of plugins to support building, deploying and automating any project. 用来构建一切 其实就是用Java写的一个项目…

怎么编译OpenWrt镜像?-基于Widora开发板

1.准备相应的环境&#xff0c;我使用的环境是VMware16ubuntu20.04&#xff0c;如图1所示安装编译所需的依赖包&#xff1b; sudo apt-get install build-essential asciidoc binutils bzip2 gawk gettext git libncurses5-dev libz-dev patch python3 python2.7 unzip zlib1g-…

Python 获取微博用户信息及作品(完整版)

在当今的社交媒体时代&#xff0c;微博作为一个热门的社交平台&#xff0c;蕴含着海量的用户信息和丰富多样的内容。今天&#xff0c;我将带大家深入了解一段 Python 代码&#xff0c;它能够帮助我们获取微博用户的基本信息以及下载其微博中的相关素材&#xff0c;比如图片等。…

MySQL底层概述—1.InnoDB内存结构

大纲 1.InnoDB引擎架构 2.Buffer Pool 3.Page管理机制之Page页分类 4.Page管理机制之Page页管理 5.Change Buffer 6.Log Buffer 1.InnoDB引擎架构 (1)InnoDB引擎架构图 (2)InnoDB内存结构 (1)InnoDB引擎架构图 下面是InnoDB引擎架构图&#xff0c;主要分为内存结构和磁…

Linux---ps命令

​​​​​​Linux ps 命令 | 菜鸟教程 (runoob.com) process status 用于显示进程的状态 USER: 用户名&#xff0c;运行此进程的用户名。PID: 进程ID&#xff08;Process ID&#xff09;&#xff0c;每个进程的唯一标识号%CPU: 进程当前使用的CPU百分比%MEM: 进程当前使用的…

企业OA管理系统:Spring Boot技术实现与案例研究

摘要 随着信息技术在管理上越来越深入而广泛的应用&#xff0c;管理信息系统的实施在技术上已逐步成熟。本文介绍了企业OA管理系统的开发全过程。通过分析企业OA管理系统管理的不足&#xff0c;创建了一个计算机管理企业OA管理系统的方案。文章介绍了企业OA管理系统的系统分析部…

Charles抓包工具-笔记

摘要 概念&#xff1a; Charles是一款基于 HTTP 协议的代理服务器&#xff0c;通过成为电脑或者浏览器的代理&#xff0c;然后截取请求和请求结果来达到分析抓包的目的。 功能&#xff1a; Charles 是一个功能全面的抓包工具&#xff0c;适用于各种网络调试和优化场景。 它…

数据结构(顺序队列——c语言实现)

队列的概念&#xff1a; 队列是限制在两端进行插入和删除操作的线性表&#xff0c;允许进行存入的一端称为“队尾”&#xff0c;允许进行删除操作的一端称为“队头”。当线性表中没有元素时&#xff0c;称为“空队”。特点&#xff1a;先进先出&#xff08;FIFO&#xff09;。 …

Vulnhub靶场 Jangow: 1.0.1 练习

目录 0x00 准备0x01 主机信息收集0x02 站点信息收集0x03 漏洞查找与利用1. 命令执行2. 反弹shell3. 提权4. 补充4.1 其他思路4.2 问题 0x04 总结 0x00 准备 下载链接&#xff1a;https://download.vulnhub.com/jangow/jangow-01-1.0.1.ova 介绍&#xff1a; Difficulty: easy…

Fakelocation Server服务器/专业版 Centos7

前言:需要Centos7系统 Fakelocation开源文件系统需求 Centos7 | Fakelocation | 任务一 更新Centos7 &#xff08;安装下载不再赘述&#xff09; sudo yum makecache fastsudo yum update -ysudo yum install -y kernelsudo reboot//如果遇到错误提示为 Another app is curre…

【Ubuntu24.04】服务部署(虚拟机)

目录 0 背景1 安装虚拟机1.1 下载虚拟机软件1.2 安装虚拟机软件1.2 安装虚拟电脑 2 配置虚拟机2.1 配置虚拟机网络及运行初始化脚本2.2 配置服务运行环境2.2.1 安装并配置JDK172.2.2 安装并配置MySQL8.42.2.3 安装并配置Redis 3 部署服务4 总结 0 背景 你的服务部署在了你的计算…

深入解析 EasyExcel 组件原理与应用

✨深入解析 EasyExcel 组件原理与应用✨ 官方&#xff1a;EasyExcel官方文档 - 基于Java的Excel处理工具 | Easy Excel 官网 在日常的 Java 开发工作中&#xff0c;处理 Excel 文件的导入导出是极为常见的需求。 今天&#xff0c;咱们就一起来深入了解一款非常实用的操作 Exce…

Java爬虫:获取商品详情的实践之旅

在当今这个信息爆炸的时代&#xff0c;数据的价值日益凸显。对于电商行业来说&#xff0c;商品详情的获取尤为重要&#xff0c;它不仅关系到产品的销售&#xff0c;还直接影响到用户体验。传统的人工获取方式耗时耗力&#xff0c;而自动化的爬虫技术则提供了一种高效解决方案。…

C# 数据结构之【树】C#树

以二叉树为例进行演示。二叉树每个节点最多有两个子节点。 1. 新建二叉树节点模型 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks;namespace DataStructure {class TreeNode{public int Data { get;…

单片机_简单AI模型训练与部署__从0到0.9

IDE&#xff1a; CLion MCU&#xff1a; STM32F407VET6 一、导向 以求知为导向&#xff0c;从问题到寻求问题解决的方法&#xff0c;以兴趣驱动学习。 虽从0&#xff0c;但不到1&#xff0c;剩下的那一小步将由你迈出。本篇主要目的是体验完整的一次简单AI模型部署流程&#x…

【Spiffo】环境配置:VScode+Windows开发环境

摘要&#xff1a; 在Linux下直接开发有时候不习惯快捷键和操作逻辑&#xff0c;用Windows的话其插件和工具都更齐全、方便&#xff0c;所以配置一个Windows的开发环境能一定程度提升效率。 思路&#xff1a; 自己本地网络内远程连接自己的虚拟机&#xff08;假定用的是虚拟机…

使用eclipse构建SpringBoot项目

我这里用eclipse2018版本做演示&#xff0c;大家有需要的可以下载Eclipse Downloads | The Eclipse Foundation 1.打开eclipse&#xff0c;选择存放代码的位置 2.选择 file >> new >> project >> 选择springboot文件下的 spring starter project 2.这里选择N…

C++ 日期计算器的实现(运算符重载)

目录 一、前言 二、正文 1.1 Date类框架 1.2 两个日期间的直接赋值 1.3判断两个日期是否相同 1.4判断两个日期是否不同 1.5日期加天数 1.6日期天数 1.7日期的前置和后置 1.8日期减天数 1.9自身日期减天数 2.1自身日期前置--和后置-- 2.2两个日期的差值为差距天数 2…