天天看點

go-gin架構二次封裝go-sword詳解(配置篇)

这是我所参与搭建的第三个框架,还继续以以前的风格,从main函数开始拆解分析这个框架,是目前我所能接触和实现的一个最为有技术含量的go的后端项目框架。

main函数

go-gin架構二次封裝go-sword詳解(配置篇)
这个注释的作用是swagger生成接口文档的第一步即:添加注释
   具体:
   @title  这里写的是标题
   @version	这里是版本号
   @description	这是对这个项目的一个描述
   
   @contact.name	这个写的是联系人的名字
   @contact.url 这里写的是联系网址
   @contact.email	这里写的是联系邮箱
   
   @host	这里写的是接口服务的host
   @BasePath	这里写base path
           
go-gin架構二次封裝go-sword詳解(配置篇)
定义命令行flag参数的两种方式之一		
Go语言内置的flag包实现了命令行参数的解析,flag包使得开发命令行工具更为简单。
flag.StringVar(type指针,flag名,默认值,帮助信息):用来 
定义好o命令行flag参数后,调用flag.Parse()来对命令行参数进行解析
支持的命令行参数格式有以下几种:
           
go-gin架構二次封裝go-sword詳解(配置篇)

config.Setup()该方法的作用是载入配置文件,具体如下(讲解一下config):

这个文件代码较多,涉及也较广,具体如下:

下面这些变量是子树。原因:所有配置写在一个文件,里面是针对不同方面的不同配置,所以使用下面这种方式来定义每一块儿配置
           
// Mysql配置项
var cfgMysql *viper.Viper

// 应用配置项
var cfgApplication *viper.Viper

// Token配置项
var cfgJwt *viper.Viper

// Log配置项
var cfgLogger *viper.Viper

// Redis配置项
var cfgRedis *viper.Viper
           
下面这个函数的作用是载入配置文件(我会对其进行拆解,一小块一小块的解析):
//载入配置文件
func Setup(path string) {
}
           
下面这一系列方法作用:
    viper.SetConfigFile()指定配置文件
    ioutil.ReadFile()读取文件的方式之一,传入文件名字,返回一个byte[]和一个err,会将文件的内容做为一个字节数组返回,如果报错则打印。
    使用ioutil.ReadFile()注意:①不需要手动 打开与关闭文件,系统会帮我们自动完成;②只适合于小文件
           
// 加载配置文件
	viper.SetConfigFile(path)
	content, err := ioutil.ReadFile(path)
	if err != nil {
		log.Fatal(fmt.Sprintf("Read config file fail: %s", err.Error()))
	}
           
下面这些方法作用:
  string(content):将[]byte类型数据转为string类型
  os.ExpandEnv():把字符串的s替换成环境变量的内容,函数的原形是func ExpandEnv(s string) string
  strings.NewReader():创建一个从s读取数据的Reader,返回的*Reader中的i指的是读取到的位置
  viper.ReadConfig():viper 支持从io.Reader中读取配置。这种形式很灵活,来源可以是文件,也可以是程序中生成的字符串,甚至可以从网络连接中读取的字节流。
           
go-gin架構二次封裝go-sword詳解(配置篇)
//Replace environment variables
	err = viper.ReadConfig(strings.NewReader(os.ExpandEnv(string(content))))
	if err != nil {
		log.Fatal(fmt.Sprintf("Parse config file fail: %s", err.Error()))
	}
           

viper.Sub():访问嵌套的键

通过传入.分隔的路径来访问嵌套字段

将不同模块儿的配置信息存给不同的变量。

然后在不同位置初始化(添加配置信息)各自模块。

cfgMysql = viper.Sub("settings.mysql")
	if cfgMysql == nil {
		panic("No found settings.mysql in the configuration")
	}
	MysqlConfig = InitMySql(cfgMysql)

	cfgJwt = viper.Sub("settings.jwt")
	if cfgJwt == nil {
		panic("No found settings.jwt in the configuration")
	}
	JwtConfig = InitJwt(cfgJwt)

	cfgRedis = viper.Sub("settings.redis")
	if cfgRedis == nil {
		panic("No found settings.redis in the configuration")
	}
	RedisConfig = InitRedis(cfgRedis)
           
