当前位置: 首页 > article >正文

1.项目初始化

目录

1.相关联的数据库表

2.使用gorm操作数据库

使用gen生成model和对数据库的操作

3.使用viper进行配置管理

读取配置文件

进行热更新

4.使用Pflag来进行命令行参数解析

5.使用日志slog

日志轮转与切割功能

6.错误码和http返回格式标准化

提供错误码

提供错误类型

提供统一的http返回格式

7.使用gin构建web服务器

路由管理

实现优雅关闭 


项目地址: https://github.com/liwook/PublicReview

1.相关联的数据库表

创建的sql语句保存在dianping.sql文件中。其中已经添加了一些用户信息和商户的信息。

该章节对应的文件目录:

 

2.使用gorm操作数据库

数据库是使用MySql。我们使用第三方的开源库 gorm来操作数据库。gorm是目前 Go 语言中最流行的 ORM 库,同时也是一个功能齐全且对开发人员友好的 ORM 库。

创建internal/db目录,在这创建mysql.go文件。

var DBEngine *gorm.DB

func NewMySQL() (*gorm.DB, error) {
	dsn := "root:123456@tcp(127.0.0.1:3306)/mydbname?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		return nil, err
	}

	sqlDB, err := db.DB()
	if err != nil {
		return nil, err
	}
	sqlDB.SetMaxOpenConns(100) //设置数据库连接池最大连接数
	sqlDB.SetMaxIdleConns(30)  //连接池最大允许的空闲连接数,如果没有sql任务需要执行的连接数大于MaxIdleConns,超过的连接会被连接池关闭
	return db, nil
}

提示:要是出现错误gorm.io/plugin/dbresolver@v1.2.1/dbresolver.go:139:18: cannot use map[string]gorm.Stmt{} (value of type map[string]gorm.Stmt) as type map[string]*gorm.Stmt in struct literal。

解决方案是:执行 go get gorm.io/plugin/dbresolver@latest 把 gorm.io/plugin/dbresolver 升级到最新版本 。

之后需要写数据库表的表结构和表的一些操作。

使用gen生成model和对数据库的操作

以前需要手动写每个数据库表的model和自己写增删改查。而GEN 是一个基于 GORM 的安全 ORM 框架,其主要通过代码生成方式实现 GORM 代码封装。旨在安全上避免业务代码出现 SQL 注入,同时给研发带来最佳用户体验。

官方文档:Gen Guides | GORM - The fantastic ORM library for Golang, aims to be developer friendly.

而现有库gen,写极少代码就可以生成表结构和表操作的代码,很方便。

创建cmd目录,在该目录下创建generate目录,在generate目录添加generate.go文件。添加如下代码。这样就可以生成表的结构体和对表操作的一些函数。

// 这里使用的是gorm.io/gen@v0.3.16
func main() {
	db, err := db.NewMySQL()
	if err != nil {
		panic(err)
	}

	g := gen.NewGenerator(gen.Config{
		// OutPath是相对执行`go run`时的路径
		OutPath: "./dal/query",    //代码生成的路径
		// ModelPkgPath:      "./dal/model",不写是最好,不然就出现目录:dal/dal/model
		Mode:              gen.WithDefaultQuery | gen.WithoutContext | gen.WithQueryInterface,
		FieldNullable:     false,
		FieldCoverable:    false,
		FieldSignable:     true,
		FieldWithIndexTag: false,
		FieldWithTypeTag:  true,
	})
	g.UseDB(db)
	dataMap := map[string]func(detailType string) (dataType string){
		"tinyint":  func(detailType string) (dataType string) { return "int8" },
		"smallint": func(detailType string) (dataType string) { return "int16" },
		"bigint":   func(detailType string) (dataType string) { return "int64" },
		"int":      func(detailType string) (dataType string) { return "int64" },
	}

	g.WithDataTypeMap(dataMap)

	autoUpdateTimeField := gen.FieldGORMTag("modified_on", "column:modified_on;type:int unsigned;autoUpdateTime")
	autoCreateTimeField := gen.FieldGORMTag("created_on", "column:created_on;type:int unsigned;autoCreateTime")
	// softDeleteField := gen.FieldType("deleted_on", "soft_delete.DeletedAt")
	// 模型自定义选项组
	fieldOpts := []gen.ModelOpt{autoCreateTimeField, autoUpdateTimeField}

	allModel := g.GenerateAllTable(fieldOpts...)
	g.ApplyBasic(allModel...)
	g.Execute()
}

