A general web scaffold based on gin framework -- CLD layering concept

Posted by knight on Thu, 16 Dec 2021 07:50:42 +0100

catalogue

preface

Premises

CLD layering concept

Gin web scaffolding

config - global configuration information

settings - set configuration information

logger logging

dao database configuration

Use of MySQL slqx

Introduction to the use of redis redis

Routes - routing

controllers

logic

main.go

preface

In my cognitive world, the layered concept of MVC can't keep up with the pace of the times, in the era of separation of front and rear ends.

Premises

Tools used

vcsode

go module

CLD layering concept

1.controller -- control interface

2.local -- logic processing

3.dao -- database management

Gin web scaffolding

config - global configuration information

The file type is yaml

config.yaml

app:
  name: "bubble"
  mode: "dev"
  port: 8081

log:
  level: "debug"
  filename: "bubble.log"
  max_size: 200
  max_age: 30
  max_backups: 7

mysql:
  host: "127.0.0.1"
  port: "3306"
  user: "root"
  password: "xxxxxxxx"
  dbname: "sql_demo"
  max_open_conns: 
  max_idle_conns:

redis:
  host: "127.0.0.1"
  port: 6379
  password: ""
  db: 0
  pool_size: 100

Why does this folder exist?

Think about it. When we modify the configuration information, such as buying a new server, we need to modify a series of values of the host and port that need to be changed. We will modify the configuration in various files. The process of search and modification is time-consuming and laborious. At this time, the programmer's "laziness" comes into play, creating a new folder with configuration information. When looking, it's clear at a glance.

Note that on the Linux server, we can use the Docker container to host the code, which can be easily migrated to different servers

settings - set configuration information

Initialization of viper- Introduction and use of viper Library

func Init() (err error) {
	viper.SetConfigFile("config.yaml") // Specify profile
	viper.AddConfigPath("./config/")   // Specify the path to find the configuration file
	err = viper.ReadInConfig()         // Read configuration information
	if err != nil {                    // Failed to read configuration information
		return err
	}

	// Monitor profile changes
	viper.WatchConfig()
	viper.OnConfigChange(func(in fsnotify.Event) {
		fmt.Println("The configuration file has been modified")
	})
	return 
}

The effect of using this library is to get twice the result with half the effort and obtain configuration information.

logger logging

Initialization of zap- zap receives the default log of gin

// InitLogger initialization Logger
func Init() (err error) {
	writeSyncer := getLogWriter(
		viper.GetString("log.filename"),
		viper.GetInt("log.max_size"),
		viper.GetInt("log.max_backups"),
		viper.GetInt("log.max_age"),
	)
	encoder := getEncoder()
	var l = new(zapcore.Level)
	err = l.UnmarshalText([]byte(viper.GetString("log.level")))
	if err != nil {
		return
	}
	core := zapcore.NewCore(encoder, writeSyncer, l)

	lg := zap.New(core, zap.AddCaller())

	zap.ReplaceGlobals(lg) // Replace the global logger instance in the zap package. In other packages, you only need to use zap L () can be called
	return
}

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)
}

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 receives the default log of the gin framework
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 := 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 recovers the possible Panics of the project and uses zap to record relevant logs
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()
	}
}

During initialization, the middleware should be rewritten to overwrite the default log of gin

dao database configuration

mysql-Use of slqx

var db *sqlx.DB

func Init() (err error) {
	//fmt.Sprintf splicing function
	dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True",
		viper.GetString("mysql.user"),
		viper.GetString("mysql.password"),
		viper.GetString("mysql.host"),
		viper.GetInt("mysql.port"),
		viper.GetString("mysql.dbname"),
	)
	// You can also use MustConnect to panic if the connection is unsuccessful
	db, err = sqlx.Connect("mysql", dsn)
	if err != nil {
		zap.L().Error("connect db err", zap.Error(err))
		return
	}
	db.SetMaxOpenConns(viper.GetInt("mysql.max_open_conns"))
	db.SetMaxIdleConns(viper.GetInt("mysql.max_idle_conns"))
	return
}


func Close (){
	 _ = db.Close()
}

Don't forget to import anonymously

	_ "github.com/go-sql-driver/mysql"

The viper and zap libraries are used in the code

redis-Introduction to redis

// Declare a global rdb variable
var rdb *redis.Client

// Initialize connection
func Init() (err error) {
	rdb = redis.NewClient(&redis.Options{
		Addr:     fmt.Sprintf("%s:%d", 
		viper.GetString("redis.host"),
		viper.GetInt("redis.port"),
	),
		Password: viper.GetString("redis.password"), // no password set
		DB:       viper.GetInt("redis.db"),  // use default DB
		PoolSize: viper.GetInt("reids.pool_size"),
	})

	_, err = rdb.Ping().Result()
	return 
}


func Close (){
	_ = rdb.Close()
}

The configuration information is in config yaml

Routes - routing

func Setup() *gin.Engine {
	r := gin.New()
	//Use of Middleware
	r.Use(logger.GinLogger(),logger.GinRecovery(true))
	r.GET("/",func(c *gin.Context) {
		c.String(http.StatusOK,"ok")
	})
	return r;
}

controllers

According to the actual business scenario

logic

According to the actual business scenario

main.go

func main() {
	//load configuration
	if err := logger.Init(); err != nil {
		fmt.Printf("init settings failed,err:%v\n", err)
		return
	}
	//Initialize log
	if err := settings.Init(); err != nil {
		fmt.Printf("init settings failed,err:%v\n", err)
		return
	}
	defer zap.L().Sync()
	zap.L().Debug("logger init succ")
	//Initialize Mysql database
	
	if err := mysql.Init(); err != nil {
		fmt.Printf("init settings failed,err:%v\n", err)
		return
	}
	mysql.Close()
	//Initialize redis
	if err := redis.Init(); err != nil {
		fmt.Printf("init settings failed,err:%v\n", err)
		return
	}
	redis.Close()
	//Register routing
	r := routers.Setup()
	//Start service (graceful shutdown)
	srv := &http.Server{
		Addr:    fmt.Sprintf(":%d", viper.GetInt("app.port")),
		Handler: r,
	}

	go func() {
		// Start a goroutine startup service
		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			log.Fatalf("listen: %s\n", err)
		}
	}()

	// Wait for the interrupt signal to gracefully shut down the server, and set a 5-second timeout for the server shutdown operation
	quit := make(chan os.Signal, 1) // Create a channel to receive signals
	// kill will send syscall by default SIGTERM signal
	// kill -2 send syscall SIGINT signal, the commonly used Ctrl+C is the SIGINT signal that triggers the system
	// kill -9 send syscall Sigkill signal, but it can't be captured, so it doesn't need to be added
	// signal.Notify sends the received syscall SIGINT or syscall SIGTERM signal forwarded to quit
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) // There will be no blocking here
	<-quit                                               // Blocking here, when the above two signals are received, it will be executed downward
	log.Println("Shutdown Server ...")
	// Create a 5-second timeout context
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	// Close the service gracefully within 5 seconds (finish processing the unprocessed requests and then close the service). If it exceeds 5 seconds, it will timeout and exit
	if err := srv.Shutdown(ctx); err != nil {
		log.Fatal("Server Shutdown: ", err)
	}

	log.Println("Server exiting")
}

summary

The basic scaffold has been completed and can meet the general needs. In view of the limited capacity, it will be continuously updated later.

Topics: Go Web Development