下面这个是mysql数据库的初始化:
定义mysql结构体
然后对其进行初始化:值通过viper的方式传递。
返回的类型是Mysql类型的指针
以下这一系列方法都有一个特点:
即最后一行代码(很特别的一种写法):
声明一个对象供外部使用。
上面的代码即是这种方式实现的。
           
type Mysql struct {
	User      string
	Password  string
	Host      string
	DbName    string
	Port      int
	DbMaxOpen int
	DbMaxIdle int
}
//  InitMySql 初始化mysql配置
func InitMySql(cfg *viper.Viper) *Mysql {

	db := &Mysql{
		User:      cfg.GetString("user"),
		Password:  cfg.GetString("password"),
		Host:      cfg.GetString("host"),
		DbName:    cfg.GetString("dbname"),
		Port:      cfg.GetInt("port"),
		DbMaxOpen: cfg.GetInt("maxopen"),
		DbMaxIdle: cfg.GetInt("maxidle"),
	}
	return db
}

var MysqlConfig = new(Mysql)
           

下面这个是redis初始化配置操作

Redis里的字段作用:

poolSize:最大连接数(设置为0则视情况而定)

IdleTimeOutSec:最大空闲连接时间

DB:数据库(redis一共十六个数据库)

Port:端口

Password:密码(一般不设密码)

Host:服务器地址

type Redis struct {
	PoolSize      int
	IdleTimeOutSec int
	DB             int
	Port           int
	Password       string
	Host           string
}

// InitRedis 初始化redis配置
func InitRedis(cfg *viper.Viper) *Redis {

	db := &Redis{
		PoolSize:        cfg.GetInt("poolsize"),
		IdleTimeOutSec: cfg.GetInt("idletimeoutsec"),
		DB:             cfg.GetInt("db"),
		Port:           cfg.GetInt("port"),
		Host:           cfg.GetString("host"),
		Password:       cfg.GetString("password"),
	}
	return db
}

var RedisConfig = new(Redis)

           
下面是jwt初始化配置操作(加盐+设置超时时间)
type Jwt struct {
	Secret  string
	Timeout int64
}
// InitJwt 初始化jwt配置
func InitJwt(cfg *viper.Viper) *Jwt {
	return &Jwt{
		Secret:  cfg.GetString("secret"),
		Timeout: cfg.GetInt64("timeout"),
	}
}

var JwtConfig = new(Jwt)
           
回到main方法
// 2. 初始化日志
	if err := logger.Init(config.LoggerConfig, config.ApplicationConfig.Mode); err != nil {
		fmt.Printf("init logger failed, err:%v\n", err)
		return
	}
	defer zap.L().Sync()
	zap.L().Debug(utils.Green("logger init success..."))
           
初始化日志操作,传入两个参数:一个是日志配置信息,另一个是模式(主要是开发模式和生产模式)
 
  下面这个方法就是初始化日志操作,返回一个错误。
  主要是想使用定制的logger:将日志写入文件而不是终端
  内部代码见下图:
           

传入指定四个参数,返回一个zapcore.WriteSyncer对象(下)

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.Core
	if mode == string(utils.ModeProd) {
		// 进入生产模式
		core = zapcore.NewCore(encoder, writeSyncer, l)
	} else {
		// 进入开发模式或测试模式
		consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig())
		core = zapcore.NewTee(
			zapcore.NewCore(encoder, writeSyncer, l),
			zapcore.NewCore(consoleEncoder, zapcore.Lock(os.Stdout), l),
		)
	}

	lg := zap.New(core, zap.AddCaller())
	zap.ReplaceGlobals(lg) // 替换zap包中全局的logger实例,后续在其他包中只需使用zap.L()调用即可
	return
           
具体步骤一:WriterSyncer :指定日志将写到哪里去。
  但是如果想实现将日志切割归档,则需要使用第三方库Lumberjack来实现,
  要在zap中加入Lumberjack支持,我们需要修改WriteSyncer代码
  其中Logger属性的意思如下:
    Filename: 日志文件的位置
	MaxSize:在进行切割之前,日志文件的最大大小(以MB为单位)
	MaxBackups:保留旧文件的最大个数
	MaxAges:保留旧文件的最大天数
	Compress:是否压缩/归档旧文件
  最后使用zapcore.AddSync()函数并且将打开的文件句柄传进去		
           
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)
}
           