3.使用viper进行配置管理

我们一般不会在代码中把一些可能会改变的参数写成常量,比如监听的端口。我们一般是配置在配置文件中,然后程序读取配置文件。

创建configs目录,该目录是用来存放配置文件,html等文件的。在该目录中添加文件config.yaml。

Server:
  RunMode: debug
  HttpPort: 10000
  ReadTimeout: 60
  WriteTimeout: 60
mysql:
  Username: root   # 填写你的数据库账号
  Password: 123456 # 填写你的数据库密码
  Host: 127.0.0.1:3306
  DBName: dianping
  MaxIdleConns: 30
  MaxOpenConns: 100

然后在internal目录下创建config目录,在该目录中新建 config.go 文件,用于声明配置属性的结构体并编写读取段配置的配置方法。

var (
	ServerOption *ServerSetting
	MysqlOption  *MysqlSetting
)

type ServerSetting struct {
	RunMode      string
	HttpPort     string
	ReadTimeout  time.Duration
	WriteTimeout time.Duration
}

type MysqlSetting struct {
	UserName     string
	Password     string
	Host         string
	DbName       string
	MaxIdleConns int
	MaxOpenConns int
}

读取配置文件

编写读取配置文件的方法,按照每个结构体来读取,即是分段读取。

//config.go

//打开配置文件进行读取
func ReadConfigFile() error {
	//viper是可以开箱即用的,这样写法就类似单例模式
	//也可以创建viper 比如 vp:=viper.New()
	viper.SetConfigFile("../configs/config.yaml") // 指定配置文件名和位置
	return viper.ReadInConfig()
}

//分段读取
func ReadSection(key string, v any) error {
	return viper.UnmarshalKey(key, v)
}

进行读取。

func init() {
	InitConfig()
}

func InitConfig() {
	if err := ReadConfigFile(); err != nil {
		panic(err)
	}

	err := ReadSection("server", &ServerOption)
	if err != nil {
		panic(err)
	}
	err = ReadSection("mysql", &MysqlOption)
	if err != nil {
		panic(err)
	}
}

在cmd目录中添加main.go文件,添加代码进行读取配置文件并打印。因为config.go文件使用了init函数,所以在main.go文件中无需手动进行初始化,会自动使用init函数。

package main

import (
	"dianping/internal/config"
	"fmt"
)

func main() {
	fmt.Println("Hello, World!")
	fmt.Println(config.ServerOption)
	fmt.Println(config.MysqlOption)
}

接着修改下mysql.go文件中的NewMySQL函数,现在是使用了参数设置。

//mysql.go
func NewMySQL(config *config.MysqlSetting) (*gorm.DB, error) {
	dsn := fmt.Sprintf(`%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=true&loc=Local`,
		config.UserName,
		config.Password,
		config.Host,
		config.DbName)
    ..............
	sqlDB.SetMaxOpenConns(config.MaxOpenConns) //设置数据库连接池最大连接数
	sqlDB.SetMaxIdleConns(config.MaxIdleConns) //连接池最大允许的空闲连接数,如果没有sql任务需要执行的连接数大于MaxIdleConns,超过的连接会被连接池关闭

    //添加这句
    query.SetDefault(db) //设置了才能使用query包,这样方便
}

//generate.go的也要修改
var options config.MysqlSetting

func init() {
	err := config.ReadConfigFile()
    ..........
	err = config.ReadSection("mysql", &options)
}

func main() {
	db, err := db.NewMySQL(&options)
	if err != nil {
		panic(err)
	}
    ................
}

进行热更新

我们也会有些要求,想要在程序运行中,更改了配置文件后,程序不用重启也可以生效。viper也可以实现该效果。使用如下代码可以实现其效果,就是设置回调函数。注意:WatchConfig要使用在前面。

