Golang:开发Web程序

发布于:2025-07-02 ⋅ 阅读:(24) ⋅ 点赞:(0)

背景

本文是基于Golang Web开发框架Gin实现的二次开发(尽量向Java风格靠拢),在Gin框架的基础上,添加了如下功能(后续再考虑提取为一个公共模块):

  1. 支持 配置文件,实现配置与代码解耦;
  2. 整合了 Zap 日志框架;
  3. 整合了 Gorm 持久层框架;
  4. 启动Gin服务

目录结构

	├── server
		├── resouce          (配置文件)
		├── profile          (配置文件实体对象)
		├──core              (核心配置)
		├──global            (全局变量)

支持配置文件

  1. 使用 Viper插件yaml文件 解析为实体对象
  2. 配置文件放在了 resource 目录下,其中:
    1. application.yaml 是公共配置文件;
    2. 读取 环境配置文件:通过 profile.active 参数配置,读取优先级顺序:启动参数 > application.yaml
    3. 建议包含以下几个环境:
      • local:本地(application-local.yaml
      • sit:开发环境(application-sit.yaml
      • beta:预发布环境(application-beta.yaml
      • pro:生产环境(application-pro.yaml

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

  1. 重新执行 swag init 命令
  2. 重启服务

访问Swagger

localhost:8888/swagger/index.html

Swagger页面


网站公告

今日签到

点亮在社区的每一天
去签到