【Go】基于 Gin 从0到1搭建 Web 管理后台系统后端服务(一)项目初始化、配置和日志(上)

简介: 【Go】基于 Gin 从0到1搭建 Web 管理后台系统后端服务(一)项目初始化、配置和日志(上)

image.png

1、项目初始化

1.1 目录结构

├── api
|   ├── v1 # v1版本接口服务
|       ├── system # 系统级服务
|       └── enter.go # 统一入口
├── config # 配置相关
├── core # 核心模块
├── dao # dao层
├── global # 全局变量
├── initialize # 配置启动初始化
├── middleware # 中间件
├── model # 数据库结构体
├── router # 路由
├── service # 业务层
├── utils # 工具函数
├── config.yaml # 配置文件
├── go.mod # 包管理
├── main.go # 项目启动文件
└── README.md # 项目README

image.png

1.2 go.mod

使用以下命令初始化:

mkdir ewa_admin_server
cd ewa_admin_server
# init go.mod
go mod init ewa_admin_server

就会在根目录下生成一个包管理配置文件 go.mod

一般来说,基本的后端服务都需要包括配置解析、日志、数据库连接等流程,新建入口文件 main.go

package main
import "fmt"
func main() {
   fmt.Println("hello world")
   // TODO:1.配置初始化
   // TODO:2.日志
   // TODO:3.数据库连接
   // TODO:4.其他初始化
   // TODO:5.启动服务
}

1.3 引入 gin

安装依赖

go get -u github.com/gin-gonic/gin

接着,我们可以试试用gin开启一个服务:

package main
import (
   "net/http"
   "github.com/gin-gonic/gin"
)
func main() {
   // TODO:1.配置初始化
   // TODO:2.日志
   // TODO:3.数据库连接
   // TODO:4.其他初始化
   // TODO:5.启动服务
   r := gin.Default()
   // 测试路由
   r.GET("/ping", func(c *gin.Context) {
      c.String(http.StatusOK, "pong")
   })
   // 启动服务器
   r.Run(":8080")
}

启动服务:

go run main.go

然后在浏览器中输入 http://127.0.0.1:8080/ping,就会在页面返回一个 pong
image.png

2、配置初始化 & 全局变量

2.1 viper简介

配置文件是每个项目必不可少的部分,用来保存应用基本数据、数据库配置等信息,避免要修改一个配置项需要到处找的尴尬。这里我使用 viper 作为配置管理方案。

ViperGo 语言中一个非常流行的配置管理库,它可以帮助程序员在应用程序中加载和解析各种配置格式,如 JSON、YAML、TOML、INI 等。Viper 库提供了一个简单的接口,允许开发人员通过各种方法来访问和管理配置。

下面是 Viper 库的一些主要特点:

  • 设置默认值
  • JSONTOMLYAMLHCLenvfileJava properties 格式的配置文件读取配置信息
  • 实时监控和重新读取配置文件(可选)
  • 从环境变量中读取
  • 从远程配置系统(etcd或Consul)读取并监控配置变化
  • 从命令行参数读取配置
  • 从buffer读取配置
  • 显式配置值

2.2 viper 基本使用

先安装依赖:

go get -u github.com/spf13/viper 

然后在项目根目录下的 config.yaml 文件中添加基本的配置信息:

app: # 基本配置信息
  env: local # 环境
  port: 8889 # 服务监听端口
  app_name: ewa_admin_server # 应用名称
  app_url: http://localhost # 应用域名
  db_type: mysql # 使用的数据库

在项目根目录下新建文件夹 config,用于存放所有配置对应的结构体,新建 config.go 文件,定义 Configuration 结构体,其 App 属性对应 config.yaml 中的 app

package config
type Configuration struct {
    App App `mapstructure:"app" json:"app" yaml:"app"`
}

新建 app.go 文件,定义 App 结构体,其所有属性分别对应 config.yaml 中 app 下的所有配置

package config
type App struct {
   Env     string `mapstructure:"env" json:"env" yaml:"env"`
   Port    int    `mapstructure:"port" json:"port" yaml:"port"`
   AppName string `mapstructure:"app_name" json:"app_name" yaml:"app_name"`
   AppUrl  string `mapstructure:"app_url" json:"app_url" yaml:"app_url"`
   DbType  string `mapstructure:"db_type" json:"db_type" yaml:"db_type"`
}