func ReadConfigFile() error {
	//viper是可以开箱即用的,这样写法就类似单例模式
	//也可以创建viper 比如 vp:=viper.New()
	viper.SetConfigFile("./configs/config.yaml")
	if err := viper.ReadInConfig(); err != nil {
		return err
	}
	
	viper.WatchConfig() //该函数内部是开启了一个新协程去监听配置文件是否更新
	//设置回调函数
	viper.OnConfigChange(func(in fsnotify.Event) {
		reloadAllSection()
		fmt.Println("更新了 :", in.Name)
	})

	return nil
}

在配置文件有更新后,就需要使用函数reloadAllSection再次读取配置并重新赋值给之前的变量。

把要解析的配置存储在key,而ServerOption等存储在value。之后有更新时候,使用变量sections的key来更新其value,那么该ServerOption也就更新了。

ReadSection函数也有改动,每次读取时候,往sections添加key,value。

var sections = make(map[string]any)

func ReadSection(key string, v any) error {
	err := viper.UnmarshalKey(key, v)
	if err != nil {
		return nil
	}

	//增加读取section的存储记录,以便在重新加载配置的方法中进行处理
	if _, ok := sections[key]; !ok {
		sections[key] = v
	}
	return nil
}

reloadAllSection函数就是在配置文件更新时候会执行的函数,其内容也就是重新读取文件嘛。

// 用于重新读取配置
func reloadAllSection() error {
	for k, v := range sections {
		if err := ReadSection(k, v); err != nil {
			return nil
		}
	}
	return nil
}

 在main.go中进行测试,程序运行后修改配置文件,之后查看打印出来的是否有改动即可。

注意:这个热更新尽量少用。因为有些在程序启动后一些参数就不能改动的。比如http的监听端口,在程序启动后,你再更新配置文件也不会起作用的了。

这个热更新可以用于更新日志等级。这样不重启程序,新的日志等级也会在程序中生效。

func main() {
	go func() {
		for {
			fmt.Println(*global.ServerOption)
			fmt.Println(*global.DatabaseOption)
			fmt.Println()
            time.Sleep(5 * time.Second)
		}
	}()
    
    time.Sleep(20 * time.Second)
}

4.使用Pflag来进行命令行参数解析

Go服务开发中,经常需要给开发的组件加上各种启动参数来配置服务进程,影响服务的行为。一些大型服务就有多达上百个启动参数,而且这些参数的类型各不相同(例如:string、int、ip类型等),使用方式也不相同(例如:需要支持--长选项,-短选项等),所以我们需要一个强大的命令行参数解析工具。

比如我们的配置文件位置改变了,那我们就读取不了配置,所以需要可以在命令行来获取配置文件位置。比如执行 ./main --config=./config.yaml。

Go源码中提供了一个标准库Flag包,用来对命令行参数进行解析,但在大型项目中应用更广泛的是另外一个包:Pflag

Pflag提供了很多强大的特性,非常适合用来构建大型项目。Pflag有很多功能,这里就只介绍和本项目相关的部分。

想要手动指定配置文件的位置。可以使用pflag.StringP函数。参数config是对应的长选项,c是短选项。"../configs/config.yaml"是默认值。

//main.go
func init() {
	configPath := pflag.StringP("config", "c", "../configs/config.yaml", "config file path")
	pflag.Parse()
    fmt.Println("path:", *configPath)
	config.InitConfig(*configPath)
    ..................
}

func main() {
    ............................
}


//这样,就需要修改config.go文件,不在该文件提供init函数。
//config.go

//不再使用
// func init() {
// 	InitConfig()
// }

func ReadConfigFile(path string) error {
	viper.SetConfigFile(path) // 指定配置文件名和位置
    ............
}

func InitConfig(path string) {
	if err := ReadConfigFile(path); err != nil {
		panic(err)
	}

	err := ReadSection("server", &ServerOption)
	if err != nil {
		panic(err)
	}
	err = ReadSection("mysql", &MysqlOption)
	if err != nil {
		panic(err)
	}
	err = ReadSection("log", &LogOption)
	if err != nil {
		panic(err)
	}
}

查看效果

5.使用日志slog

在程序出错的时候,我们会想记录一些出错的位置和原因。这时我们就需要使用日志来记录了。

