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
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
。
2、配置初始化 & 全局变量
2.1 viper简介
配置文件是每个项目必不可少的部分,用来保存应用基本数据、数据库配置等信息,避免要修改一个配置项需要到处找的尴尬。这里我使用 viper 作为配置管理方案。
Viper
是 Go
语言中一个非常流行的配置管理库,它可以帮助程序员在应用程序中加载和解析各种配置格式,如 JSON、YAML、TOML、INI
等。Viper
库提供了一个简单的接口,允许开发人员通过各种方法来访问和管理配置。
下面是 Viper
库的一些主要特点:
- 设置默认值
- 从
JSON
、TOML
、YAML
、HCL
、envfile
和Java 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语言中,将配置信息存储在全局变量中是一种常见的做法,这主要是因为全局变量的值可以在整个程序中访问和共享,因此在某些情况下可以方便地进行配置管理和使用。
下面是一些常见的场景,可能会使用全局变量来存储配置信息:
- 在应用程序的不同模块中需要使用相同的配置信息时,使用全局变量可以方便地实现这一点。例如,在一个Web应用程序中,可能需要在多个处理程序中使用数据库的连接信息,这时将连接信息存储在全局变量中可以方便地在各个处理程序中使用。
- 在需要频繁访问配置信息的场景中,使用全局变量可以避免反复读取配置文件或重复创建配置对象的开销,提高程序的性能和效率。
不过,使用全局变量也可能会带来一些潜在的问题,比如:
- 全局变量的值可以在整个程序中被修改,这可能会导致意外的行为和错误。
- 全局变量可能会使程序的依赖关系更加复杂和难以管理,因为它们可以被程序中的任何模块访问和修改。
因此,在使用全局变量存储配置信息时,应该仔细考虑其对程序的影响,并确保采取适当的措施来减少潜在的问题。例如,可以使用只读全局变量或使用锁来保护全局变量的访问。此外,也可以考虑使用依赖注入等技术来管理程序中的配置信息。
下面我们在 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
DebugMode
比ReleaseMode
多了一些额外的错误信息,生产环境不需要这些信息。而TestMode
是gin
用于自己的单元测试,用来快速开关DebugMode
。对其它开发者没什么意义。可以通过gin.SetMode(AppMode)
来设置mode。
需要注意的是:
SetMode()
应该声明在gin.New()
前,否则配置无法更新:
关于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
,就说明我们的初始化配置成功了。