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

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: 【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月前
|
弹性计算 监控 负载均衡
|
3月前
|
Web App开发 JavaScript 前端开发
构建高效后端服务:Node.js与Express框架的实战指南
【9月更文挑战第6天】在数字化时代的潮流中,后端开发作为支撑现代Web和移动应用的核心,其重要性不言而喻。本文将深入浅出地介绍如何使用Node.js及其流行的框架Express来搭建一个高效、可扩展的后端服务。通过具体的代码示例和实践技巧,我们将探索如何利用这两个强大的工具提升开发效率和应用性能。无论你是后端开发的新手还是希望提高现有项目质量的老手,这篇文章都将为你提供有价值的见解和指导。
|
29天前
|
中间件 Go API
Go语言中几种流行的Web框架,如Beego、Gin和Echo,分析了它们的特点、性能及适用场景,并讨论了如何根据项目需求、性能要求、团队经验和社区支持等因素选择最合适的框架
本文概述了Go语言中几种流行的Web框架,如Beego、Gin和Echo,分析了它们的特点、性能及适用场景,并讨论了如何根据项目需求、性能要求、团队经验和社区支持等因素选择最合适的框架。
70 1
|
2月前
|
算法 Java Linux
java制作海报五:java 后端整合 echarts 画出 折线图,项目放在linux上,echarts图上不显示中文,显示方框口口口
这篇文章介绍了如何在Java后端整合ECharts库来绘制折线图,并讨论了在Linux环境下ECharts图表中文显示问题。
47 1
|
2月前
|
前端开发 Java Shell
后端项目打包上传服务器部署运行记录
后端项目打包上传服务器部署运行记录
47 0
|
4月前
|
JavaScript 安全 API
构建高效后端服务:RESTful API 设计与实现
【8月更文挑战第31天】在数字化时代,一个清晰、高效且安全的后端服务是应用程序成功的关键。本文将深入探讨如何设计并实现一个遵循REST原则的API,确保服务的可扩展性和维护性。我们将从基础概念出发,逐步引入真实代码示例,展示如何利用现代技术栈创建高性能的后端服务。无论你是初学者还是有经验的开发者,这篇文章都将为你提供新的视角和实用的技巧。
|
4月前
|
前端开发 开发者 Apache
揭秘Apache Wicket项目结构:如何打造Web应用的钢铁长城,告别混乱代码!
【8月更文挑战第31天】Apache Wicket凭借其组件化设计深受Java Web开发者青睐。本文详细解析了Wicket项目结构,帮助你构建可维护的大型Web应用。通过示例展示了如何使用Maven管理依赖,并组织页面、组件及业务逻辑,确保代码清晰易懂。Wicket提供的页面继承、组件重用等功能进一步增强了项目的可维护性和扩展性。掌握这些技巧,能够显著提升开发效率,构建更稳定的Web应用。
117 0
|
4月前
|
前端开发 程序员 API
从后端到前端的无缝切换:一名C#程序员如何借助Blazor技术实现全栈开发的梦想——深入解析Blazor框架下的Web应用构建之旅,附带实战代码示例与项目配置技巧揭露
【8月更文挑战第31天】本文通过详细步骤和代码示例,介绍了如何利用 Blazor 构建全栈 Web 应用。从创建新的 Blazor WebAssembly 项目开始,逐步演示了前后端分离的服务架构设计,包括 REST API 的设置及 Blazor 组件的数据展示。通过整合前后端逻辑,C# 开发者能够在统一环境中实现高效且一致的全栈开发。Blazor 的引入不仅简化了 Web 应用开发流程,还为习惯于后端开发的程序员提供了进入前端世界的桥梁。
481 0
|
4月前
|
XML JSON API
打造高效后端服务:RESTful API 设计实践
【8月更文挑战第31天】在数字化浪潮中,后端服务是支撑起整个互联网生态的骨架。本文将带你深入理解RESTful API的设计哲学,通过具体案例学习如何构建清晰、灵活且高效的后端服务接口。我们将一起探索资源定位、接口约束以及状态传输的关键要素,并通过代码示例揭示最佳实践。无论你是初学者还是有经验的开发者,这篇文章都将为你提供宝贵的洞见和实用的技巧。
|
1月前
|
XML 安全 Java
【日志框架整合】Slf4j、Log4j、Log4j2、Logback配置模板
本文介绍了Java日志框架的基本概念和使用方法,重点讨论了SLF4J、Log4j、Logback和Log4j2之间的关系及其性能对比。SLF4J作为一个日志抽象层,允许开发者使用统一的日志接口,而Log4j、Logback和Log4j2则是具体的日志实现框架。Log4j2在性能上优于Logback,推荐在新项目中使用。文章还详细说明了如何在Spring Boot项目中配置Log4j2和Logback,以及如何使用Lombok简化日志记录。最后,提供了一些日志配置的最佳实践,包括滚动日志、统一日志格式和提高日志性能的方法。
282 30
【日志框架整合】Slf4j、Log4j、Log4j2、Logback配置模板
下一篇
DataWorks