注意:配置结构体中 mapstructure 标签需对应 config.ymal 中的配置名称, viper 会根据标签 value 值把 config.yaml 的数据赋予给结构体

2.3 将配置放入全局变量

为什么要将配置信息放入全局变量中?

在Go语言中,将配置信息存储在全局变量中是一种常见的做法,这主要是因为全局变量的值可以在整个程序中访问和共享,因此在某些情况下可以方便地进行配置管理和使用。

下面是一些常见的场景,可能会使用全局变量来存储配置信息:

  1. 在应用程序的不同模块中需要使用相同的配置信息时,使用全局变量可以方便地实现这一点。例如,在一个Web应用程序中,可能需要在多个处理程序中使用数据库的连接信息,这时将连接信息存储在全局变量中可以方便地在各个处理程序中使用。
  2. 在需要频繁访问配置信息的场景中,使用全局变量可以避免反复读取配置文件或重复创建配置对象的开销,提高程序的性能和效率。

不过,使用全局变量也可能会带来一些潜在的问题,比如:

  1. 全局变量的值可以在整个程序中被修改,这可能会导致意外的行为和错误。
  2. 全局变量可能会使程序的依赖关系更加复杂和难以管理,因为它们可以被程序中的任何模块访问和修改。

因此,在使用全局变量存储配置信息时,应该仔细考虑其对程序的影响,并确保采取适当的措施来减少潜在的问题。例如,可以使用只读全局变量或使用锁来保护全局变量的访问。此外,也可以考虑使用依赖注入等技术来管理程序中的配置信息。

下面我们在 global 中创建一个 global.go 文件来集中存放全局变量:

package global
import (
   "ewa_admin_server/config"
   "github.com/spf13/viper"
)
var (
   EWA_CONFIG config.Configuration
   EWA_VIPER  *viper.Viper
)

考虑实际工作中多环境开发、测试的场景,我们需要针对不同的环境使用不同的配置,在core中加入一个internal文件,添加一个constants.go,写入

package internal
const (
   ConfigEnv         = "EWA_CONFIG"
   ConfigDefaultFile = "config.yaml"
   ConfigTestFile    = "config.test.yaml"
   ConfigDebugFile   = "config.debug.yaml"
   ConfigReleaseFile = "config.release.yaml"
)

然后在 core 中新建 viper.go,编写配置初始化方法:

