作者很菜,欢迎交流,不对的请指正!
使用gin构建了一个平常开发易用脚手架,代码简洁易读,可快速进行高效web开发。 主要功能有:
创建一个docs文件夹,然后获取swagger
go get -u github.com/swaggo/swag/cmd/swag
然后运行下方代码,会获得swagger.json,swagger.yaml,docs.go
swag init
通过gin渲染swagger,在注册路由的地方,绑定gin
import (
"github.com/gin-gonic/gin"
_ "go_gateway_back/docs" // 千万不要忘了导入把你上一步生成的docs
gs "github.com/swaggo/gin-swagger"
"github.com/swaggo/gin-swagger/swaggerFiles"
)
main上面
// @title 这里写标题
// @version 1.0
// @description 这里写描述信息
// @termsOfService http://swagger.io/terms/
// @contact.name 这里写联系人信息
// @contact.url http://www.swagger.io/support
// @contact.email [email protected]
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
// @host 这里写接口服务的host
// @BasePath 这里写base path
controller上面
// GetPostListHandler2 升级版帖子列表接口
// @Summary 升级版帖子列表接口
// @Description 可按社区按时间或分数排序查询帖子列表接口
// @Tags 帖子相关接口
// @Accept application/json
// @Produce application/json
// @Param Authorization header string false "Bearer 用户令牌"
// @Param object query models.ParamPostList false "查询参数"
// @Security ApiKeyAuth
// @Success 200 {object} _ResponsePostList
// @Router /posts2 [get]
对于属性直接写注解即可。
func InitModule(configPath string, modules []string) error {
conf := flag.String("config", configPath, "input config file like ./conf/dev/")
flag.Parse()
if *conf == "" {
flag.Usage()
os.Exit(1)
}
log.Println("------------------------------------------------------------------------")
log.Printf("[INFO] config=%s\n", *conf)
log.Printf("[INFO] %s\n", " start loading resources.")
// 设置ip信息,优先设置便于日志打印
ips := GetLocalIPs()
if len(ips) > 0 {
LocalIP = ips[0]
}
// 解析配置文件目录
if err := ParseConfPath(*conf); err != nil {
return err
}
//初始化配置文件
if err := InitViperConf(); err != nil {
return err
}
// 加载base配置
if InArrayString("base", modules) {
if err := InitBaseConf(GetConfPath("base")); err != nil {
fmt.Printf("[ERROR] %s%s\n", time.Now().Format(TimeFormat), " InitBaseConf:"+err.Error())
}
}
// 加载redis配置
if InArrayString("redis", modules) {
if err := InitRedisConf(GetConfPath("redis_map")); err != nil {
fmt.Printf("[ERROR] %s%s\n", time.Now().Format(TimeFormat), " InitRedisConf:"+err.Error())
}
}
// 加载mysql配置并初始化实例
if InArrayString("mysql", modules) {
if err := InitDBPool(GetConfPath("mysql_map")); err != nil {
fmt.Printf("[ERROR] %s%s\n", time.Now().Format(TimeFormat), " InitDBPool:"+err.Error())
}
}
// 设置时区
if location, err := time.LoadLocation(ConfBase.TimeLocation); err != nil {
return err
} else {
TimeLocation = location
}
log.Printf("[INFO] %s\n", " success loading resources.")
log.Println("------------------------------------------------------------------------")
return nil
}
var LocalIP = net.ParseIP("127.0.0.1")//默认是回环地址
func GetLocalIPs()(ips []net.IP) {
addrs, err := net.InterfaceAddrs()
if err != nil{
return nil
}
for _,addr := range addrs{
ipNet,ok := addr.(*net.IPNet)
if ok && !ipNet.IP.IsLoopback(){
if ipNet.IP.To4() != nil {
ips = append(ips, ipNet.IP)
}
}
}
return ips
}
func ParseFilePath(config string) error{
split := strings.Split(config, "/")
ConfDir = strings.Join(split[:len(split) - 1],"/") //文件夹
ConfEnv = split[len(split) - 2] // 配置环境
return nil
}
当我们需要将viper读取的配置反序列到我们定义的结构体变量中时,一定要使用
mapstructure
tag哦!
var ViperMap map[string]*viper.Viper
func InitViper() error{
dir, err := os.Open(ConfDir)
if err != nil{
return err
}
//返回一个最大长度为1024的文件夹下的文件切片
fileList, err := dir.Readdir(1024)
if err != nil{
return err
}
for _,f := range fileList{
if !f.IsDir(){
//得到了byte切片的文件内容,viper读入需要转换成reader
content, err := ioutil.ReadFile(ConfDir + "/" + f.Name())
if err != nil{
return err
}
v := viper.New()
v.SetConfigType("yaml")
err = v.ReadConfig(bytes.NewBuffer(content))
strs := strings.Split(f.Name(),".")
if ViperMap == nil{
ViperMap = make(map[string]*viper.Viper)
}
ViperMap[strs[0]] = v
}
}
return nil
}
base.yaml
日志使用zap日志。
此时我们就要操控viper,进行配置文件的控制了。首先我们不希望一个个getString,getBool这样获得每一个详细的子配置。我们可以把这些子配置集合成一个结构体,直接从结构体获取会好很多,所以我们有这样的方法。
func ParseConfig(path string,p interface{}) error{
f, err := os.Open(path)
if err != nil {
return fmt.Errorf("Open config %v fail, %v", path, err)
}
content, err := ioutil.ReadAll(f)
if err != nil {
return fmt.Errorf("Read config fail, %v", err)
}
v := viper.New()
v.SetConfigType("yaml")
v.ReadConfig(bytes.NewBuffer(content))
if err = v.Unmarshal(p);err != nil{
return fmt.Errorf("Parse config fail, config:%v, err:%v", string(content), err)
}
return nil
}
自定义zap日志:
func GetEncoder() zapcore.Encoder{
return zapcore.NewJSONEncoder(zapcore.EncoderConfig{
TimeKey: "ts",
LevelKey: "level",
NameKey: "logger",
CallerKey: "caller",
FunctionKey: zapcore.OmitKey,
MessageKey: "msg",
StacktraceKey: "stacktrace",
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.CapitalLevelEncoder,
EncodeTime: zapcore.ISO8601TimeEncoder,
EncodeDuration: zapcore.SecondsDurationEncoder,
EncodeCaller: zapcore.ShortCallerEncoder,
})
}
func GetWriteSyncer(logFile string) zapcore.WriteSyncer{
path := GetCurrentPath()
path = path + "/logs/" + logFile
w, err := os.Open(path)
if err != nil{
return nil
}
return zapcore.AddSync(w)
}
func GetLogLevel(level string) zapcore.Level{
switch level {
case "debug": return zapcore.DebugLevel
case "info": return zapcore.InfoLevel
case "error": return zapcore.ErrorLevel
case "warn": return zapcore.WarnLevel
default:
return zapcore.DebugLevel
}
}
上面这些自定义的日志,还差一种功能,就是日志切割归档功能。为了添加日志切割归档功能,我们将使用第三方库Lumberjack来实现。
func GetWriteSyncer(logFile string) zapcore.WriteSyncer{
path := GetCurrentPath()
path = path + "/logs/" + logFile
logger := &lumberjack.Logger{
Filename: path,
MaxSize: Conf.Log.MaxSize,
MaxAge: Conf.Log.MaxAge,
MaxBackups: Conf.Log.MaxBackup,
Compress: false,
}
return zapcore.AddSync(logger)
}
其实我们更希望日志在开发环境时也可以打印在控制台上,对我们开发方便一点。又可以将代码升级为:
可以使用Zap.NewTee同时使用两个核心,一个核心打印到控制台,一个记录到文件!岂不美哉!
func InitBase(path string)error{
err := ParseConfig(path, Conf)
if err != nil {
return err
}
level := Conf.Log.Level
var core zapcore.Core
if Conf.DebugMode == "DEBUG"{
//在控制台输入
consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig())
core = zapcore.NewTee(
zapcore.NewCore(consoleEncoder,zapcore.Lock(os.Stdout),zapcore.DebugLevel),
zapcore.NewCore(GetEncoder(), GetWriteSyncer(Conf.Log.LogName), GetLogLevel(level)),
)
}else{
core = zapcore.NewCore(GetEncoder(), GetWriteSyncer(Conf.Log.LogName), GetLogLevel(level))
}
//需要一个Core,Core需要Encoder,WriteSyncer,LogLevel。
logger := zap.New(core,zap.AddCaller())
//替换全局日志
zap.ReplaceGlobals(logger)
return nil
}
全部代码:
package sys_init
import (
"bytes"
"fmt"
"github.com/spf13/viper"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"io/ioutil"
"log"
"os"
"strings"
"github.com/natefinch/lumberjack"
)
var Conf = new(BaseConfig)
type BaseConfig struct {
DebugMode string `mapstructure:"debug-mode"`
TimeLocation string `mapstructure:"time-location"`
Log LogConfig `mapstructure:"log"`
}
type LogConfig struct {
Level string `mapstructure:"level"`
LogName string `mapstructure:"logName"`
MaxSize int `mapstructure:"max-size"`
MaxAge int `mapstructure:"max-age"`
MaxBackup int `mapstructure:"max-backup"`
}
func InitBase(path string)error{
err := ParseConfig(path, Conf)
if err != nil {
return err
}
level := Conf.Log.Level
var core zapcore.Core
if Conf.DebugMode == "DEBUG"{
//在控制台输入
consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig())
core = zapcore.NewTee(
zapcore.NewCore(consoleEncoder,zapcore.Lock(os.Stdout),zapcore.DebugLevel),
zapcore.NewCore(GetEncoder(), GetWriteSyncer(Conf.Log.LogName), GetLogLevel(level)),
)
}else{
core = zapcore.NewCore(GetEncoder(), GetWriteSyncer(Conf.Log.LogName), GetLogLevel(level))
}
//需要一个Core,Core需要Encoder,WriteSyncer,LogLevel。
logger := zap.New(core,zap.AddCaller())
//替换全局日志
zap.ReplaceGlobals(logger)
return nil
}
func GetCurrentPath() string {
dir, err := os.Getwd()
if err != nil {
log.Fatal(err)
}
return strings.Replace(dir, "\\", "/", -1)
}
func GetEncoder() zapcore.Encoder{
return zapcore.NewJSONEncoder(zapcore.EncoderConfig{
TimeKey: "ts",
LevelKey: "level",
NameKey: "logger",
CallerKey: "caller",
FunctionKey: zapcore.OmitKey,
MessageKey: "msg",
StacktraceKey: "stacktrace",
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.CapitalLevelEncoder,
EncodeTime: zapcore.ISO8601TimeEncoder,
EncodeDuration: zapcore.SecondsDurationEncoder,
EncodeCaller: zapcore.ShortCallerEncoder,
})
}
func GetWriteSyncer(logFile string) zapcore.WriteSyncer{
path := GetCurrentPath()
path = path + "/logs/" + logFile
logger := &lumberjack.Logger{
Filename: path,
MaxSize: Conf.Log.MaxSize,
MaxAge: Conf.Log.MaxAge,
MaxBackups: Conf.Log.MaxBackup,
Compress: false,
}
return zapcore.AddSync(logger)
}
func GetLogLevel(level string) zapcore.Level{
switch level {
case "debug": return zapcore.DebugLevel
case "info": return zapcore.InfoLevel
case "error": return zapcore.ErrorLevel
case "warn": return zapcore.WarnLevel
default:
return zapcore.DebugLevel
}
}
func ParseConfig(path string,p interface{}) error{
f, err := os.Open(path)
if err != nil {
return fmt.Errorf("Open config %v fail, %v", path, err)
}
content, err := ioutil.ReadAll(f)
if err != nil {
return fmt.Errorf("Read config fail, %v", err)
}
v := viper.New()
v.SetConfigType("yaml")
v.ReadConfig(bytes.NewBuffer(content))
if err = v.Unmarshal(p);err != nil{
return fmt.Errorf("Parse config fail, config:%v, err:%v", string(content), err)
}
return nil
}
最后就要整合到Gin中!我们看看Gin的Default如何实现的。
所以我们需要模仿这个,用zap实现Logger和Recovery
//接受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
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()
}
}
首先看看mysql的配置信息:
在mysql.go文件中初始化了这几个属性:
var MysqlPoolMap map[string]*sql.DB
var GormPoolMap map[string]*gorm.DB
var MysqlDefaultConn *sql.DB
var GormDefaultConn *gorm.DB
主要就是通过配置文件取到dsn,创建响应的mysql.db和gorm.db,值得注意的是我将Logger配置到了控制台
package sys_init
import (
"errors"
"fmt"
"gorm.io/gorm"
"database/sql"
logger2 "gorm.io/gorm/logger"
"log"
"os"
"time"
"gorm.io/driver/mysql"
)
type MysqlMapConf struct{
List map[string]*MysqlConf `mapstructure:"list"`
}
type MysqlConf struct{
DriverName string `mapstructure:"driver-name"`
DataSourceName string `mapstructure:"data-source-name"`
MaxOpenConn int `mapstructure:"max-open-conn"`
MaxIdleConn int `mapstructure:"max-idle-conn"`
MaxConnLifeTime int `mapstructure:"max-conn-life-time"`
}
var MysqlPoolMap map[string]*sql.DB
var GormPoolMap map[string]*gorm.DB
var MysqlDefaultConn *sql.DB
var GormDefaultConn *gorm.DB
func InitMysql(path string)error{
m := &MysqlMapConf{}
if err := ParseConfig(path, m);err != nil{
return err
}
if len(m.List) == 0{
fmt.Printf("[INFO] %s%s\n", time.Now().Format(TimeFormat), " empty mysql config.")
}
MysqlPoolMap = make(map[string]*sql.DB)
GormPoolMap = map[string]*gorm.DB{}
//mysql的日志
logger := logger2.New(log.New(os.Stdout,"\r\n",log.LstdFlags),logger2.Config{LogLevel: logger2.Info})
for k,v := range m.List{
data, err := sql.Open("mysql", v.DataSourceName)
if err != nil{
return err
}
data.SetMaxOpenConns(v.MaxOpenConn)
data.SetMaxIdleConns(v.MaxIdleConn)
data.SetConnMaxLifetime(time.Duration(v.MaxConnLifeTime ) * time.Second)
g, err := gorm.Open(mysql.New(mysql.Config{
Conn: data,
}), &gorm.Config{Logger: logger})
if err != nil{
return err
}
MysqlPoolMap[k] = data
GormPoolMap[k] = g
}
if err, db := GetMysqlConn("default");err == nil{
MysqlDefaultConn = db
}
if err, db := GetGormConn("default");err == nil{
GormDefaultConn = db
}
return nil
}
func GetMysqlConn(name string) (error,*sql.DB){
db,ok := MysqlPoolMap[name]
if !ok{
return errors.New("no match mysql connection"),nil
}
return nil,db
}
func GetGormConn(name string) (error,*gorm.DB){
db,ok := GormPoolMap[name]
if !ok{
return errors.New("no match gorm connection"),nil
}
return nil,db
}
func CloseDB() error {
for _, dbpool := range MysqlPoolMap {
dbpool.Close()
}
MysqlPoolMap = make(map[string]*sql.DB)
GormPoolMap = make(map[string]*gorm.DB)
return nil
}
读取Redis,相当于暴露读取完了的结构体。
package sys_init
type RedisConfMap struct {
list map[string]*RedisConf `mapstructure:"redis"`
}
type RedisConf struct {
ProxyList []string `mapstructure:"proxy_list"`
Password string `mapstructure:"password"`
Db int `mapstructure:"db"`
ConnTimeout int `mapstructure:"conn_timeout"`
ReadTimeout int `mapstructure:"read_timeout"`
WriteTimeout int `mapstructure:"write_timeout"`
}
var ConfRedis *RedisConfMap
func InitRedis(path string)error{
r := &RedisConfMap{}
if err := ParseConfig(path, r);err != nil{
return err
}
ConfRedis = r
return nil
}
RedisFactory
func RedisFactory(name string)(redis.Conn,error){
if ConfRedis != nil && ConfRedis.list != nil{
for n,v := range ConfRedis.list{
if name == n{
//默认值
if v.ConnTimeout == 0 {
v.ConnTimeout = 50
}
if v.ReadTimeout == 0 {
v.ReadTimeout = 100
}
if v.WriteTimeout == 0 {
v.WriteTimeout = 100
}
ranHost := v.ProxyList[rand.Intn(len(v.ProxyList))]
conn, err := redis.Dial(
"tcp",
ranHost,
redis.DialConnectTimeout(time.Duration(v.ConnTimeout)*time.Millisecond),
redis.DialReadTimeout(time.Duration(v.ReadTimeout)*time.Millisecond),
redis.DialWriteTimeout(time.Duration(v.WriteTimeout)*time.Millisecond))
if err != nil{
return nil,err
}
if v.Password != ""{
_, err := conn.Do("auth", v.Password)
if err != nil{
return nil,AuthError
}
}
if v.Db != 0{
_, err := conn.Do("select", v.Db)
if err != nil{
return nil,SelectError
}
}
return conn,nil
}
}
}
return nil, CreateError
}
在Redis运行时,进行了日志处理
func RedisConfDo(name string, commandName string, args ...interface{}) (interface{}, error) {
c, err := RedisFactory(name)
if err != nil {
zap.L().Sugar().Errorf("_com_redis_failure,method:%v,err:%v,args:%v",commandName,errors.New("RedisConnFactory_error:" + name),args)
return nil, err
}
defer c.Close()
startExecTime := time.Now()
reply, err := c.Do(commandName, args...)
endExecTime := time.Now()
if err != nil {
zap.L().Sugar().Errorf("_com_redis_failure,method:%v,err:%v,args:%v,time:%v",commandName,errors.New("RedisConnFactory_error:" + name),args,fmt.Sprintf("%fs", endExecTime.Sub(startExecTime).Seconds()))
} else {
replyStr, _ := redis.String(reply, nil)
zap.L().Sugar().Errorf("_com_redis_success,method:%v,err:%v,args:%v,time:%v,reply:%v",commandName,errors.New("RedisConnFactory_error:" + name),args,fmt.Sprintf("%fs", endExecTime.Sub(startExecTime).Seconds()),replyStr)
}
return reply, err
}
package route
import (
"github.com/gin-gonic/gin"
"go_gateway_back/controller"
_ "go_gateway_back/docs" // 千万不要忘了导入把你上一步生成的docs
gs "github.com/swaggo/gin-swagger"
"github.com/swaggo/gin-swagger/swaggerFiles"
)
//参数就相当于中间件,其参数实现了ServeHttp,通过next来到下一个中间件
func InitRoute(middleware ...gin.HandlerFunc) *gin.Engine{
engine := gin.Default()
engine.GET("/swagger/*any",gs.WrapHandler(swaggerFiles.Handler))
engine.GET("/hello",controller.HelloHandler)
return engine
}
package route
import (
"context"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"go_gateway_back/sys_init"
"net/http"
"time"
)
var HttpServer *http.Server
func StartServer(){
gin.SetMode(sys_init.Conf.DebugMode)
route := InitRoute()
HttpServer = &http.Server{
Addr: sys_init.Conf.Addr,
Handler: route,
ReadTimeout: time.Duration(sys_init.Conf.ReadTimeout) * time.Second,
WriteTimeout: time.Duration(sys_init.Conf.ReadTimeout) * time.Second,
MaxHeaderBytes: 1 << uint(sys_init.Conf.MaxHeaderBytes),
}
go func() {
zap.L().Sugar().Infof(" [INFO] HttpServerRun:%s\n",sys_init.Conf.Addr)
if err := HttpServer.ListenAndServe();err != nil{
zap.L().Sugar().Fatalf(" [ERROR] HttpServerRun:%s err:%v\n", sys_init.Conf.Addr, err)
}
}()
}
func StopServer(){
//该方法返回一个Deadline为10s后的ctx
ctx, cancelFunc := context.WithTimeout(context.Background(), 10*time.Second)
defer cancelFunc()
if err := HttpServer.Shutdown(ctx);err != nil{
zap.L().Sugar().Fatalf(" [ERROR] HttpServerStop err:%v\n", err)
}
zap.L().Sugar().Infof(" [INFO] HttpServerStop stopped\n")
}
quit := make(chan os.Signal)
signal.Notify(quit, syscall.SIGKILL, syscall.SIGQUIT, syscall.SIGINT, syscall.SIGTERM)
<-quit
在git bash中写代码
git config --global user.name "user.name"
git config --global user.email "你的邮箱"
ssh-keygen -t rsa -C "你的邮箱"
整合goland
那么我的代码上传到了Gitee:
https://gitee.com/sekiro-phm/gin_web_structure