Go语言中有自己的官方日志库slog,也支持结构化。

创建pkg目录,在该目录下创建logger目录,创建log.go文件。

// 初始化后,日志使用直接 slog.Info("dfsf")就行
func InitLogger(level string) error {
	file, err := os.OpenFile("dianping.log", os.O_CREATE|os.O_APPEND, 0666)
	if err != nil {
		return err
	}
    //使用json格式
	logger := slog.New(slog.NewJSONHandler(file, &slog.HandlerOptions{
		AddSource: true,
		Level:     LogLevel(level),
	}))
	slog.SetDefault(logger)
	return nil
}

// 获得日志等级
func LogLevel(level string) slog.Level {
	switch strings.ToLower(level) {
	case "debug":
		return slog.LevelDebug
	case "info":
		return slog.LevelInfo
	case "warn":
		return slog.LevelWarn
	case "error":
		return slog.LevelError
	}
	return slog.LevelInfo
}

日志轮转与切割功能

要是不断记录日志,日志文件会越来越大。单个文件过大会影响写入效率,那我们就会希望在日志文件达到一个大小上限后,自动开启一个新文件再记录日志。还有  最多保留可以保留多少个日志文件,最多保留多少天,要不要做压缩处理?而这些使用库lumberjack都可以方便做到

lumberjack 是一个专门设计用于日志轮转和切割的库,其作用可以类比于一个可插拔的组件。我们可以通过配置该组件,并将其 集成 到所选的日志库中,从而实现日志文件的轮转与切割功能。

有v2.0版本,需要go get gopkg.in/natefinch/lumberjack.v2。

初始化 lumberjack 组件的代码如下所示:

log := &lumberjack.Logger{
    Filename:   "/path/file.log", // 日志文件的位置
    MaxSize:    10, // 文件最大尺寸(以MB为单位)
    MaxBackups: 3, // 保留的最大旧文件数量
    MaxAge:     28, // 保留旧文件的最大天数
    Compress:   true, // 是否压缩/归档旧文件
    LocalTime:  true, // 使用本地时间创建时间戳
}

需要注意的是, lumberjackLogger 结构体实现了 io.Writer 接口。这意味着所有关于日志文件的轮转与切割的核心逻辑都封装在 Write 方法中。这一实现也方便 Logger 结构体被集成到任何支持 io.Writer 参数的日志库中。

在log.go中添加日志参数变量,并进行解析。之后在config.yaml添加log的参数。

//config
var LogOption    *logger.LogSetting

//并进行解析.............

//config.yaml
log :
  Filename  : ../dianping.log
  level : debug
  MaxSize : 10 #mb
  MaxBackups :  10 #保留的最大文件个数
  MaxAge    : 30 #保留的最大天数

修改InitLogger函数。

// 日志选项结构体
type LogSetting struct {
	Filename   string
	Level      slog.Level
	MaxSize    int
	MaxBackups int
	MaxAge     int
}


var LogLevel = new(slog.LevelVar)

// 使用lumberjack库将日志轮转与切割
func InitLogger(config *LogSetting) error {
	log := lumberjack.Logger{
		Filename:   config.Filename,   //日志文件的位置
		MaxSize:    config.MaxSize,    //文件最大尺寸(以mb为单位)
		MaxBackups: config.MaxBackups, //保留的最大文件个数
		MaxAge:     config.MaxAge,     //保留旧文件的最大天数
		LocalTime:  true,              //使用本地时间创建时间戳
	}

	LogLevel.Set(GetLogLevel(config.Level)) //这样就可以在运行时更新日志等级

	//使用json格式
	logger := slog.New(slog.NewJSONHandler(&log, &slog.HandlerOptions{
		AddSource: true,
		Level:     LogLevel,
	}))

	slog.SetDefault(logger)
	return nil
}

要可以在热更新时候使日志等级生效,需要修改热更新函数。在设置回调函数时候,更新日志等级。

func ReadConfigFile() error {
    .................
	viper.WatchConfig() //该函数内部是开启了一个新协程去监听配置文件是否更新
	//设置回调函数
	viper.OnConfigChange(func(in fsnotify.Event) {
		reloadAllSection()
		//查看是否有更新了日志等级
		level := viper.GetString("log.level")
		// fmt.Println("new_level:", level)
		logger.LogLevel.Set(logger.GetLogLevel(level))
	})
}