package core
import (
   "ewa_admin_server/core/internal"
   "ewa_admin_server/global"
   "flag"
   "fmt"
   "os"
   "github.com/fsnotify/fsnotify"
   "github.com/gin-gonic/gin"
   "github.com/spf13/viper"
)
// InitializeViper 优先级: 命令行 > 环境变量 > 默认值
func InitializeViper(path ...string) *viper.Viper {
   var config string
   if len(path) == 0 {
      // 定义命令行flag参数,格式:flag.TypeVar(Type指针, flag名, 默认值, 帮助信息)
      flag.StringVar(&config, "c", "", "choose config file.")
      // 定义好命令行flag参数后,需要通过调用flag.Parse()来对命令行参数进行解析。
      flag.Parse()
      // 判断命令行参数是否为空
      if config == "" {
         /*
            判断 internal.ConfigEnv 常量存储的环境变量是否为空
            比如我们启动项目的时候,执行:GVA_CONFIG=config.yaml go run main.go
            这时候 os.Getenv(internal.ConfigEnv) 得到的就是 config.yaml
            当然,也可以通过 os.Setenv(internal.ConfigEnv, "config.yaml") 在初始化之前设置
         */
         if configEnv := os.Getenv(internal.ConfigEnv); configEnv == "" {
            switch gin.Mode() {
            case gin.DebugMode:
               config = internal.ConfigDefaultFile
               fmt.Printf("您正在使用gin模式的%s环境名称,config的路径为%s\n", gin.EnvGinMode, internal.ConfigDefaultFile)
            case gin.ReleaseMode:
               config = internal.ConfigReleaseFile
               fmt.Printf("您正在使用gin模式的%s环境名称,config的路径为%s\n", gin.EnvGinMode, internal.ConfigReleaseFile)
            case gin.TestMode:
               config = internal.ConfigTestFile
               fmt.Printf("您正在使用gin模式的%s环境名称,config的路径为%s\n", gin.EnvGinMode, internal.ConfigTestFile)
            }
         } else {
            // internal.ConfigEnv 常量存储的环境变量不为空 将值赋值于config
            config = configEnv
            fmt.Printf("您正在使用%s环境变量,config的路径为%s\n", internal.ConfigEnv, config)
         }
      } else {
         // 命令行参数不为空 将值赋值于config
         fmt.Printf("您正在使用命令行的-c参数传递的值,config的路径为%s\n", config)
      }
   } else {
      // 函数传递的可变参数的第一个值赋值于config
      config = path[0]
      fmt.Printf("您正在使用func Viper()传递的值,config的路径为%s\n", config)
   }
   vip := viper.New()
   vip.SetConfigFile(config)
   vip.SetConfigType("yaml")
   err := vip.ReadInConfig()
   if err != nil {
      panic(fmt.Errorf("Fatal error config file: %s \n", err))
   }
   vip.WatchConfig()
   vip.OnConfigChange(func(e fsnotify.Event) {
      fmt.Println("config file changed:", e.Name)
      if err = vip.Unmarshal(&global.EWA_CONFIG); err != nil {
         fmt.Println(err)
      }
   })
   if err = vip.Unmarshal(&global.EWA_CONFIG); err != nil {
      fmt.Println(err)
   }
   fmt.Println("====1-viper====: viper init config success")
   return vip
}

重新启动项目,就会在控制台打印:

====1-viper====: viper init config success
====app_name====:  ewa_admin_server

这里面涉及到几个知识点:

2.3.1 命令行 flag

Go 提供了一个 flag 包,支持基本的命令行标志解析,请看下面的示例

package main
import (
    "flag"
    "fmt"
)
func main() {
    wordPtr := flag.String("word", "foo", "a string")
    numbPtr := flag.Int("numb", 42, "an int")
    forkPtr := flag.Bool("fork", false, "a bool")
    var svar string
    flag.StringVar(&svar, "svar", "bar", "a string var")
    flag.Parse()
    fmt.Println("word:", *wordPtr)
    fmt.Println("numb:", *numbPtr)
    fmt.Println("fork:", *forkPtr)
    fmt.Println("svar:", svar)
    fmt.Println("tail:", flag.Args())
}

使用:

$ go build command-line-flags.go
$ ./command-line-flags -word=opt -numb=7 -fork -svar=flag 
word: opt 
numb: 7 
fork: true 
svar: flag 
tail: []
$ ./command-line-flags -word=opt
word: opt
numb: 42
fork: false
svar: bar
tail: []
$ ./command-line-flags -word=opt a1 a2 a3
word: opt
...
tail: [a1 a2 a3]
$ ./command-line-flags -word=opt a1 a2 a3 -numb=7
word: opt
numb: 42
fork: false
svar: bar
tail: [a1 a2 a3 -numb=7]
$ ./command-line-flags -h
Usage of ./command-line-flags:
  -fork=false: a bool
  -numb=42: an int
  -svar="bar": a string var
  -word="foo": a string
$ ./command-line-flags -wat
flag provided but not defined: -wat
Usage of ./command-line-flags:
...

2.3.2 os.Setenv() & os.Getenv()

package main
import (
    "fmt"
    "os"
    "strings"
)
func main() {
    os.Setenv("FOO", "1")
    fmt.Println("FOO:", os.Getenv("FOO"))
    fmt.Println("BAR:", os.Getenv("BAR"))
    fmt.Println()
    for _, e := range os.Environ() {
        pair := strings.SplitN(e, "=", 2)
        fmt.Println(pair[0])
    }
}

使用:

$ go run environment-variables.go 
FOO: 1
BAR:
TERM_PROGRAM
PATH
SHELL
$ BAR=2 go run environment-variables.go
FOO: 1
BAR: 2
...

2.3.3 gin.Mode()