具体步骤二:Encoder:编码器(如何写入日志)
  使用开箱即用的NewJSONEncoder()
  并使用预先设置的ProductionEncoderConfig()
  添加一些其他信息(覆盖默认的):
  		encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder // 修改时间编码器
  		encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder// 在日志文件中使用大写字母记录日志级别
  		EncodeDuration:一般zapcore.SecondsDurationEncoder,执行消耗的时间转化成浮点型的秒
  		EncodeCaller:一般zapcore.ShortCallerEncoder,以包/文件:行号 格式化调用堆栈    caller:可以以相对格式short和绝对格式full显示
  		TimeKey:输出时间的key名
           
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)
}
           
  • 下面两行代码的主要作用是:设置日志级别
  • 级别是日志记录的优先级,级别越高越重要。实例化level,然后调用其UnmarshalText()方法,
var l = new(zapcore.Level)
	err = l.UnmarshalText([]byte(cfg.Level))
	if err != nil {
		return
	}
           
通过zapcore.NewCore创建一个core,将日志写入WriteSyncer。	
  zapcore.NewConsoleEncoder():将日志信息在控制台打印(将编码器从JSON Encoder更改为普通Encoder)
  通过调用zap.NewProduction()/zap.NewDevelopment()或者zap.Example()创建一个Logger。其中的每一个函数都将创建一个logger。唯一的区别在于它将记录的信息不同。NewDevelopment()方法打印内容更详细。
  zap.NewDevelopmentEncoderConfig():该方法返回一个初始化好的zapcore.EncoderConfig(编码器)
           
var core zapcore.Core
	if mode == string(utils.ModeProd) {
		// 进入生产模式
		core = zapcore.NewCore(encoder, writeSyncer, l)
	} else {
		// 进入开发模式或测试模式
		consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig())
		core = zapcore.NewTee(
			zapcore.NewCore(encoder, writeSyncer, l),
			zapcore.NewCore(consoleEncoder, zapcore.Lock(os.Stdout), l),
		)
	}
           
zap.New(…)方法来手动传递所有配置来创建日志实例    func New(core zapcore.Core, options ...Option) *Logger
zap.AddCaller():添加将调用函数信息记录到日志中的功能
           
lg := zap.New(core, zap.AddCaller())
	zap.ReplaceGlobals(lg) // 替换zap包中全局的logger实例,后续在其他包中只需使用zap.L()调用即可
	return
           
Sync调用底层核心的Sync方法,刷新任何缓冲的日志条目。应用程序在退出之前应该注意调用sync。
zap.L().Debug():记录日志
这里面调用了utils里面的一个方法。
           
defer zap.L().Sync()
	zap.L().Debug(utils.Green("logger init success..."))
           
进入utils.setText文件
先看批量声明的变量:const中每新增一行常量声明将使iota计数一次
           
package utils

import (
	"fmt"
)

// 前景 背景 颜色
// ---------------------------------------
// 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)
}
           
继续main方法:
初始化MySQL连接
对于mysql.Init()方法下面会进行拆分。
关闭连接这一部分:
把*gorm.DB放到一个公共目录中,哪里需要哪里调
           
// 3. 初始化MySQL连接
	if err := mysql.Init(config.MysqlConfig); err != nil {
		zap.L().Error(fmt.Sprintf("init mysql failed, err:%v\n", err))
		return
	}
	defer mysql.Close()
	zap.L().Debug(utils.Green("mysql init success..."))

	// Close 关闭连接
	func Close() {
		db, err := global.Eloquent.DB()
		if err != nil {
			zap.L().Error("db close err", zap.Error(err))
		}
		_ = db.Close()
	}
           
传入的是mysql的配置信息,使用一个结构体存储
charset=utf8mb4:这种字符编码方式可以处理表情包
parseTime=true&loc=Local说明会解析时间,时区是机器的local时区。机器之间的时区可能不一致会设置有问题,这导致从相同库的不同实例查询出来的结果可能解析以后就不一样。因此推荐将loc统一设置为一个时区
timeout=1000ms:连接超时
在控制台打印提示信息
           