在mian.go中进行测试。config.yaml中level是info,之后修改为debug,这样不用重启程序,debug等级的也可打印出来。

func init() {
	err := logger.InitLogger(config.LogOption)
	if err != nil {
		panic(err)
	}
}

func main() {
	fmt.Println(config.LogOption)

	go func() {
		for {
			slog.Info("Error")
			slog.Error("Error")
			slog.Debug("debug")
			time.Sleep(5 * time.Second)
		}
	}()

	time.Sleep(30 * time.Second)
}

6.错误码和http返回格式标准化

首先,我们要对错误码有统一的处理,哪个错误码对应哪个错误。接着,服务端返回错误给客户端时候,我们也要统一错误信息的格式,这样客户端解析的时候就可以统一解析。

提供错误码

创建pkg/code文件夹,添加commoncode.go文件,添加如下代码:

使用的httpcode就使用200、400、401、403、404、500这6个HTTP错误码,不需要过多的HTTP错误码展示给用户。

var OnlyUseHTTPStatus = map[int]bool{200: true, 400: true, 401: true, 403: true, 404: true, 500: true}

//http状态码 5开头表示服务器端错误。4开头表示客户端错误

// 基础错误
// code must start with 1xxxxx
const (
	ErrSuccess int = iota + 100001
	ErrUnknown
	ErrBind
	ErrValidation   //validation failed
	ErrTokenInvalid //token invalid
)

// 数据库类错误
const (
	ErrDatabase int = iota + 100101
)

// 认证授权类错误
const (
	ErrEncrypt int = iota + 100201
	ErrSignatureInvalid
	ErrExpired
	ErrInvalidAuthHeader
	ErrMissingHeader //The `Authorization` header was empty.
	ErrPasswordIncorrect
	ErrPermissionDenied //Permission denied.
)

// 编解码类错误
const (
	// ErrEncodingFailed - 500: Encoding failed due to an error with the data.
	ErrEncodingFailed int = iota + 100301
	ErrDecodingFailed
	ErrInvalidJSON
	ErrEncodingJSON
	ErrDecodingJSON
	// ErrInvalidYaml - 500: Data is not valid Yaml.
	ErrInvalidYaml
	ErrEncodingYaml
	ErrDecodingYaml
)

提供错误类型

添加code.go。错误码用来指代一个错误类型,该错误类型需要包含一些有用的信息,例如对应的HTTP Status Code、对外展示的Message,以及跟该错误匹配的帮助文档。所以,我们还需要实现一个Coder来承载这些信息。我们定义了ErrCode结构体:

type Errcode struct {
	code    int
	HTTP    int
	message string
}

func (coder Errcode) Error() string {
	return coder.message
}

func (coder Errcode) Code() int {
	return coder.code
}

func (coder Errcode) String() string {
	return coder.message
}

func (coder Errcode) HTTPStatus() int {
	if coder.HTTP == 0 {
		return 500
	}
	return coder.HTTP
}

之后再用前面的错误码和错误码结构体进行注册,方便后续的使用。

var codes = map[int]*Errcode{}
var codeMux = &sync.Mutex{}

func register(code int, httpStaus int, message string) {
	if code == 0 {
		panic("code 0 is reserved")
	}
	if _, ok := OnlyUseHTTPStatus[httpStaus]; !ok {
		panic("httstatuscode and code  are not good")
	}

	codeMux.Lock()
	defer codeMux.Unlock()
	errcode := &Errcode{
		code:    code,
		HTTP:    httpStaus,
		message: message,
	}
	codes[code] = errcode
}

func ParseCoder(code int) *Errcode {
	if coder, ok := codes[code]; ok {
		return coder
	}

	return &Errcode{code: 1, HTTP: http.StatusInternalServerError, message: "unknown error"}
}

 添加code_generated.go文件,进行注册Coder。

func init() {
	register(ErrSuccess, 200, "OK")
	register(ErrUnknown, 500, "Internal server error")
	register(ErrBind, 400, "Error occurred while binding the request body to the struct")
    ............................
}