在初始化本路由的时候使用,从源码可看出,通过给变量ginMode赋值的方式提供了三种模式:

  • DebugMode
  • ReleaseMode
  • TestMode

DebugModeReleaseMode多了一些额外的错误信息,生产环境不需要这些信息。而TestModegin用于自己的单元测试,用来快速开关DebugMode。对其它开发者没什么意义。可以通过gin.SetMode(AppMode)来设置mode。

需要注意的是:SetMode() 应该声明在 gin.New() 前,否则配置无法更新:

image.png

关于viper的使用,最好是看官方文档,也可以看看下面几篇不错的文章:

2.4 使用配置

现在我们已经将配置解析到了全局变量中,就可以将其使用到服务启动逻辑中了,在 core 中新建 server.go 文件,然后将服务启动的方法写在这里:

package core
import (
   "ewa_admin_server/global"
   "fmt"
   "net/http"
   "time"
   "github.com/fvbock/endless"
   "github.com/gin-gonic/gin"
)
type server interface {
   ListenAndServe() error
}
func RunServer() {
   r := gin.Default()
   r.GET("/ping", func(c *gin.Context) {
      c.String(http.StatusOK, "pong")
   })
   address := fmt.Sprintf(":%d", global.EWA_CONFIG.App.Port)
   s := initServer(address, r)
   // 保证文本顺序输出
   time.Sleep(10 * time.Microsecond)
   fmt.Println(`address`, address)
   s.ListenAndServe()
}
func initServer(address string, router *gin.Engine) server {
   // 使用endless库创建一个HTTP服务器,其中address是服务器的监听地址(如:8080),router是HTTP请求路由器。
   s := endless.NewServer(address, router)
   // 设置HTTP请求头的读取超时时间为20秒,如果在20秒内未读取到请求头,则会返回一个超时错误。
   s.ReadHeaderTimeout = 20 * time.Second
   // 设置HTTP响应体的写入超时时间为20秒,如果在20秒内未将响应体写入完成,则会返回一个超时错误。
   s.WriteTimeout = 20 * time.Second
   // 设置HTTP请求头的最大字节数为1MB。如果请求头超过1MB,则会返回一个错误。
   s.MaxHeaderBytes = 1 << 20
   return s
}

使用 endless 的作用是实现无缝重载和优雅关闭 HTTP 服务器。自带优雅地重启或停止你的Web服务器,我们可以使用fvbock/endless来替换默认的ListenAndServe,有关详细信息,请参阅问题#296

endless 是一个可以用于重新加载和优雅关闭HTTP服务器的库。它可以在运行时更新服务器代码而无需停止正在运行的HTTP服务器。这使得服务器能够在生产环境下无缝地进行更新和维护,同时不影响当前正在运行的请求和连接。

使用 endless,可以在代码修改后,通过发送信号量通知服务器进行重载,新的代码会被加载并运行,旧的连接会继续服务,新的连接将使用新的代码进行处理。当需要关闭服务器时,endless 会等待所有当前处理的请求完成后再关闭服务器,这样可以确保所有请求都能得到处理,避免数据丢失和用户体验下降。

Gin 中使用 endless 可以提高服务器的可靠性和稳定性,同时也能提高开发效率,减少服务器维护和更新的停机时间。

这些配置可以帮助我们优化HTTP服务器的性能和安全性。通过设置超时时间和最大字节数等参数,可以防止一些潜在的安全问题和性能问题。

例如,设置超时时间可以防止客户端故意保持连接而导致的资源浪费,设置最大字节数可以防止客户端发送过大的请求头而导致的资源浪费和安全问题。

重启项目,访问 http://127.0.0.1:8889/ping,依然能在页面看到 pong,就说明我们的初始化配置成功了。