// Init 配置mysql gorm
func Init(cfg *config.Mysql) (err error) {
	source := fmt.Sprintf("%s:%[email protected](%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local&timeout=1000ms",
		cfg.User,
		cfg.Password,
		cfg.Host,
		cfg.Port,
		cfg.DbName,
	)
	zap.L().Info(utils.Green(source))
           
设置最大连接数和最大空闲连接数
           
db.SetMaxOpenConns(cfg.DbMaxOpen)
	db.SetMaxIdleConns(cfg.DbMaxIdle)
           
下面这一波操作作用是:与数据建立连接
zap.L().xxx():xxx表示的是打印日志的级别,不同级别的日志信息颜色不同。
           
zap.L().Info(utils.Green(source))
	db, err := sql.Open("mysql", source)
	if err != nil {
		zap.L().Fatal(utils.Red("mysql connect error :"), zap.Error(err))
		return
	}

	db.SetMaxOpenConns(cfg.DbMaxOpen)
	db.SetMaxIdleConns(cfg.DbMaxIdle)

	global.Eloquent, err = open(db, &gorm.Config{
			NamingStrategy: schema.NamingStrategy{
			SingularTable: true,
		},
	})
	if err != nil {
		zap.L().Fatal(utils.Red("mysql connect error :"), zap.Error(err))
		return
	} else {
		zap.L().Info(utils.Green("mysql connect success !"))
	}
           
调用下面的方法,返回一个抽象的gorm数据库。
连接数据库比较简单,直接传入地址即可。下面这种是较为高级的用法:通过一个现有的数据库连接来初始化 *gorm.DB
GORM 允许用户通过覆盖默认的NamingStrategy来更改命名约定,这需要实现1245/*--*接口 Namer
SingularTable: true, // 使用单数表名,启用该选项后,`User` 表将是`user`
           
// Open 打开数据库连接
func open(db *sql.DB, cfg *gorm.Config) (*gorm.DB, error) {
	return gorm.Open(mysql.New(mysql.Config{Conn: db}), cfg)
}
           
判断生成的连接池是否有错误
           
if global.Eloquent.Error != nil {
		zap.L().Fatal(utils.Red(" database error :"), zap.Error(global.Eloquent.Error))
		return
	}
           
GORM 定义一个 gorm.Model 结构体,可以将其嵌入到您的结构体中,以包含这几个字段
AutoMigrate 用于自动迁移您的 schema,保持您的 schema 是最新的(根据模型对象建表,没有则创建,修改则自动修改)
           
err = migrateModel()
	if err != nil {
		zap.L().Fatal(utils.Red(" migrateModel error :"), zap.Error(err))
	}
	return
	// 这两部分代码不在一个文件中
	func migrateModel() error {
	err := orm.Eloquent.AutoMigrate(&models.SysUser{})
	return err
}


package models
type SysUser struct {
	*BaseModel
	Username     string `json:"username"`
	Password     string `json:"password"`
	DeptId       int    `json:"dept_id"`        //部门id
	PostId       int    `json:"post_id"`        //
	RoleId       int    `json:"role_id"`        //
	NickName     string `json:"nick_name"`      //
	Phone        string `json:"phone"`          //
	Email        string `json:"email"`          //
	AvatarPath   string `json:"avatar_path"`    //头像路径
	Avatar       string `json:"avatar"`         //
	Sex          string `json:"sex"`            //
	Status       string `json:"status"`         //
	Remark       string `json:"remark"`         //
	Salt         string `json:"salt"`           //
	Gender       []byte `json:"gender"`         //性别(0为男默认,1为女)
	IsAdmin      []byte `json:"is_admin"`       //是否为admin账号
	Enabled      []byte `json:"enabled"`        //状态:1启用(默认)、0禁用
	PwdResetTime int64  `json:"pwd_reset_time"` //修改密码的时间
	CreateBy     int    `json:"create_by"`      //
	UpdateBy     int    `json:"update_by"`      //
}


package models
// BaseModel orm公有字段
type BaseModel struct {
	ID         int    `gorm:"primary_key" json:"id"`                   //ID
	IsDeleted  []byte `gorm:"default:[]byte{0}" json:"is_deleted"`     //默认为零
	CreateTime int64  `gorm:"autoCreateTime:milli" json:"create_time"` //创建日期 默认当前时间戳 毫秒
	UpdateTime int64  `gorm:"autoUpdateTime:milli" json:"update_time"` //更新时间 默认当前时间戳 毫秒
}
           

至此,MySQL数据库初始化结束,

下面代码是初始化redis连接

// 4. 初始化Redis连接
	if err := redis.Init(config.RedisConfig); err != nil {
		zap.L().Error(fmt.Sprintf("init redis failed, err:%v\n", err))
		return
	}
	defer redis.Close()
	zap.L().Debug(utils.Green("redis init success..."))
           

通过 redis.NewClient 函数即可创建一个 redis 客户端

这个方法接收一个 redis.Options 对象参数, 通过这个参数, 我们可以配置 redis 相关的属性, 例如 redis 服务器地址, 数据库名, 数据库密码等.

通过 cient.Ping() 来检查是否成功连接到了 redis 服务器

// Init 初始化redis连接
func Init(cfg *config.Redis) (err error) {
	global.Rdb = redis.NewClient(&redis.Options{
		Addr: fmt.Sprintf("%s:%d",
			cfg.Host,
			cfg.Port,
		),
		Password: cfg.Password, // no password set
		DB:       cfg.DB,       // use default DB
		PoolSize: cfg.PoolSize,
		IdleTimeout: time.Duration(cfg.IdleTimeOutSec),
	})

	_, err = global.Rdb.Ping().Result()
	return
}

func Close() {
	_ = global.Rdb.Close()
}
           

初始化casbin

//初始化casbin
	if err := mycasbin.Setup(); err != nil {
		zap.L().Error("casbin failed set up", zap.Error(err))
	}
           

gormadapter.NewAdapterByDB(): // 将数据库连接同步给插件, 插件用来操作数据库,生成mySQL适配器

 可以先将字符串解析为Model对象,然后再创建Enforcer:

enforcer, err = casbin.NewSyncedEnforcer(m): // 在多线程环境下使用Enforcer对象的接口,必须使用casbin.NewSyncedEnforcer创建Enforcer

casbin简述:

 Model存储:

  在 Casbin 中, 访问控制模型被抽象为基于 PERM (Policy, Effect, Request, Matcher) 的一个文件

  Model语法:

   Request定义:[requset_definition]部分用于request的定义,sub:访问实体(subject),obj:访问资源(object)和访问方法(Action)

   Policyt定义:对policy的定义,policy部分的每一行称之为一个策略规则

   matchers定义:定义了策略匹配者。匹配者是一组表达式。它定义了如何根据请求来匹配策略规则

  与policy不同,model只能加载,不能保存(如下图的text)

   使用加载模型(这里使用从字符串中加载model):model.NewModelFromString(): //可以从多行字符串加载整个模型文本。这种方法的优点是您不需要维护模型文件。

// Initialize the model from a string.
var text = `
[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = r.sub == p.sub && (keyMatch2(r.obj, p.obj) || keyMatch(r.obj, p.obj)) && (r.act == p.act || p.act == "*")
`

func Setup() (err error) {
	//var err error
	var Apter *gormAdapter.Adapter
	var m model.Model
	var e *casbin.SyncedEnforcer

	Apter, err = gormAdapter.NewAdapterByDB(global.Eloquent)
	if err != nil {
		zap.L().Error("NewAdapterByDB()", zap.Error(err))
		return err
	}

	m, err = model.NewModelFromString(text)
	if err != nil {
		zap.L().Error("NewModelFromString()", zap.Error(err))
		return err
	}
	e, err = casbin.NewSyncedEnforcer(m, Apter)
	if err != nil {
		zap.L().Error("NewSyncedEnforcer()", zap.Error(err))
		return err
	}
	global.CasbinEnforcer = e
	return nil
}
           

截至现在,终于把开篇的配置环境讲解完毕,下一篇就开始我们的正题。。。。。。