背景
本文是基于Golang Web开发框架Gin实现的二次开发(尽量向Java风格靠拢),在Gin框架的基础上,添加了如下功能(后续再考虑提取为一个公共模块):
- 支持 配置文件,实现配置与代码解耦;
- 整合了 Zap 日志框架;
- 整合了 Gorm 持久层框架;
- 启动Gin服务
目录结构
├── server
├── resouce (配置文件)
├── profile (配置文件实体对象)
├──core (核心配置)
├──global (全局变量)
支持配置文件
- 使用 Viper插件 将 yaml文件 解析为实体对象
- 配置文件放在了
resource
目录下,其中:- application.yaml 是公共配置文件;
- 读取 环境配置文件:通过
profile.active
参数配置,读取优先级顺序:启动参数 > application.yaml; - 建议包含以下几个环境:
- local:本地(
application-local.yaml
) - sit:开发环境(
application-sit.yaml
) - beta:预发布环境(
application-beta.yaml
) - pro:生产环境(
application-pro.yaml
)
- local:本地(
viper.go
package core
import (
"flag"
"fmt"
"github.com/spf13/viper"
"inventory-management/global"
)
// LoadProfile 加载资源配置 /resource下配置文件
func LoadProfile() {
// 先加载公共配置文件,再加载环境配置文件
v := viper.New()
loadProfile(v)
// 获取配置文件激活环境
startupEnv := getStartupEnv()
if startupEnv != "" {
loadEnvProfile(v, startupEnv)
}
if err := v.Unmarshal(&global.ResourceProfile); err != nil {
panic(fmt.Errorf("unmarshal resource_config error:%w", err))
}
}
// 加载公共配置文件
func loadProfile(v *viper.Viper) {
profilePath := "./resource/application.yaml"
v.SetConfigFile(profilePath)
v.SetConfigType("yaml")
if err := v.ReadInConfig(); err != nil {
panic(fmt.Errorf("read profile file[%s] error: %s", profilePath, err))
}
}
// 加载环境配置文件(与公共配置文件合并)
func loadEnvProfile(v *viper.Viper, startupEnv string) {
profilePath := fmt.Sprintf("./resource/application-%s.yaml", startupEnv)
v.SetConfigFile(profilePath)
v.SetConfigType("yaml")
if err := v.MergeInConfig(); err != nil {
panic(fmt.Errorf("read profile file[%s] error: %s", profilePath, err))
}
}
func getStartupEnv() (startupEnv string) {
flag.StringVar(&startupEnv, "profile.active", "", "choose profile active.")
// 解析命令行参数
flag.Parse()
if startupEnv != "" {
return
}
fmt.Printf("profile.active from Program-arguments is empty, load from profile")
startupEnv = global.ResourceProfile.Profile.Active
fmt.Printf("profile.active is %s", startupEnv)
return
}
激活环境配置
这里以激活本地环境为例:
配置文件方式
- 配置文件
# 激活环境
profile:
active: local
- 配置文件结构体
package profile
type Profile struct {
Active string `mapstructure:"active" json:"active" yaml:"active"`
}
- 引入全局变量
Profile Profile `mapstructure:"profile" json:"profile" yaml:"profile"`
启动参数方式
# 在 Program-arguments 中配置
-profile.active=local
main.go
在main文件中添加如下代码
core.LoadProfile() // 加载配置文件
路由配置
这里采用分层结构
- router负责路由流转
- service负责处理实际业务
- 这里以暴露 商品相关接口 为例
router
entry.go 路由分组总入口
package router
import "inventory-management/router/system"
// RouterGroup 路由分组入口
type RouterGroup struct {
System system.System
}
// RouterGroupApp 统一对外暴露路由分组
var RouterGroupApp = new(RouterGroup)
system/entry.go
package system
import "inventory-management/service"
// System 暴露system-router
type System struct {
GoodsRouter
}
// service层注入
var (
goodsService = service.ServiceEntryApp.ServiceGroup.GoodsService
)
system/goods_router.go
package system
import (
"github.com/gin-gonic/gin"
"net/http"
)
type GoodsRouter struct{}
// GoodsReqEntry 商品请求入口
func (goodsRouter *GoodsRouter) GoodsReqEntry(Router *gin.RouterGroup) {
Router.GET("/hello", func(c *gin.Context) {
systemGoodsDo := goodsService.GetGoodsInfo()
c.JSON(http.StatusOK, systemGoodsDo)
})
}
service
entry.go
package service
import "inventory-management/service/system"
type Entry struct {
ServiceGroup system.ServiceGroup
}
var ServiceEntryApp = new(Entry)
system/entry.go
package system
type ServiceGroup struct {
GoodsService
}
system/goods_service.go
package system
import (
"inventory-management/model/system"
)
type GoodsService struct{}
func (goodService *GoodsService) GetGoodsInfo() system.GoodsInfoDo {
var systemGoodsDo = system.GoodsInfoDo{ID: 1, Name: "AAA"}
return systemGoodsDo
}
添加路由配置
package router
import (
"github.com/gin-gonic/gin"
"inventory-management/global"
"inventory-management/middleware"
"inventory-management/router"
)
// InitRouter 初始化路由(路由调用总入口)
func InitRouter() *gin.Engine {
Router := gin.New()
Router.Use(gin.Recovery())
if gin.Mode() == gin.DebugMode {
Router.Use(gin.Logger())
}
// 跨域请求访问
Router.Use(middleware.Cors())
// 上下文路径
PublicGroup := Router.Group(global.ResourceProfile.System.ContextPath)
systemRouter := router.RouterGroupApp.System
systemRouter.GoodsReqEntry(PublicGroup.Group("/goods"))
return Router
}
跨域请求中间件
package middleware
import (
"github.com/gin-gonic/gin"
"inventory-management/global"
"net/http"
)
// Cors 直接放行所有跨域请求并放行所有 OPTIONS 方法
func Cors() gin.HandlerFunc {
global.ZapLog.Info("use middleware cors...")
return func(context *gin.Context) {
method := context.Request.Method
origin := context.Request.Header.Get("Origin")
context.Header("Access-Control-Allow-Origin", origin)
context.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token, Authorization, Token,X-Token,X-User-Id")
context.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS,DELETE,PUT")
context.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type, New-Token, New-Expires-At")
context.Header("Access-Control-Allow-Credentials", "true")
// 放行所有OPTIONS方法
if method == "OPTIONS" {
context.AbortWithStatus(http.StatusNoContent)
}
// 处理请求
context.Next()
}
}
main.go
在main中添加如下代码引入路由配置,后续启动服务需要添加此路由
Router := router.InitRouter() // 配置路由
启动服务
server.go
在
goroutine
中启动服务
package core
import (
"context"
"errors"
"fmt"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func RunServer(port int, router *gin.Engine, readTimeout, writeTimeout time.Duration) {
server := http.Server{
Addr: fmt.Sprintf(":%d", port),
Handler: router,
ReadTimeout: readTimeout,
WriteTimeout: writeTimeout,
MaxHeaderBytes: 1 << 20,
}
// 在goroutine中启动服务
go func() {
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
_ = fmt.Errorf("WEB服务启动失败:%w", err)
zap.L().Error("WEB服务启动失败", zap.Error(err))
os.Exit(1)
}
}()
// 等待终端信号以优雅的关闭服务器
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
zap.L().Info("关闭WEB服务...")
// 设置5s超时时间,等待其他请求处理完成
timeout, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := server.Shutdown(timeout); err != nil {
zap.L().Fatal("WEB服务关闭异常:", zap.Error(err))
}
zap.L().Info("WEB服务已关闭")
}
配置文件
system:
port: 8888
context-path: /sale-system
read-timeout: 30 # 读取请求最大等待时间,单位:s
write-timeout: 30 # 写入响应最大等待时间,单位:s
配置文件实体类
package profile
type System struct {
Port int `mapstructure:"port" json:"port" yaml:"port"`
ContextPath string `mapstructure:"context-path" json:"context-path" yaml:"context-path"`
ReadTimeout int64 `mapstructure:"read-timeout" json:"read-timeout" yaml:"read-timeout"`
WriteTimeout int64 `mapstructure:"write-timeout" json:"write-timeout" yaml:"write-timeout"`
}
main.go
在main中添加如下代码:
core.RunServer(global.ResourceProfile.System.Port, Router,
time.Duration(global.ResourceProfile.System.ReadTimeout)*time.Second,
time.Duration(global.ResourceProfile.System.WriteTimeout)*time.Second)
整合Zap日志框架
- 日志文件以目录形式按照日期切割;
- 支持设置日志目录的保留时间;
zap_entry.go
package core
import (
"fmt"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"inventory-management/core/zap_base"
"inventory-management/global"
"inventory-management/utils"
"os"
)
func InitZap() (logger *zap.Logger) {
if isExist, _ := utils.IsExist(global.ResourceProfile.Zap.Director); !isExist {
fmt.Printf("zap_base-director[%v] not exist, create.", global.ResourceProfile.Zap.Director)
// 文件所有者有读写权限,其他用户只有读权限
_ = os.Mkdir(global.ResourceProfile.Zap.Director, 0644)
}
levels := global.ResourceProfile.Zap.Levels()
length := len(levels)
cores := make([]zapcore.Core, 0, length)
for i := 0; i < length; i++ {
core := zap_base.NewZapCore(levels[i])
cores = append(cores, core)
}
logger = zap.New(zapcore.NewTee(cores...))
if global.ResourceProfile.Zap.ShowLine {
logger = logger.WithOptions(zap.AddCaller())
}
global.ZapLog = logger
return logger
}
zap_core_base.go
package zap_base
import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"inventory-management/global"
"os"
"time"
)
type ZapCore struct {
level zapcore.Level
zapcore.Core
}
func NewZapCore(level zapcore.Level) *ZapCore {
entity := &ZapCore{level: level}
syncer := entity.WriteSyncer()
levelEnabler := zap.LevelEnablerFunc(func(l zapcore.Level) bool {
return l == level
})
entity.Core = zapcore.NewCore(global.ResourceProfile.Zap.Encoder(), syncer, levelEnabler)
return entity
}
func (z *ZapCore) WriteSyncer(formats ...string) zapcore.WriteSyncer {
cutter := NewCutter(global.ResourceProfile.Zap.Director, z.level.String(), global.ResourceProfile.Zap.RetentionDay,
CutterWithLayout(time.DateOnly), CutterWithFormats(formats...))
if global.ResourceProfile.Zap.LogInConsole { // 是否输出到控制台
multiSyncer := zapcore.NewMultiWriteSyncer(os.Stdout, cutter)
return zapcore.AddSync(multiSyncer)
}
return zapcore.AddSync(cutter)
}
func (z *ZapCore) Enabled(level zapcore.Level) bool {
return z.level == level
}
func (z *ZapCore) With(fields []zapcore.Field) zapcore.Core {
return z.Core.With(fields)
}
func (z *ZapCore) Check(entry zapcore.Entry, check *zapcore.CheckedEntry) *zapcore.CheckedEntry {
if z.Enabled(entry.Level) {
return check.AddCore(entry, z)
}
return check
}
func (z *ZapCore) Write(entry zapcore.Entry, fields []zapcore.Field) error {
for i := 0; i < len(fields); i++ {
if fields[i].Key == "business" || fields[i].Key == "folder" || fields[i].Key == "directory" {
syncer := z.WriteSyncer(fields[i].String)
z.Core = zapcore.NewCore(global.ResourceProfile.Zap.Encoder(), syncer, z.level)
}
}
return z.Core.Write(entry, fields)
}
func (z *ZapCore) Sync() error {
return z.Core.Sync()
}
cutter.go
package zap_base
import (
"os"
"path/filepath"
"sync"
"time"
)
// Cutter 实现 io.Writer 接口
// 用于日志切割, strings.Join([]string{director,layout, formats..., level+".log"}, os.PathSeparator)
type Cutter struct {
level string // 日志级别(debug, info, warn, error, dpanic, panic, fatal)
layout string // 时间格式 2006-01-02 15:04:05
formats []string // 自定义参数([]string{Director,"2006-01-02", "business"(此参数可不写), level+".log"}
director string // 日志文件夹
retentionDay int // 日志保留天数
file *os.File // 文件句柄
mutex *sync.RWMutex // 读写锁
}
// 写入及清理过期日志
func (c *Cutter) Write(bytes []byte) (n int, err error) {
c.mutex.Lock()
defer func() {
if c.file != nil {
_ = c.file.Close()
c.file = nil
}
c.mutex.Unlock()
}()
length := len(c.formats)
values := make([]string, 0, 3+length)
values = append(values, c.director)
if c.layout != "" {
values = append(values, time.Now().Format(c.layout))
}
for i := 0; i < length; i++ {
values = append(values, c.formats[i])
}
values = append(values, c.level+".log")
filename := filepath.Join(values...)
director := filepath.Dir(filename)
err = os.MkdirAll(director, os.ModePerm)
if err != nil {
return 0, err
}
err = removeNDaysFolders(c.director, c.retentionDay)
if err != nil {
return 0, err
}
c.file, err = os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
return 0, err
}
return c.file.Write(bytes)
}
// 增加日志目录文件清理 小于等于零的值默认忽略不再处理
func removeNDaysFolders(dir string, days int) error {
if days <= 0 {
return nil
}
cutoff := time.Now().AddDate(0, 0, -days)
return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() && info.ModTime().Before(cutoff) && path != dir {
err = os.RemoveAll(path)
if err != nil {
return err
}
}
return nil
})
}
func (c *Cutter) Sync() error {
c.mutex.Lock()
defer c.mutex.Unlock()
if c.file != nil {
return c.file.Sync()
}
return nil
}
type CutterOption func(*Cutter)
func CutterWithFormats(format ...string) CutterOption {
return func(cutter *Cutter) {
if len(format) > 0 {
cutter.formats = format
}
}
}
func CutterWithLayout(layout string) CutterOption {
return func(cutter *Cutter) {
if len(layout) > 0 {
cutter.layout = layout
}
}
}
func NewCutter(director string, level string, retentionDay int, options ...CutterOption) *Cutter {
cutter := &Cutter{
level: level,
director: director,
retentionDay: retentionDay,
mutex: new(sync.RWMutex),
}
for i := 0; i < len(options); i++ {
options[i](cutter)
}
return cutter
}
配置文件
# Zap日志库
zap:
level: info
format: console
director: log
show-line: true
stacktrace-key: stacktrace
log-in-console: false
retention-day: 7 # 保留7天
配置文件实体类
package profile
import (
"go.uber.org/zap/zapcore"
"time"
)
type Zap struct {
Level string `mapstructure:"level" json:"level" yaml:"level"` // 级别
Format string `mapstructure:"format" json:"format" yaml:"format"` // 输出
Director string `mapstructure:"director" json:"director" yaml:"director"` // 日志文件夹
EncodeLevel string `mapstructure:"encode-level" json:"encode-level" yaml:"encode-level"` // 编码级展示
StacktraceKey string `mapstructure:"stacktrace-key" json:"stacktrace-key" yaml:"stacktrace-key"` // 栈名
ShowLine bool `mapstructure:"show-line" json:"show-line" yaml:"show-line"` // 显示行
LogInConsole bool `mapstructure:"log-in-console" json:"log-in-console" yaml:"log-in-console"` // 输出控制台
RetentionDay int `mapstructure:"retention-day" json:"retention-day" yaml:"retention-day"` // 日志保留天数
}
// Levels 封装日志级别
func (zap *Zap) Levels() []zapcore.Level {
levels := make([]zapcore.Level, 0, 7)
parseLevel, err := zapcore.ParseLevel(zap.Level)
if err != nil {
parseLevel = zapcore.DebugLevel
}
for ; parseLevel <= zapcore.FatalLevel; parseLevel++ {
levels = append(levels, parseLevel)
}
return levels
}
// Encoder 日志打印格式
func (zap *Zap) Encoder() zapcore.Encoder {
config := zapcore.EncoderConfig{
TimeKey: "time",
NameKey: "name",
LevelKey: "level",
CallerKey: "caller",
MessageKey: "message",
StacktraceKey: zap.StacktraceKey,
LineEnding: zapcore.DefaultLineEnding,
EncodeTime: func(t time.Time, encoder zapcore.PrimitiveArrayEncoder) {
encoder.AppendString(t.Format("2006-01-02 15:04:05.000"))
},
EncodeLevel: zap.LevelEncoder(),
EncodeCaller: zapcore.FullCallerEncoder,
EncodeDuration: zapcore.SecondsDurationEncoder,
}
if zap.Format == "json" {
return zapcore.NewJSONEncoder(config)
}
return zapcore.NewConsoleEncoder(config)
}
// LevelEncoder 日志级别展示格式,默认小写(即:info debug这种格式)
func (zap *Zap) LevelEncoder() zapcore.LevelEncoder {
switch {
case zap.EncodeLevel == "LowercaseLevelEncoder": // 小写编码器(默认)
return zapcore.LowercaseLevelEncoder
case zap.EncodeLevel == "LowercaseColorLevelEncoder": // 小写编码器带颜色
return zapcore.LowercaseColorLevelEncoder
case zap.EncodeLevel == "CapitalLevelEncoder": // 大写编码器
return zapcore.CapitalLevelEncoder
case zap.EncodeLevel == "CapitalColorLevelEncoder": // 大写编码器带颜色
return zapcore.CapitalColorLevelEncoder
default:
return zapcore.LowercaseLevelEncoder
}
}
引入全局变量
ZapLog *zap.Logger
main.go
在main.go中添加如下代码:
zap.ReplaceGlobals(core.InitZap()) // 配置zap日志库
业务调用
global.ZapLog.Info("zap init success")
整合Gorm持久层框架
- 这里以mysql服务为例
gorm_config_entry
package gorm
import (
"inventory-management/config/gorm/internal"
"inventory-management/global"
)
func InitGorm() {
global.GormDB = initMySQL() // 配置数据库连接
internal.RegisterTables() // 初始化表结构
}
gorm_mysql.go
package gorm
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"inventory-management/config/gorm/internal"
"inventory-management/global"
)
// 初始化mysql连接
func initMySQL() *gorm.DB {
mysqlProfile := &global.ResourceProfile.Mysql
if mysqlProfile.Dsn == "" {
return nil
}
mysqlConfig := mysql.Config{
DSN: fmt.Sprintf(mysqlProfile.Dsn, mysqlProfile.Username, mysqlProfile.Password),
DefaultStringSize: 191, // 设置string类型字段默认长度
SkipInitializeWithVersion: false, // gorm根据根据 MySQL 服务器的版本自动进行一些初始化配置
}
if db, err := gorm.Open(mysql.New(mysqlConfig), internal.Gorm.Config()); err != nil {
panic(fmt.Errorf("gorm open error:%w", err))
} else {
db.InstanceSet("gorm:table_options", "ENGINE="+mysqlProfile.Engine) // 设置表的存储引擎
sqlDB, _ := db.DB()
sqlDB.SetMaxIdleConns(mysqlProfile.MaxIdleConns)
sqlDB.SetMaxOpenConns(mysqlProfile.MaxOpenConns)
return db
}
}
gorm_base.go
package internal
import (
"go.uber.org/zap"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
"inventory-management/global"
"inventory-management/model/system"
"os"
"time"
)
var Gorm = new(_gorm)
type _gorm struct{}
// Config gorm 自定义配置
func (g *_gorm) Config() *gorm.Config {
var general = global.ResourceProfile.Mysql
return &gorm.Config{
Logger: logger.New(NewWriter(general), logger.Config{
SlowThreshold: 200 * time.Millisecond, // 慢sql配置,与数据库引擎自带的慢sql无关
LogLevel: general.LogLevel(), // 日志级别
Colorful: true, // 采用彩色日志打印
}),
NamingStrategy: schema.NamingStrategy{
TablePrefix: general.Prefix, // 表名前缀
SingularTable: general.Singular, // 表名是否以复数命名
},
DisableForeignKeyConstraintWhenMigrating: true, // 迁移数据库时禁用外键约束
}
}
func RegisterTables() {
gormDB := global.GormDB
err := gormDB.AutoMigrate(
system.GoodsInfoDo{},
)
if err != nil {
global.ZapLog.Error("register tables fail, ", zap.Error(err))
os.Exit(0)
}
global.ZapLog.Info("register tables success.")
}
gorm_logger_writer.go
package internal
import (
"fmt"
"gorm.io/gorm/logger"
"inventory-management/global"
"inventory-management/profile"
)
type Writer struct {
config profile.GeneralDB
writer logger.Writer
}
func NewWriter(config profile.GeneralDB) *Writer {
return &Writer{config: config}
}
// Printf 实现Writer接口:格式化打印日志
func (c *Writer) Printf(message string, data ...any) {
// 当有日志时候均需要输出到控制台
fmt.Printf(message, data...)
fmt.Println()
// 当开启了zap的情况,会打印到日志记录
if c.config.LogZap {
switch c.config.LogLevel() {
case logger.Silent:
global.ZapLog.Debug(fmt.Sprintf(message, data...))
case logger.Error:
global.ZapLog.Error(fmt.Sprintf(message, data...))
case logger.Warn:
global.ZapLog.Warn(fmt.Sprintf(message, data...))
case logger.Info:
global.ZapLog.Info(fmt.Sprintf(message, data...))
default:
global.ZapLog.Info(fmt.Sprintf(message, data...))
}
return
}
}
配置文件
# MySQL数据库 loc:系统本地时区
mysql:
engine: InnoDB
dsn: "%s:%s@tcp(localhost:3306)/sale_system_db?charset=utf8mb4&parseTime=True&loc=Local"
username: test_sale
password: xxx
table-prefix: t_sale_ # 表名前缀,手动指定表名则不会生效
max-idle-conns: 10 # 空闲连接最大数量
max-open-conns: 100 # 打开连接最大数量
log-zap: false
配置文件结构体
package profile
import (
"gorm.io/gorm/logger"
"strings"
)
type GeneralDB struct {
Dsn string `mapstructure:"dsn" json:"dsn" yaml:"dsn"` // 数据源名称
Username string `mapstructure:"username" json:"username" yaml:"username"`
Password string `mapstructure:"password" json:"password" yaml:"password"`
Prefix string `mapstructure:"table-prefix" json:"table-prefix" yaml:"table-prefix"` // 数据库前缀
Engine string `mapstructure:"engine" json:"engine" yaml:"engine"` // 数据库引擎,InnoDB
LogMode string `mapstructure:"log-mode" json:"log-mode" yaml:"log-mode"` // 是否开启Gorm全局日志
MaxIdleConns int `mapstructure:"max-idle-conns" json:"max-idle-conns" yaml:"max-idle-conns"` // 空闲中的最大连接数
MaxOpenConns int `mapstructure:"max-open-conns" json:"max-open-conns" yaml:"max-open-conns"` // 打开到数据库的最大连接数
Singular bool `mapstructure:"singular" json:"singular" yaml:"singular"` // 是否开启全局禁用复数,true表示开启
LogZap bool `mapstructure:"log-zap_base" json:"log-zap_base" yaml:"log-zap_base"` // 是否通过zap写入日志文件
}
// LogLevel 输出日志级别
func (c GeneralDB) LogLevel() logger.LogLevel {
switch strings.ToLower(c.LogMode) {
case "silent":
return logger.Silent
case "error":
return logger.Error
case "warn":
return logger.Warn
case "info":
return logger.Info
default:
return logger.Info
}
}
引入全局变量
// goram
Mysql GeneralDB `mapsturcture:"mysql" json:"mysql" yaml:"mysql"`
将以上代码作为公共模块
引入公共模块common-module
此时目录结构为:
sales-system/
├── server
│ ├── common-module
│ │ ├── go.mod
│ │ └── util
│ │ └── util.go
│ ├── inventory-management
│ │ ├── go.mod
│ │ └── main.go
在 go.mod中引入 common-module
require (
common-module v0.0.0
)
replace (
common-module => ../common-module
)
将 加载配置文件、配置zap日志库、加载数据库配置、启动服务 逻辑挪到公共模块。初始化表的逻辑挪到对应业务服务中。
package main
import (
"common-module/config/gorm"
"common-module/core"
"common-module/global"
"go.uber.org/zap"
"inventory-management/config"
"inventory-management/config/router"
"time"
)
func main() {
core.LoadProfile() // 加载配置文件
zap.ReplaceGlobals(core.InitZap()) // 配置zap日志库
gorm.InitGorm() // 加载数据库配置
config.RegisterTables() // 初始化表结构
Router := router.InitRouter() // 配置路由
core.RunServer(global.ResourceProfile.System.Port, Router,
time.Duration(global.ResourceProfile.System.ReadTimeout)*time.Second,
time.Duration(global.ResourceProfile.System.WriteTimeout)*time.Second)
}
整合Swagger
下载swagger插件
go install github.com/swaggo/swag/cmd/swag@latest
初始化swagger doc
# 项目根目录下执行,执行成功后会生成doc目录
swag init
Router配置swagger接口
// 配置swagger
docs.SwaggerInfo.BasePath = global.ResourceProfile.System.ContextPath
Router.GET(global.ResourceProfile.System.ContextPath+"/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
main.go
// @title inventory-management
// @version 1.0.0
// @description 售卖系统后台管理
// @host 127.0.0.1:8888
// @BasePath /sale-system
func main() {
}
重新加载Swagger doc
- 重新执行 swag init 命令
- 重启服务
访问Swagger
localhost:8888/swagger/index.html