image.png

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
1月前
|
运维 监控 安全
在Linux系统中,认证日志
Linux系统中的认证日志对于安全监控和故障排查至关重要,常见的日志文件包括:`/var/log/auth.log`(Debian、Ubuntu)、`/var/log/secure`(RPM发行版)、`/var/log/lastlog`、`/var/log/faillog`、`/var/log/wtmp`和`/var/run/utmp`。这些文件记录登录尝试、失败、当前用户等信息。日志管理可通过文本编辑器、日志查看工具或`rsyslog`、`syslog-ng`等工具进行。注意日志位置可能因发行版和配置差异而变化,应确保日志文件的安全访问,并定期轮转归档以保护敏感信息和节省空间。
26 3
|
2月前
|
API 数据库 数据安全/隐私保护
利用Django框架构建高效后端API服务
本文将介绍如何利用Django框架构建高效的后端API服务。通过深入分析Django框架的特性和优势,结合实际案例,探讨了如何利用Django提供的强大功能来构建高性能、可扩展的后端服务。同时,还对Django框架在后端开发中的一些常见问题进行了解决方案的探讨,并提出了一些建设性的建议。
68 3
|
2月前
|
Shell Linux C语言
【Shell 命令集合 网络通讯 】Linux 查看系统中的UUCP日志文件 uulog命令 使用指南
【Shell 命令集合 网络通讯 】Linux 查看系统中的UUCP日志文件 uulog命令 使用指南
33 0
|
2天前
|
jenkins 网络安全 持续交付
新的centos7.9安装docker版本的jenkins2.436.1最新版本-后端项目发布(四)
新的centos7.9安装docker版本的jenkins2.436.1最新版本-后端项目发布(四)
|
3天前
|
jenkins 持续交付
基于Jeecgboot前后端分离的平台后端系统采用jenkins发布
基于Jeecgboot前后端分离的平台后端系统采用jenkins发布
|
13天前
|
运维 监控 Go
Golang深入浅出之-Go语言中的日志记录:log与logrus库
【4月更文挑战第27天】本文比较了Go语言中标准库`log`与第三方库`logrus`的日志功能。`log`简单但不支持日志级别配置和多样化格式,而`logrus`提供更丰富的功能,如日志级别控制、自定义格式和钩子。文章指出了使用`logrus`时可能遇到的问题,如全局logger滥用、日志级别设置不当和过度依赖字段,并给出了避免错误的建议,强调理解日志级别、合理利用结构化日志、模块化日志管理和定期审查日志配置的重要性。通过这些实践,开发者能提高应用监控和故障排查能力。
88 1
|
13天前
|
API 开发者 UED
构建高效微服务架构:后端开发的新趋势移动应用与系统:开发与优化的艺术
【4月更文挑战第30天】 随着现代软件系统对可伸缩性、灵活性和敏捷性的日益需求,传统的单体应用架构正逐渐向微服务架构转变。本文将探讨微服务架构的核心概念,分析其优势,并着重讨论如何利用最新的后端技术栈实现一个高效的微服务系统。我们将涵盖设计模式、服务划分、数据一致性、服务发现与注册、API网关以及容器化等关键技术点,为后端开发者提供一份实操指南。 【4月更文挑战第30天】 在数字化时代的浪潮中,移动应用和操作系统的紧密交织已成为日常生活和商业活动的基石。本文将深入探讨移动应用开发的关键技术、跨平台开发工具的选择以及移动操作系统的架构和性能优化策略。通过分析当前移动应用开发的挑战与机遇,我们将
|
14天前
|
机器学习/深度学习 Kubernetes 微服务
后端技术发展及其在高性能系统中的应用研究
后端技术发展及其在高性能系统中的应用研究
18 0
|
15天前
|
监控 Linux 开发者
【专栏】`head`命令是Linux系统中用于快速查看文件开头内容的工具,常用于处理日志文件
【4月更文挑战第28天】`head`命令是Linux系统中用于快速查看文件开头内容的工具,常用于处理日志文件。基本用法包括指定查看行数(如`head -n 10 file.txt`)和与其他命令(如`grep`)结合使用。高级用法涉及动态查看日志、过滤内容、管道操作及在脚本中的应用。实际应用案例包括监控系统日志、排查错误和分析应用日志。使用时注意文件存在性、行数选择及权限问题。熟练掌握head命令能提升工作效率,结合其他工具可实现更多功能,助力Linux用户提升技能。
|
17天前
|
存储 缓存 NoSQL
node实战——koa给邮件发送验证码并缓存到redis服务(node后端储备知识)
node实战——koa给邮件发送验证码并缓存到redis服务(node后端储备知识)
20 0