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

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 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日志并进行多维度分析。
相关文章
|
2月前
|
JavaScript 前端开发 Java
制造业ERP源码,工厂ERP管理系统,前端框架:Vue,后端框架:SpringBoot
这是一套基于SpringBoot+Vue技术栈开发的ERP企业管理系统,采用Java语言与vscode工具。系统涵盖采购/销售、出入库、生产、品质管理等功能,整合客户与供应商数据,支持在线协同和业务全流程管控。同时提供主数据管理、权限控制、工作流审批、报表自定义及打印、在线报表开发和自定义表单功能,助力企业实现高效自动化管理,并通过UniAPP实现移动端支持,满足多场景应用需求。
239 1
|
26天前
|
JSON 中间件 Go
Go 网络编程:HTTP服务与客户端开发
Go 语言的 `net/http` 包功能强大,可快速构建高并发 HTTP 服务。本文从创建简单 HTTP 服务入手,逐步讲解请求与响应对象、URL 参数处理、自定义路由、JSON 接口、静态文件服务、中间件编写及 HTTPS 配置等内容。通过示例代码展示如何使用 `http.HandleFunc`、`http.ServeMux`、`http.Client` 等工具实现常见功能,帮助开发者掌握构建高效 Web 应用的核心技能。
160 61
|
23天前
|
开发框架 JSON 中间件
Go语言Web开发框架实践:路由、中间件、参数校验
Gin框架以其极简风格、强大路由管理、灵活中间件机制及参数绑定校验系统著称。本文详解其核心功能:1) 路由管理,支持分组与路径参数;2) 中间件机制,实现全局与局部控制;3) 参数绑定,涵盖多种来源;4) 结构体绑定与字段校验,确保数据合法性;5) 自定义校验器扩展功能;6) 统一错误处理提升用户体验。Gin以清晰模块化、流程可控及自动化校验等优势,成为开发者的优选工具。
|
24天前
|
开发框架 JSON 中间件
Go语言Web开发框架实践:使用 Gin 快速构建 Web 服务
Gin 是一个高效、轻量级的 Go 语言 Web 框架,支持中间件机制,非常适合开发 RESTful API。本文从安装到进阶技巧全面解析 Gin 的使用:快速入门示例(Hello Gin)、定义 RESTful 用户服务(增删改查接口实现),以及推荐实践如参数校验、中间件和路由分组等。通过对比标准库 `net/http`,Gin 提供更简洁灵活的开发体验。此外,还推荐了 GORM、Viper、Zap 等配合使用的工具库,助力高效开发。
|
2月前
|
存储 消息中间件 前端开发
PHP后端与uni-app前端协同的校园圈子系统:校园社交场景的跨端开发实践
校园圈子系统校园论坛小程序采用uni-app前端框架,支持多端运行,结合PHP后端(如ThinkPHP/Laravel),实现用户认证、社交关系管理、动态发布与实时聊天功能。前端通过组件化开发和uni.request与后端交互,后端提供RESTful API处理业务逻辑并存储数据于MySQL。同时引入Redis缓存热点数据,RabbitMQ处理异步任务,优化系统性能。核心功能包括JWT身份验证、好友系统、WebSocket实时聊天及活动管理,确保高效稳定的用户体验。
141 4
PHP后端与uni-app前端协同的校园圈子系统:校园社交场景的跨端开发实践
|
1月前
|
自然语言处理 JavaScript 前端开发
一夜获千星!已获 1.7k+,Art Design Pro:Vue3 + Vite + TypeScript 打造的高颜值管理系统模板,这个让后端小哥直呼救命的后台系统
Art Design Pro 是一款基于 Vue 3、Vite 和 TypeScript 的高颜值后台管理系统模板,已获 1.7k+ 星标。项目专注于用户体验与视觉设计,支持主题切换、多语言、权限管理及图表展示等功能,内置常用业务组件,便于快速搭建现代化管理界面。其技术栈先进,开发体验流畅,适配多设备,满足企业级应用需求。项目地址:[GitHub](https://github.com/Daymychen/art-design-pro)。
267 11
|
2月前
|
Java 关系型数据库 MySQL
在Linux平台上进行JDK、Tomcat、MySQL的安装并部署后端项目
现在,你可以通过访问http://Your_IP:Tomcat_Port/Your_Project访问你的项目了。如果一切顺利,你将看到那绚烂的胜利之光照耀在你的项目之上!
207 41
|
2月前
|
开发框架 Java 关系型数据库
在Linux系统中安装JDK、Tomcat、MySQL以及部署J2EE后端接口
校验时,浏览器输入:http://[your_server_IP]:8080/myapp。如果你看到你的应用的欢迎页面,恭喜你,一切都已就绪。
285 17
|
3月前
|
人工智能 小程序 NoSQL
【一步步开发AI运动小程序】二十一、如何将AI运动项目配置持久化到后端?
本文介绍基于云智「Ai运动识别引擎」的运动配置持久化方案,旨在优化小程序或Uni APP中AI运动识别能力。通过将运动检测参数(如`Key`、`Name`、`TickMode`、`rules`或`samples`)持久化到后端,可避免因频繁调整运动参数而重新发布应用,提升用户体验。持久化数据结构支持规则和姿态样本存储,适用于关系数据库、文件或文档数据库(如MongoDB)。此外,云智还提供运动自动适配工具及「AI乐运动」产品,助力快速实现AI体育、全民健身等场景。
|
4月前
|
存储 JSON Go
PHP 日志系统的最佳搭档:一个 Go 写的远程日志收集服务
为了不再 SSH 上去翻日志,我写了个 Go 小脚本,用来接收远程日志。PHP 负责记录日志,Go 负责存储和展示,按天存储、支持 API 访问、可远程管理,终于能第一时间知道项目炸了。
78 10