提供统一的http返回格式

添加response.go文件,在该文件添加response结构体:

// Response defines project response format
// 使用json标签中的omitempty选项来实现当字段为空值时不返回该字段
type Response struct {
	Code    int    `json:"code,omitempty"`
	Message string `json:"message,omitempty"`
	Data    any    `json:"data,omitempty"`
}

// WriteResponse used to write an error and JSON data into response.
func WriteResponse(c *gin.Context, code int, data interface{}) {

	coder := ParseCoder(code)
	if coder.HTTPStatus() != http.StatusOK {
		c.JSON(coder.HTTPStatus(), Response{
			Code:    coder.Code(),
			Message: coder.String(),
			Data:    data,
		})
		return
	}

	c.JSON(http.StatusOK, Response{Data: data})
}

7.使用gin构建web服务器

路由管理

创建目录internal/routeer,在该目录添加router.go文件。该文件是对所有的路由进行管理的。

func NewRouter() *gin.Engine {
	r := gin.Default()

	r.GET("/ping", func(c *gin.Context) {
		code.WriteResponse(c, code.ErrSuccess, "pong")
	})

	r.GET("/test", func(c *gin.Context) {
		code.WriteResponse(c, code.ErrDatabase, "not find this data")
	})

	r.GET("/test2", func(c *gin.Context) {
		code.WriteResponse(c, code.ErrDatabase, nil)
	})
	return r
}

 在main.go文件中测试

func main() {
	r := router.NewRouter()

	err := r.Run(":" + config.ServerOption.HttpPort)
	if err != nil {
		panic(err)
	}
}

实现优雅关闭 

对于一个web服务器,客户端正在使用,而这时服务器却关闭了,用户可能会收到错误消息,并且不知道发生了什么。所以要是服务器可以等待已连接的所有数据处理完所有用户,并且这个时段也不再接收客户端的新请求。

  1. 使用信号监听。通过监听操作系统信号来触发服务器的停止。当接收到特定信号(如 os.Interrupt 或 syscall.SIGTERM)时,开始执行停止流程。
  2. 使用 http.Server 内置的 Shutdown() 方法优雅地关机。
func main() {
	r := router.NewRouter()

	// err := r.Run(":" + config.ServerOption.HttpPort)
	// if err != nil {
	// 	panic(err)
	// }

	//创建HTTP服务器
	server := http.Server{
		Addr:    ":" + config.ServerOption.HttpPort,
		Handler: r,
	}

	go func() {
		err := server.ListenAndServe()
		if err != nil {
			panic(err)
		}
	}()

	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) // syscall.SIGKILL是无法捕捉的
	<-quit
	fmt.Println("shutdown server...")

	//创建超时上下文,Shutdown可以让未处理的连接在这个时间内关闭
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	
	if err := server.Shutdown(ctx); err != nil {
		panic(err)
	}
	fmt.Println("server shutdown success")
}


http://www.kler.cn/news/353473.html

相关文章:

  • 关键词提取技术:TF-IDF 详解
  • react页面跳转时携带参数,返回时能展示跳转之前的数据
  • informer学习笔记
  • SpringBoot请求注解详解
  • 【云从】四、私有网络VPC
  • 学习threejs,THREE.LineDashedMaterial 虚线材质,基于gosper高斯帕曲线生成雪花动画
  • 【c++】左值右值
  • 记一次ruoyi站点突破到内网后渗透
  • 谷歌新安装包文件形式 .aab 在UE4中的打包原理
  • 【AI知识】KNN算法原理代码示例可视化
  • 前端布局与响应式设计综合指南(三)
  • 电力建设中的常见翻译场景
  • PCL 点云配准 KD-ICP算法(精配准)
  • Invoke 和 InvokeRequired以及他们两个的区别
  • C++/初识C++
  • 【C语言】一维数组应用Fibonacci数列
  • 中文文本内容模板式总结,gpto1,claude某言初步测评
  • Vscode+Pycharm+Vue.js+WEUI+django火锅(五)Django的API
  • 021 elasticsearch索引管理
  • 10.10 QT服务器与客户端