0


Go 语言之搭建通用 Web 项目开发脚手架

Go 语言之搭建通用 Web 项目开发脚手架

MVC 模式

MVC 模式代表 Model-View-Controller(模型-视图-控制器) 模式。这种模式用于应用程序的分层开发。

  • Model(模型) - 模型代表一个存取数据的对象或 JAVA POJO。它也可以带有逻辑,在数据变化时更新控制器。
  • View(视图) - 视图代表模型包含的数据的可视化。
  • Controller(控制器) - 控制器作用于模型和视图上。它控制数据流向模型对象,并在数据变化时更新视图。它使视图与模型分离开。

Web 项目 CLD 分层

协议处理层:支持各种协议

Controller:服务的入口,负责处理路由、参数校验、请求转发。

Logic/Service:逻辑(服务)层,负责处理业务逻辑。

DAO/Repository:负责数据与存储相关功能。

搭建 Web 项目开发脚手架

创建 web_app 项目

创建 main 文件

项目目录

  1. Code/go/web_app via 🐹 v1.20.3 via 🅒 base took 8h 53m 28.4s
  2. tree
  3. .
  4. ├── config.yaml
  5. ├── controllers
  6. ├── dao
  7. ├── mysql
  8. └── mysql.go
  9. └── redis
  10. └── redis.go
  11. ├── go.mod
  12. ├── go.sum
  13. ├── logger
  14. └── logger.go
  15. ├── logic
  16. ├── main.go
  17. ├── models
  18. ├── pkg
  19. ├── routes
  20. └── routes.go
  21. ├── settings
  22. └── settings.go
  23. ├── web_app
  24. └── web_app.log
  25. 11 directories, 11 files
  26. Code/go/web_app via 🐹 v1.20.3 via 🅒 base
config.yaml
  1. app:
  2. name: "web_app"
  3. mode: "dev"
  4. port: 8080
  5. log:
  6. level: "debug"
  7. filename: "web_app.log"
  8. max_size: 30
  9. max_age: 30
  10. max_backups: 7
  11. mysql:
  12. host: "127.0.0.1"
  13. port: 3306
  14. user: "root"
  15. password: "12345678"
  16. dbname: "sql_test"
  17. max_open_conns: 200
  18. max_idle_conns: 50
  19. redis:
  20. host: "127.0.0.1"
  21. port: 6379
  22. password: ""
  23. db: 0
  24. pool_size: 100
settings/settings.go
  1. package settings
  2. import (
  3. "fmt"
  4. "github.com/fsnotify/fsnotify"
  5. "github.com/spf13/viper"
  6. )
  7. func Init() (err error) {
  8. // 设置默认值
  9. viper.SetDefault("fileDir", "./")
  10. // 读取配置文件
  11. viper.SetConfigFile("./config.yaml") // 指定配置文件路径
  12. viper.SetConfigName("config") // 配置文件名称(无扩展名)
  13. viper.SetConfigType("yaml") // SetConfigType设置远端源返回的配置类型,例如:“json”。
  14. viper.AddConfigPath(".") // 还可以在工作目录中查找配置
  15. err = viper.ReadInConfig() // 查找并读取配置文件
  16. if err != nil { // 处理读取配置文件的错误
  17. fmt.Printf("viper.ReadInConfig failed, error: %v\n", err)
  18. return
  19. }
  20. // 实时监控配置文件的变化 WatchConfig 开始监视配置文件的更改。
  21. viper.WatchConfig()
  22. // OnConfigChange设置配置文件更改时调用的事件处理程序。
  23. // 当配置文件变化之后调用的一个回调函数
  24. viper.OnConfigChange(func(e fsnotify.Event) {
  25. fmt.Println("Config file changed:", e.Name)
  26. })
  27. return
  28. }
logger/logger.go
  1. package settings
  2. import (
  3. "fmt"
  4. "github.com/fsnotify/fsnotify"
  5. "github.com/spf13/viper"
  6. )
  7. func Init() (err error) {
  8. // 设置默认值
  9. viper.SetDefault("fileDir", "./")
  10. // 读取配置文件
  11. viper.SetConfigFile("./config.yaml") // 指定配置文件路径
  12. viper.SetConfigName("config") // 配置文件名称(无扩展名)
  13. viper.SetConfigType("yaml") // 如果配置文件的名称中没有扩展名,则需要配置此项
  14. viper.AddConfigPath(".") // 还可以在工作目录中查找配置
  15. err = viper.ReadInConfig() // 查找并读取配置文件
  16. if err != nil { // 处理读取配置文件的错误
  17. fmt.Printf("viper.ReadInConfig failed, error: %v\n", err)
  18. return
  19. }
  20. // 实时监控配置文件的变化 WatchConfig 开始监视配置文件的更改。
  21. viper.WatchConfig()
  22. // OnConfigChange设置配置文件更改时调用的事件处理程序。
  23. // 当配置文件变化之后调用的一个回调函数
  24. viper.OnConfigChange(func(e fsnotify.Event) {
  25. fmt.Println("Config file changed:", e.Name)
  26. })
  27. return
  28. }
dao/mysql/mysql.go
  1. package mysql
  2. import (
  3. "fmt"
  4. "go.uber.org/zap"
  5. "github.com/jmoiron/sqlx"
  6. "github.com/spf13/viper"
  7. _ "github.com/go-sql-driver/mysql" // 匿名导入 自动执行 init()
  8. )
  9. var db *sqlx.DB
  10. func Init() (err error) {
  11. //DSN (Data Source Name) Sprintf根据格式说明符进行格式化,并返回结果字符串。
  12. dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=true",
  13. viper.GetString("mysql.user"),
  14. viper.GetString("mysql.password"),
  15. viper.GetString("mysql.host"),
  16. viper.GetInt("mysql.port"),
  17. viper.GetString("mysql.dbname"),
  18. )
  19. // 连接到数据库并使用ping进行验证。
  20. // 也可以使用 MustConnect MustConnect连接到数据库,并在出现错误时恐慌 panic。
  21. db, err = sqlx.Connect("mysql", dsn)
  22. if err != nil {
  23. zap.L().Error("connect DB failed", zap.Error(err))
  24. return
  25. }
  26. db.SetMaxOpenConns(viper.GetInt("mysql.max_open_conns")) // 设置数据库的最大打开连接数。
  27. db.SetMaxIdleConns(viper.GetInt("mysql.max_idle_conns")) // 设置空闲连接池中的最大连接数。
  28. return
  29. }
  30. func Close() {
  31. _ = db.Close()
  32. }
dao/redis/redis.go
  1. package redis
  2. import (
  3. "context"
  4. "fmt"
  5. "github.com/redis/go-redis/v9"
  6. "github.com/spf13/viper"
  7. )
  8. // 声明一个全局的 rdb 变量
  9. var rdb *redis.Client
  10. // 初始化连接
  11. func Init() (err error) {
  12. // NewClient将客户端返回给Options指定的Redis Server。
  13. // Options保留设置以建立redis连接。
  14. rdb = redis.NewClient(&redis.Options{
  15. Addr: fmt.Sprintf("%s:%d", viper.GetString("redis.host"), viper.GetInt("redis.port")),
  16. Password: viper.GetString("redis.password"), // 没有密码,默认值
  17. DB: viper.GetInt("redis.db"), // 默认DB 0 连接到服务器后要选择的数据库。
  18. PoolSize: viper.GetInt("redis.pool_size"), // 最大套接字连接数。 默认情况下,每个可用CPU有10个连接,由runtime.GOMAXPROCS报告。
  19. })
  20. // Background返回一个非空的Context。它永远不会被取消,没有值,也没有截止日期。
  21. // 它通常由main函数、初始化和测试使用,并作为传入请求的顶级上下文
  22. ctx := context.Background()
  23. _, err = rdb.Ping(ctx).Result()
  24. return
  25. }
  26. func Close() {
  27. _ = rdb.Close()
  28. }
routes/routes.go
  1. package routes
  2. import (
  3. "net/http"
  4. "web_app/logger"
  5. "github.com/gin-gonic/gin"
  6. )
  7. func Setup() *gin.Engine {
  8. r := gin.New()
  9. r.Use(logger.GinLogger(), logger.GinRecovery(true))
  10. r.GET("/", func(context *gin.Context) {
  11. context.String(http.StatusOK, "OK")
  12. })
  13. return r
  14. }
main.go
  1. package main
  2. import (
  3. "context"
  4. "fmt"
  5. "log"
  6. "net/http"
  7. "os"
  8. "os/signal"
  9. "syscall"
  10. "time"
  11. "web_app/dao/mysql"
  12. "web_app/dao/redis"
  13. "web_app/logger"
  14. "web_app/routes"
  15. "web_app/settings"
  16. "github.com/spf13/viper"
  17. "go.uber.org/zap"
  18. )
  19. // Go Web 开发通用的脚手架模版
  20. func main() {
  21. // 1. 加载配置
  22. if err := settings.Init(); err != nil {
  23. fmt.Printf("init settings failed, error: %v\n", err)
  24. return
  25. }
  26. // 2. 初始化日志
  27. if err := logger.Init(); err != nil {
  28. fmt.Printf("init logger failed, error: %v\n", err)
  29. return
  30. }
  31. defer zap.L().Sync()
  32. zap.L().Debug("logger initialized successfully")
  33. // 3. 初始化 MySQL 连接
  34. if err := mysql.Init(); err != nil {
  35. fmt.Printf("init mysql failed, error: %v\n", err)
  36. return
  37. }
  38. defer mysql.Close()
  39. // 4. 初始化 Redis 连接
  40. if err := redis.Init(); err != nil {
  41. fmt.Printf("init redis failed, error: %v\n", err)
  42. return
  43. }
  44. defer redis.Close()
  45. // 5. 注册路由
  46. router := routes.Setup()
  47. // 6. 启动服务(优雅关机)
  48. // 服务器定义运行HTTP服务器的参数。Server的零值是一个有效的配置。
  49. srv := &http.Server{
  50. // Addr可选地以“host:port”的形式指定服务器要监听的TCP地址。如果为空,则使用“:http”(端口80)。
  51. // 服务名称在RFC 6335中定义,并由IANA分配
  52. Addr: fmt.Sprintf(":%d", viper.GetInt("app.port")),
  53. Handler: router,
  54. }
  55. go func() {
  56. // 开启一个goroutine启动服务,如果不用 goroutine,下面的代码 ListenAndServe 会一直接收请求,处理请求,进入无限循环。代码就不会往下执行。
  57. // ListenAndServe监听TCP网络地址srv.Addr,然后调用Serve来处理传入连接上的请求。接受的连接配置为使TCP能保持连接。
  58. // ListenAndServe always returns a non-nil error. After Shutdown or Close,
  59. // the returned error is ErrServerClosed.
  60. if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
  61. log.Fatalf("listen: %s\n", err) // Fatalf 相当于Printf()之后再调用os.Exit(1)。
  62. }
  63. }()
  64. // 等待中断信号来优雅地关闭服务器,为关闭服务器操作设置一个5秒的超时
  65. // make内置函数分配并初始化(仅)slice、map或chan类型的对象。
  66. // 与new一样,第一个参数是类型,而不是值。
  67. // 与new不同,make的返回类型与其参数的类型相同,而不是指向它的指针
  68. // Channel:通道的缓冲区用指定的缓冲区容量初始化。如果为零,或者忽略大小,则通道未被缓冲。
  69. // 信号 Signal 表示操作系统信号。通常的底层实现依赖于操作系统:在Unix上是syscall.Signal。
  70. quit := make(chan os.Signal, 1) // 创建一个接收信号的通道
  71. // kill 默认会发送 syscall.SIGTERM 信号
  72. // kill -2 发送 syscall.SIGINT 信号,Ctrl+C 就是触发系统SIGINT信号
  73. // kill -9 发送 syscall.SIGKILL 信号,但是不能被捕获,所以不需要添加它
  74. // signal.Notify把收到的 syscall.SIGINT或syscall.SIGTERM 信号转发给quit
  75. // Notify使包信号将传入的信号转发给c,如果没有提供信号,则将所有传入的信号转发给c,否则仅将提供的信号转发给c。
  76. // 包信号不会阻塞发送到c:调用者必须确保c有足够的缓冲空间来跟上预期的信号速率。对于仅用于通知一个信号值的通道,大小为1的缓冲区就足够了。
  77. // 允许使用同一通道多次调用Notify:每次调用都扩展发送到该通道的信号集。从集合中移除信号的唯一方法是调用Stop。
  78. // 允许使用不同的通道和相同的信号多次调用Notify:每个通道独立地接收传入信号的副本。
  79. signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) // 此处不会阻塞
  80. <-quit // 阻塞在此,当接收到上述两种信号时才会往下执行
  81. zap.L().Info("Shutdown Server ...")
  82. // 创建一个5秒超时的context
  83. ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  84. defer cancel()
  85. // 5秒内优雅关闭服务(将未处理完的请求处理完再关闭服务),超过5秒就超时退出
  86. // 关机将在不中断任何活动连接的情况下优雅地关闭服务器。
  87. // Shutdown的工作原理是首先关闭所有打开的侦听器,然后关闭所有空闲连接,然后无限期地等待连接返回空闲状态,然后关闭。
  88. // 如果提供的上下文在关闭完成之前过期,则shutdown返回上下文的错误,否则返回关闭服务器的底层侦听器所返回的任何错误。
  89. // 当Shutdown被调用时,Serve, ListenAndServe和ListenAndServeTLS会立即返回ErrServerClosed。确保程序没有退出,而是等待Shutdown返回。
  90. // 关闭不试图关闭或等待被劫持的连接,如WebSockets。如果需要的话,Shutdown的调用者应该单独通知这些长寿命连接关闭,并等待它们关闭。
  91. // 一旦在服务器上调用Shutdown,它可能不会被重用;以后对Serve等方法的调用将返回ErrServerClosed。
  92. if err := srv.Shutdown(ctx); err != nil {
  93. zap.L().Fatal("Server Shutdown", zap.Error(err))
  94. }
  95. zap.L().Info("Server exiting")
  96. }

运行并访问:http://127.0.0.1:8080

  1. Code/go/web_app via 🐹 v1.20.3 via 🅒 base
  2. go build
  3. Code/go/web_app via 🐹 v1.20.3 via 🅒 base
  4. ./web_app
  5. [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
  6. - using env: export GIN_MODE=release
  7. - using code: gin.SetMode(gin.ReleaseMode)
  8. [GIN-debug] GET / --> web_app/routes.Setup.func1 (3 handlers)

使用结构体变量保存配置信息

项目 web_app2 目录

  1. ode/go/web_app2 via 🐹 v1.20.3 via 🅒 base
  2. tree
  3. .
  4. ├── config.yaml
  5. ├── controllers
  6. ├── dao
  7. ├── mysql
  8. └── mysql.go
  9. └── redis
  10. └── redis.go
  11. ├── go.mod
  12. ├── go.sum
  13. ├── logger
  14. └── logger.go
  15. ├── logic
  16. ├── main.go
  17. ├── models
  18. ├── pkg
  19. ├── routes
  20. └── routes.go
  21. ├── settings
  22. └── settings.go
  23. ├── web_app.log
  24. └── web_app2
  25. 11 directories, 11 files
  26. Code/go/web_app2 via 🐹 v1.20.3 via 🅒 base

config.yaml

  1. name: "web_app"
  2. mode: "dev"
  3. port: 8080
  4. version: "v0.0.2"
  5. log:
  6. level: "debug"
  7. filename: "web_app.log"
  8. max_size: 30
  9. max_age: 30
  10. max_backups: 7
  11. mysql:
  12. host: "127.0.0.1"
  13. port: 3306
  14. user: "root"
  15. password: "12345678"
  16. dbname: "sql_test"
  17. max_open_conns: 200
  18. max_idle_conns: 50
  19. redis:
  20. host: "127.0.0.1"
  21. port: 6379
  22. password: ""
  23. db: 0
  24. pool_size: 100

settings/settings.go

  1. package settings
  2. import (
  3. "fmt"
  4. "github.com/fsnotify/fsnotify"
  5. "github.com/spf13/viper"
  6. )
  7. // Conf 全局变量,用来保存程序的所有配置信息
  8. var Conf = new(AppConfig)
  9. type AppConfig struct {
  10. Name string `mapstructure:"name"`
  11. Mode string `mapstructure:"mode"`
  12. Version string `mapstructure:"version"`
  13. Port int `mapstructure:"port"`
  14. *LogConfig `mapstructure:"log"`
  15. *MySQLConfig `mapstructure:"mysql"`
  16. *RedisConfig `mapstructure:"redis"`
  17. }
  18. type LogConfig struct {
  19. Level string `mapstructure:"level"`
  20. Filename string `mapstructure:"filename"`
  21. MaxSize int `mapstructure:"max_size"`
  22. MaxAge int `mapstructure:"max_age"`
  23. MaxBackups int `mapstructure:"max_backups"`
  24. }
  25. type MySQLConfig struct {
  26. Host string `mapstructure:"host"`
  27. User string `mapstructure:"user"`
  28. Password string `mapstructure:"password"`
  29. DbName string `mapstructure:"db_name"`
  30. Port int `mapstructure:"port"`
  31. MaxOpenConns int `mapstructure:"max_open_conns"`
  32. MaxIdleConns int `mapstructure:"max_idle_conns"`
  33. }
  34. type RedisConfig struct {
  35. Host string `mapstructure:"host"`
  36. Password string `mapstructure:"password"`
  37. Port int `matstructure:"port"`
  38. DB int `mapstructure:"db"`
  39. PoolSize int `mapstructure:"pool_size"`
  40. }
  41. func Init() (err error) {
  42. // 方式1:直接指定配置文件路径(相对路径或者绝对路径)
  43. // 相对路径:相对执行的可执行文件的相对路径
  44. // viper.SetConfigFile("./conf/config.yaml")
  45. // 绝对路径:系统中实际的文件路径
  46. // viper.SetConfigFile("/Users/qiaopengjun/Desktop/web_app2 /conf/config.yaml")
  47. // 方式2:指定配置文件名和配置文件的位置,viper 自行查找可用的配置文件
  48. // 配置文件名不需要带后缀
  49. // 配置文件位置可配置多个
  50. // 注意:viper 是根据文件名查找,配置目录里不要有同名的配置文件。
  51. // 例如:在配置目录 ./conf 中不要同时存在 config.yaml、config.json
  52. // 读取配置文件
  53. viper.SetConfigFile("./config.yaml") // 指定配置文件路径
  54. viper.SetConfigName("config") // 配置文件名称(无扩展名)
  55. viper.AddConfigPath(".") // 指定查找配置文件的路径(这里使用相对路径)可以配置多个
  56. viper.AddConfigPath("./conf") // 指定查找配置文件的路径(这里使用相对路径)可以配置多个
  57. // SetConfigType设置远端源返回的配置类型,例如:“json”。
  58. // 基本上是配合远程配置中心使用的,告诉viper 当前的数据使用什么格式去解析
  59. viper.SetConfigType("yaml")
  60. err = viper.ReadInConfig() // 查找并读取配置文件
  61. if err != nil { // 处理读取配置文件的错误
  62. fmt.Printf("viper.ReadInConfig failed, error: %v\n", err)
  63. return
  64. }
  65. // 把读取到的配置信息反序列化到 Conf 变量中
  66. if err = viper.Unmarshal(Conf); err != nil {
  67. fmt.Printf("viper unmarshal failed, error: %v\n", err)
  68. return
  69. }
  70. // 实时监控配置文件的变化 WatchConfig 开始监视配置文件的更改。
  71. viper.WatchConfig()
  72. // OnConfigChange设置配置文件更改时调用的事件处理程序。
  73. // 当配置文件变化之后调用的一个回调函数
  74. viper.OnConfigChange(func(e fsnotify.Event) {
  75. fmt.Println("Config file changed:", e.Name)
  76. if err = viper.Unmarshal(Conf); err != nil {
  77. fmt.Printf("viper unmarshal OnConfigChange failed, error: %v\n", err)
  78. }
  79. })
  80. return
  81. }

logger/logger.go

  1. package logger
  2. import (
  3. "gopkg.in/natefinch/lumberjack.v2"
  4. "net"
  5. "net/http"
  6. "net/http/httputil"
  7. "os"
  8. "runtime/debug"
  9. "strings"
  10. "time"
  11. "web_app2/settings"
  12. "github.com/gin-gonic/gin"
  13. "go.uber.org/zap"
  14. "go.uber.org/zap/zapcore"
  15. )
  16. func Init(cfg *settings.LogConfig) (err error) {
  17. writeSyncer := getLogWriter(
  18. cfg.Filename,
  19. cfg.MaxSize,
  20. cfg.MaxBackups,
  21. cfg.MaxAge,
  22. )
  23. encoder := getEncoder()
  24. var l = new(zapcore.Level)
  25. err = l.UnmarshalText([]byte(cfg.Level))
  26. if err != nil {
  27. return
  28. }
  29. // NewCore创建一个向WriteSyncer写入日志的Core。
  30. // A WriteSyncer is an io.Writer that can also flush any buffered data. Note
  31. // that *os.File (and thus, os.Stderr and os.Stdout) implement WriteSyncer.
  32. // LevelEnabler决定在记录消息时是否启用给定的日志级别。
  33. // Each concrete Level value implements a static LevelEnabler which returns
  34. // true for itself and all higher logging levels. For example WarnLevel.Enabled()
  35. // will return true for WarnLevel, ErrorLevel, DPanicLevel, PanicLevel, and
  36. // FatalLevel, but return false for InfoLevel and DebugLevel.
  37. core := zapcore.NewCore(encoder, writeSyncer, l)
  38. // New constructs a new Logger from the provided zapcore.Core and Options. If
  39. // the passed zapcore.Core is nil, it falls back to using a no-op
  40. // implementation.
  41. // AddCaller configures the Logger to annotate each message with the filename,
  42. // line number, and function name of zap's caller. See also WithCaller.
  43. logger := zap.New(core, zap.AddCaller())
  44. // 替换 zap 库中全局的logger
  45. zap.ReplaceGlobals(logger)
  46. return
  47. // Sugar封装了Logger,以提供更符合人体工程学的API,但速度略慢。糖化一个Logger的成本非常低,
  48. // 因此一个应用程序同时使用Loggers和SugaredLoggers是合理的,在性能敏感代码的边界上在它们之间进行转换。
  49. //sugarLogger = logger.Sugar()
  50. }
  51. func getEncoder() zapcore.Encoder {
  52. // NewJSONEncoder创建了一个快速、低分配的JSON编码器。编码器适当地转义所有字段键和值。
  53. // NewProductionEncoderConfig returns an opinionated EncoderConfig for
  54. // production environments.
  55. encoderConfig := zap.NewProductionEncoderConfig()
  56. encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
  57. encoderConfig.TimeKey = "time"
  58. encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
  59. encoderConfig.EncodeDuration = zapcore.SecondsDurationEncoder
  60. encoderConfig.EncodeCaller = zapcore.ShortCallerEncoder
  61. return zapcore.NewJSONEncoder(encoderConfig)
  62. }
  63. func getLogWriter(filename string, maxSize, maxBackup, maxAge int) zapcore.WriteSyncer {
  64. // Logger is an io.WriteCloser that writes to the specified filename.
  65. // 日志记录器在第一次写入时打开或创建日志文件。如果文件存在并且小于MaxSize兆字节,则lumberjack将打开并追加该文件。
  66. // 如果该文件存在并且其大小为>= MaxSize兆字节,
  67. // 则通过将当前时间放在文件扩展名(或者如果没有扩展名则放在文件名的末尾)的名称中的时间戳中来重命名该文件。
  68. // 然后使用原始文件名创建一个新的日志文件。
  69. // 每当写操作导致当前日志文件超过MaxSize兆字节时,将关闭当前文件,重新命名,并使用原始名称创建新的日志文件。
  70. // 因此,您给Logger的文件名始终是“当前”日志文件。
  71. // 如果MaxBackups和MaxAge均为0,则不会删除旧的日志文件。
  72. lumberJackLogger := &lumberjack.Logger{
  73. // Filename是要写入日志的文件。备份日志文件将保留在同一目录下
  74. Filename: filename,
  75. // MaxSize是日志文件旋转之前的最大大小(以兆字节为单位)。默认为100兆字节。
  76. MaxSize: maxSize, // M
  77. // MaxBackups是要保留的旧日志文件的最大数量。默认是保留所有旧的日志文件(尽管MaxAge仍然可能导致它们被删除)。
  78. MaxBackups: maxBackup, // 备份数量
  79. // MaxAge是根据文件名中编码的时间戳保留旧日志文件的最大天数。
  80. // 请注意,一天被定义为24小时,由于夏令时、闰秒等原因,可能与日历日不完全对应。默认情况下,不根据时间删除旧的日志文件。
  81. MaxAge: maxAge, // 备份天数
  82. // Compress决定是否应该使用gzip压缩旋转的日志文件。默认情况下不执行压缩。
  83. Compress: false, // 是否压缩
  84. }
  85. return zapcore.AddSync(lumberJackLogger)
  86. }
  87. // GinLogger
  88. func GinLogger() gin.HandlerFunc {
  89. return func(c *gin.Context) {
  90. start := time.Now()
  91. path := c.Request.URL.Path
  92. query := c.Request.URL.RawQuery
  93. c.Next() // 执行后续中间件
  94. // Since returns the time elapsed since t.
  95. // It is shorthand for time.Now().Sub(t).
  96. cost := time.Since(start)
  97. zap.L().Info(path,
  98. zap.Int("status", c.Writer.Status()),
  99. zap.String("method", c.Request.Method),
  100. zap.String("path", path),
  101. zap.String("query", query),
  102. zap.String("ip", c.ClientIP()),
  103. zap.String("user-agent", c.Request.UserAgent()),
  104. zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()),
  105. zap.Duration("cost", cost), // 运行时间
  106. )
  107. }
  108. }
  109. // GinRecovery
  110. func GinRecovery(stack bool) gin.HandlerFunc {
  111. return func(c *gin.Context) {
  112. defer func() {
  113. if err := recover(); err != nil {
  114. // Check for a broken connection, as it is not really a
  115. // condition that warrants a panic stack trace.
  116. var brokenPipe bool
  117. if ne, ok := err.(*net.OpError); ok {
  118. if se, ok := ne.Err.(*os.SyscallError); ok {
  119. if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
  120. brokenPipe = true
  121. }
  122. }
  123. }
  124. httpRequest, _ := httputil.DumpRequest(c.Request, false)
  125. if brokenPipe {
  126. zap.L().Error(c.Request.URL.Path,
  127. zap.Any("error", err),
  128. zap.String("request", string(httpRequest)),
  129. )
  130. // If the connection is dead, we can't write a status to it.
  131. c.Error(err.(error)) // nolint: errcheck
  132. c.Abort()
  133. return
  134. }
  135. if stack {
  136. zap.L().Error("[Recovery from panic]",
  137. zap.Any("error", err),
  138. zap.String("request", string(httpRequest)),
  139. zap.String("stack", string(debug.Stack())),
  140. )
  141. } else {
  142. zap.L().Error("[Recovery from panic]",
  143. zap.Any("error", err),
  144. zap.String("request", string(httpRequest)),
  145. )
  146. }
  147. c.AbortWithStatus(http.StatusInternalServerError)
  148. }
  149. }()
  150. c.Next()
  151. }
  152. }

dao/mysql/mysql.go

  1. package mysql
  2. import (
  3. "fmt"
  4. "web_app2/settings"
  5. "go.uber.org/zap"
  6. _ "github.com/go-sql-driver/mysql" // 匿名导入 自动执行 init()
  7. "github.com/jmoiron/sqlx"
  8. )
  9. var db *sqlx.DB
  10. func Init(cfg *settings.MySQLConfig) (err error) {
  11. //DSN (Data Source Name) Sprintf根据格式说明符进行格式化,并返回结果字符串。
  12. dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=true",
  13. cfg.User,
  14. cfg.Password,
  15. cfg.Host,
  16. cfg.Port,
  17. cfg.DbName,
  18. )
  19. // 连接到数据库并使用ping进行验证。
  20. // 也可以使用 MustConnect MustConnect连接到数据库,并在出现错误时恐慌 panic。
  21. db, err = sqlx.Connect("mysql", dsn)
  22. if err != nil {
  23. zap.L().Error("connect DB failed", zap.Error(err))
  24. return
  25. }
  26. db.SetMaxOpenConns(cfg.MaxOpenConns) // 设置数据库的最大打开连接数。
  27. db.SetMaxIdleConns(cfg.MaxIdleConns) // 设置空闲连接池中的最大连接数。
  28. return
  29. }
  30. func Close() {
  31. _ = db.Close()
  32. }

dao/redis/redis.go

  1. package redis
  2. import (
  3. "context"
  4. "fmt"
  5. "github.com/redis/go-redis/v9"
  6. "web_app2/settings"
  7. )
  8. // 声明一个全局的 rdb 变量
  9. var rdb *redis.Client
  10. // 初始化连接
  11. func Init(cfg *settings.RedisConfig) (err error) {
  12. // NewClient将客户端返回给Options指定的Redis Server。
  13. // Options保留设置以建立redis连接。
  14. rdb = redis.NewClient(&redis.Options{
  15. Addr: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port),
  16. Password: cfg.Password, // 没有密码,默认值
  17. DB: cfg.DB, // 默认DB 0 连接到服务器后要选择的数据库。
  18. PoolSize: cfg.PoolSize, // 最大套接字连接数。 默认情况下,每个可用CPU有10个连接,由runtime.GOMAXPROCS报告。
  19. })
  20. // Background返回一个非空的Context。它永远不会被取消,没有值,也没有截止日期。
  21. // 它通常由main函数、初始化和测试使用,并作为传入请求的顶级上下文
  22. ctx := context.Background()
  23. _, err = rdb.Ping(ctx).Result()
  24. return
  25. }
  26. func Close() {
  27. _ = rdb.Close()
  28. }

routes/routes.go

  1. package routes
  2. import (
  3. "github.com/gin-gonic/gin"
  4. "net/http"
  5. "web_app2/logger"
  6. "web_app2/settings"
  7. )
  8. func Setup() *gin.Engine {
  9. r := gin.New()
  10. r.Use(logger.GinLogger(), logger.GinRecovery(true))
  11. r.GET("/version", func(context *gin.Context) {
  12. context.String(http.StatusOK, settings.Conf.Version)
  13. })
  14. return r
  15. }

main.go

  1. package main
  2. import (
  3. "context"
  4. "fmt"
  5. "log"
  6. "net/http"
  7. "os"
  8. "os/signal"
  9. "syscall"
  10. "time"
  11. "web_app2/dao/mysql"
  12. "web_app2/dao/redis"
  13. "web_app2/logger"
  14. "web_app2/routes"
  15. "web_app2/settings"
  16. "go.uber.org/zap"
  17. )
  18. // Go Web 开发通用的脚手架模版
  19. func main() {
  20. // 1. 加载配置
  21. if err := settings.Init(); err != nil {
  22. fmt.Printf("init settings failed, error: %v\n", err)
  23. return
  24. }
  25. // 2. 初始化日志
  26. if err := logger.Init(settings.Conf.LogConfig); err != nil {
  27. fmt.Printf("init logger failed, error: %v\n", err)
  28. return
  29. }
  30. defer zap.L().Sync()
  31. zap.L().Debug("logger initialized successfully")
  32. // 3. 初始化 MySQL 连接
  33. if err := mysql.Init(settings.Conf.MySQLConfig); err != nil {
  34. fmt.Printf("init mysql failed, error: %v\n", err)
  35. return
  36. }
  37. defer mysql.Close()
  38. // 4. 初始化 Redis 连接
  39. if err := redis.Init(settings.Conf.RedisConfig); err != nil {
  40. fmt.Printf("init redis failed, error: %v\n", err)
  41. return
  42. }
  43. defer redis.Close()
  44. // 5. 注册路由
  45. router := routes.Setup()
  46. // 6. 启动服务(优雅关机)
  47. // 服务器定义运行HTTP服务器的参数。Server的零值是一个有效的配置。
  48. srv := &http.Server{
  49. // Addr可选地以“host:port”的形式指定服务器要监听的TCP地址。如果为空,则使用“:http”(端口80)。
  50. // 服务名称在RFC 6335中定义,并由IANA分配
  51. Addr: fmt.Sprintf(":%d", settings.Conf.Port),
  52. Handler: router,
  53. }
  54. go func() {
  55. // 开启一个goroutine启动服务,如果不用 goroutine,下面的代码 ListenAndServe 会一直接收请求,处理请求,进入无限循环。代码就不会往下执行。
  56. // ListenAndServe监听TCP网络地址srv.Addr,然后调用Serve来处理传入连接上的请求。接受的连接配置为使TCP能保持连接。
  57. // ListenAndServe always returns a non-nil error. After Shutdown or Close,
  58. // the returned error is ErrServerClosed.
  59. if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
  60. log.Fatalf("listen: %s\n", err) // Fatalf 相当于Printf()之后再调用os.Exit(1)。
  61. }
  62. }()
  63. // 等待中断信号来优雅地关闭服务器,为关闭服务器操作设置一个5秒的超时
  64. // make内置函数分配并初始化(仅)slice、map或chan类型的对象。
  65. // 与new一样,第一个参数是类型,而不是值。
  66. // 与new不同,make的返回类型与其参数的类型相同,而不是指向它的指针
  67. // Channel:通道的缓冲区用指定的缓冲区容量初始化。如果为零,或者忽略大小,则通道未被缓冲。
  68. // 信号 Signal 表示操作系统信号。通常的底层实现依赖于操作系统:在Unix上是syscall.Signal。
  69. quit := make(chan os.Signal, 1) // 创建一个接收信号的通道
  70. // kill 默认会发送 syscall.SIGTERM 信号
  71. // kill -2 发送 syscall.SIGINT 信号,Ctrl+C 就是触发系统SIGINT信号
  72. // kill -9 发送 syscall.SIGKILL 信号,但是不能被捕获,所以不需要添加它
  73. // signal.Notify把收到的 syscall.SIGINT或syscall.SIGTERM 信号转发给quit
  74. // Notify使包信号将传入的信号转发给c,如果没有提供信号,则将所有传入的信号转发给c,否则仅将提供的信号转发给c。
  75. // 包信号不会阻塞发送到c:调用者必须确保c有足够的缓冲空间来跟上预期的信号速率。对于仅用于通知一个信号值的通道,大小为1的缓冲区就足够了。
  76. // 允许使用同一通道多次调用Notify:每次调用都扩展发送到该通道的信号集。从集合中移除信号的唯一方法是调用Stop。
  77. // 允许使用不同的通道和相同的信号多次调用Notify:每个通道独立地接收传入信号的副本。
  78. signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) // 此处不会阻塞
  79. <-quit // 阻塞在此,当接收到上述两种信号时才会往下执行
  80. zap.L().Info("Shutdown Server ...")
  81. // 创建一个5秒超时的context
  82. ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  83. defer cancel()
  84. // 5秒内优雅关闭服务(将未处理完的请求处理完再关闭服务),超过5秒就超时退出
  85. // 关机将在不中断任何活动连接的情况下优雅地关闭服务器。
  86. // Shutdown的工作原理是首先关闭所有打开的侦听器,然后关闭所有空闲连接,然后无限期地等待连接返回空闲状态,然后关闭。
  87. // 如果提供的上下文在关闭完成之前过期,则shutdown返回上下文的错误,否则返回关闭服务器的底层侦听器所返回的任何错误。
  88. // 当Shutdown被调用时,Serve, ListenAndServe和ListenAndServeTLS会立即返回ErrServerClosed。确保程序没有退出,而是等待Shutdown返回。
  89. // 关闭不试图关闭或等待被劫持的连接,如WebSockets。如果需要的话,Shutdown的调用者应该单独通知这些长寿命连接关闭,并等待它们关闭。
  90. // 一旦在服务器上调用Shutdown,它可能不会被重用;以后对Serve等方法的调用将返回ErrServerClosed。
  91. if err := srv.Shutdown(ctx); err != nil {
  92. zap.L().Fatal("Server Shutdown", zap.Error(err))
  93. }
  94. zap.L().Info("Server exiting")
  95. }

运行

  1. Code/go/web_app2 via 🐹 v1.20.3 via 🅒 base
  2. go run main.go
  3. [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
  4. - using env: export GIN_MODE=release
  5. - using code: gin.SetMode(gin.ReleaseMode)
  6. [GIN-debug] GET /version --> web_app2/routes.Setup.func1 (3 handlers)

Git 提交

  1. ~/Code/go via 🐹 v1.20.3 via 🅒 base
  2. cd web_app
  3. Code/go/web_app via 🐹 v1.20.3 via 🅒 base
  4. echo "# go_web_app" >> README.md
  5. Code/go/web_app via 🐹 v1.20.3 via 🅒 base
  6. git init
  7. 提示:使用 'master' 作为初始分支的名称。这个默认分支名称可能会更改。要在新仓库中
  8. 提示:配置使用初始分支名,并消除这条警告,请执行:
  9. 提示:
  10. 提示: git config --global init.defaultBranch <名称>
  11. 提示:
  12. 提示:除了 'master' 之外,通常选定的名字有 'main''trunk' 'development'
  13. 提示:可以通过以下命令重命名刚创建的分支:
  14. 提示:
  15. 提示: git branch -m <name>
  16. 已初始化空的 Git 仓库于 /Users/qiaopengjun/Code/go/web_app/.git/
  17. web_app on master [?] via 🐹 v1.20.3 via 🅒 base
  18. git add .
  19. web_app on master [+] via 🐹 v1.20.3 via 🅒 base
  20. git commit -m "first commit"
  21. [master(根提交) 6b90b06] first commit
  22. 17 files changed, 1598 insertions(+)
  23. create mode 100644 .idea/.gitignore
  24. create mode 100644 .idea/dbnavigator.xml
  25. create mode 100644 .idea/modules.xml
  26. create mode 100644 .idea/watcherTasks.xml
  27. create mode 100644 .idea/web_app.iml
  28. create mode 100644 README.md
  29. create mode 100644 config.yaml
  30. create mode 100644 dao/mysql/mysql.go
  31. create mode 100644 dao/redis/redis.go
  32. create mode 100644 go.mod
  33. create mode 100644 go.sum
  34. create mode 100644 logger/logger.go
  35. create mode 100644 main.go
  36. create mode 100644 routes/routes.go
  37. create mode 100644 settings/settings.go
  38. create mode 100755 web_app
  39. create mode 100644 web_app.log
  40. web_app on master via 🐹 v1.20.3 via 🅒 base
  41. git branch -M main
  42. # go_web_app
  43. web_app on main via 🐹 v1.20.3 via 🅒 base
  44. git remote add origin git@github.com:qiaopengjun5162/go_web_app.git
  45. web_app on main via 🐹 v1.20.3 via 🅒 base
  46. git push -u origin main
  47. 枚举对象中: 26, 完成.
  48. 对象计数中: 100% (26/26), 完成.
  49. 使用 12 个线程进行压缩
  50. 压缩对象中: 100% (20/20), 完成.
  51. 写入对象中: 100% (26/26), 6.68 MiB | 467.00 KiB/s, 完成.
  52. 总共 26(差异 0),复用 0(差异 0),包复用 0
  53. To github.com:qiaopengjun5162/go_web_app.git
  54. * [new branch] main -> main
  55. 分支 'main' 设置为跟踪 'origin/main'
  56. web_app on main via 🐹 v1.20.3 via 🅒 base
  57. # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gi➜ vim README.md
  58. web_app on main [!] via 🐹 v1.20.3 via 🅒 base took 32.0s
  59. ga
  60. web_app on main [+] via 🐹 v1.20.3 via 🅒 base
  61. git commit -m "update readme"
  62. [main 9f5b231] update readme
  63. 1 file changed, 1 insertion(+)
  64. web_app on main [⇡] via 🐹 v1.20.3 via 🅒 base
  65. gp
  66. 枚举对象中: 5, 完成.
  67. 对象计数中: 100% (5/5), 完成.
  68. 使用 12 个线程进行压缩
  69. 压缩对象中: 100% (3/3), 完成.
  70. 写入对象中: 100% (3/3), 329 字节 | 329.00 KiB/s, 完成.
  71. 总共 3(差异 1),复用 0(差异 0),包复用 0
  72. remote: Resolving deltas: 100% (1/1), completed with 1 local object.
  73. To github.com:qiaopengjun5162/go_web_app.git
  74. 6b90b06..9f5b231 main -> main
  75. web_app on main via 🐹 v1.20.3 via 🅒 base took 4.1s
  76. vim .gitignore
  77. web_app on main [?] via 🐹 v1.20.3 via 🅒 base took 48.1s
  78. ga
  79. web_app on main [+] via 🐹 v1.20.3 via 🅒 base
  80. git commit -m "add gitignore"
  81. [main 827e99b] add gitignore
  82. 1 file changed, 23 insertions(+)
  83. create mode 100644 .gitignore
  84. web_app on main [⇡] via 🐹 v1.20.3 via 🅒 base
  85. gp
  86. 枚举对象中: 4, 完成.
  87. 对象计数中: 100% (4/4), 完成.
  88. 使用 12 个线程进行压缩
  89. 压缩对象中: 100% (3/3), 完成.
  90. 写入对象中: 100% (3/3), 586 字节 | 586.00 KiB/s, 完成.
  91. 总共 3(差异 1),复用 0(差异 0),包复用 0
  92. remote: Resolving deltas: 100% (1/1), completed with 1 local object.
  93. To github.com:qiaopengjun5162/go_web_app.git
  94. 9f5b231..827e99b main -> main
  95. # web_app2
  96. Code/go/web_app2 via 🐹 v1.20.3 via 🅒 base
  97. git init
  98. 提示:使用 'master' 作为初始分支的名称。这个默认分支名称可能会更改。要在新仓库中
  99. 提示:配置使用初始分支名,并消除这条警告,请执行:
  100. 提示:
  101. 提示: git config --global init.defaultBranch <名称>
  102. 提示:
  103. 提示:除了 'master' 之外,通常选定的名字有 'main''trunk' 'development'
  104. 提示:可以通过以下命令重命名刚创建的分支:
  105. 提示:
  106. 提示: git branch -m <name>
  107. 已初始化空的 Git 仓库于 /Users/qiaopengjun/Code/go/web_app2/.git/
  108. web_app2 on master [?] via 🐹 v1.20.3 via 🅒 base
  109. git add .
  110. web_app2 on master [+] via 🐹 v1.20.3 via 🅒 base
  111. git commit -m "first commit"
  112. [master(根提交) 00dd14d] first commit
  113. 15 files changed, 1597 insertions(+)
  114. create mode 100644 .idea/.gitignore
  115. create mode 100644 .idea/dbnavigator.xml
  116. create mode 100644 .idea/modules.xml
  117. create mode 100644 .idea/web_app2.iml
  118. create mode 100644 config.yaml
  119. create mode 100644 dao/mysql/mysql.go
  120. create mode 100644 dao/redis/redis.go
  121. create mode 100644 go.mod
  122. create mode 100644 go.sum
  123. create mode 100644 logger/logger.go
  124. create mode 100644 main.go
  125. create mode 100644 routes/routes.go
  126. create mode 100644 settings/settings.go
  127. create mode 100644 web_app.log
  128. create mode 100755 web_app2
  129. web_app2 on master via 🐹 v1.20.3 via 🅒 base
  130. git remote add origin git@github.com:qiaopengjun5162/go_web_app2.git
  131. web_app2 on master via 🐹 v1.20.3 via 🅒 base
  132. git push -u origin main
  133. 错误:源引用规格 main 没有匹配
  134. 错误:无法推送一些引用到 'github.com:qiaopengjun5162/go_web_app2.git'
  135. web_app2 on master via 🐹 v1.20.3 via 🅒 base
  136. git branch -M main
  137. web_app2 on main via 🐹 v1.20.3 via 🅒 base
  138. git push -u origin main
  139. To github.com:qiaopengjun5162/go_web_app2.git
  140. ! [rejected] main -> main (fetch first)
  141. 错误:无法推送一些引用到 'github.com:qiaopengjun5162/go_web_app2.git'
  142. 提示:更新被拒绝,因为远程仓库包含您本地尚不存在的提交。这通常是因为另外
  143. 提示:一个仓库已向该引用进行了推送。再次推送前,您可能需要先整合远程变更
  144. 提示:(如 'git pull ...')。
  145. 提示:详见 'git push --help' 中的 'Note about fast-forwards' 小节。
  146. ...
  147. web_app2 on main via 🐹 v1.20.3 via 🅒 base
  148. git push -u origin main
  149. To github.com:qiaopengjun5162/go_web_app2.git
  150. ! [rejected] main -> main (non-fast-forward)
  151. 错误:无法推送一些引用到 'github.com:qiaopengjun5162/go_web_app2.git'
  152. 提示:更新被拒绝,因为您当前分支的最新提交落后于其对应的远程分支。
  153. 提示:再次推送前,先与远程变更合并(如 'git pull ...')。详见
  154. 提示:'git push --help' 中的 'Note about fast-forwards' 小节。
  155. web_app2 on main via 🐹 v1.20.3 via 🅒 base took 4.1s
  156. git fetch origin
  157. web_app2 on main via 🐹 v1.20.3 via 🅒 base took 4.1s
  158. git merge origin/main
  159. 致命错误:拒绝合并无关的历史
  160. web_app2 on main via 🐹 v1.20.3 via 🅒 base
  161. git pull origin main --allow-unrelated-histories
  162. 来自 github.com:qiaopengjun5162/go_web_app2
  163. * branch main -> FETCH_HEAD
  164. Merge made by the 'ort' strategy.
  165. .gitignore | 21 +++++++++++++++++++++
  166. LICENSE | 21 +++++++++++++++++++++
  167. README.md | 2 ++
  168. 3 files changed, 44 insertions(+)
  169. create mode 100644 .gitignore
  170. create mode 100644 LICENSE
  171. create mode 100644 README.md
  172. web_app2 on main via 🐹 v1.20.3 via 🅒 base took 1m 37.5s
  173. git push -u origin main
  174. 枚举对象中: 27, 完成.
  175. 对象计数中: 100% (27/27), 完成.
  176. 使用 12 个线程进行压缩
  177. 压缩对象中: 100% (21/21), 完成.
  178. 写入对象中: 100% (26/26), 6.75 MiB | 681.00 KiB/s, 完成.
  179. 总共 26(差异 1),复用 0(差异 0),包复用 0
  180. remote: Resolving deltas: 100% (1/1), done.
  181. To github.com:qiaopengjun5162/go_web_app2.git
  182. 62e4da1..e023f0b main -> main
  183. 分支 'main' 设置为跟踪 'origin/main'
  184. web_app2 on main via 🐹 v1.20.3 via 🅒 base took 14.4s

通过命令行程序获取配置文件的路径

问题:目前只能在可执行文件所在项目目录下执行

  1. web_app2 on main via 🐹 v1.20.3 via 🅒 base
  2. ./web_app2
  3. [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
  4. - using env: export GIN_MODE=release
  5. - using code: gin.SetMode(gin.ReleaseMode)
  6. [GIN-debug] GET /version --> web_app2/routes.Setup.func1 (3 handlers)
  7. ^C%
  8. web_app2 on main [!] via 🐹 v1.20.3 via 🅒 base took 7.6s
  9. cd ..
  10. ~/Code/go via 🐹 v1.20.3 via 🅒 base
  11. web_app2/web_app2
  12. viper.ReadInConfig failed, error: Config File "config" Not Found in "[/Users/qiaopengjun/Code/go]"
  13. init settings failed, error: Config File "config" Not Found in "[/Users/qiaopengjun/Code/go]"
  14. ~/Code/go via 🐹 v1.20.3 via 🅒 base

web_app2/web_app2 执行,viper 读取配置文件失败,找不到配置文件。

使用 os.Args 优化修改

settings.go

  1. package settings
  2. import (
  3. "fmt"
  4. "github.com/fsnotify/fsnotify"
  5. "github.com/spf13/viper"
  6. )
  7. // Conf 全局变量,用来保存程序的所有配置信息
  8. var Conf = new(AppConfig)
  9. type AppConfig struct {
  10. Name string `mapstructure:"name"`
  11. Mode string `mapstructure:"mode"`
  12. Version string `mapstructure:"version"`
  13. Port int `mapstructure:"port"`
  14. *LogConfig `mapstructure:"log"`
  15. *MySQLConfig `mapstructure:"mysql"`
  16. *RedisConfig `mapstructure:"redis"`
  17. }
  18. type LogConfig struct {
  19. Level string `mapstructure:"level"`
  20. Filename string `mapstructure:"filename"`
  21. MaxSize int `mapstructure:"max_size"`
  22. MaxAge int `mapstructure:"max_age"`
  23. MaxBackups int `mapstructure:"max_backups"`
  24. }
  25. type MySQLConfig struct {
  26. Host string `mapstructure:"host"`
  27. User string `mapstructure:"user"`
  28. Password string `mapstructure:"password"`
  29. DbName string `mapstructure:"db_name"`
  30. Port int `mapstructure:"port"`
  31. MaxOpenConns int `mapstructure:"max_open_conns"`
  32. MaxIdleConns int `mapstructure:"max_idle_conns"`
  33. }
  34. type RedisConfig struct {
  35. Host string `mapstructure:"host"`
  36. Password string `mapstructure:"password"`
  37. Port int `matstructure:"port"`
  38. DB int `mapstructure:"db"`
  39. PoolSize int `mapstructure:"pool_size"`
  40. }
  41. func Init(filePath string) (err error) {
  42. // 方式1:直接指定配置文件路径(相对路径或者绝对路径)
  43. // 相对路径:相对执行的可执行文件的相对路径
  44. // viper.SetConfigFile("./conf/config.yaml")
  45. // 绝对路径:系统中实际的文件路径
  46. // viper.SetConfigFile("/Users/qiaopengjun/Desktop/web_app2 /conf/config.yaml")
  47. // 方式2:指定配置文件名和配置文件的位置,viper 自行查找可用的配置文件
  48. // 配置文件名不需要带后缀
  49. // 配置文件位置可配置多个
  50. // 注意:viper 是根据文件名查找,配置目录里不要有同名的配置文件。
  51. // 例如:在配置目录 ./conf 中不要同时存在 config.yaml、config.json
  52. // 读取配置文件
  53. viper.SetConfigFile(filePath) // 指定配置文件路径
  54. //viper.SetConfigName("config") // 配置文件名称(无扩展名)
  55. //viper.AddConfigPath(".") // 指定查找配置文件的路径(这里使用相对路径)可以配置多个
  56. //viper.AddConfigPath("./conf") // 指定查找配置文件的路径(这里使用相对路径)可以配置多个
  57. // SetConfigType设置远端源返回的配置类型,例如:“json”。
  58. // 基本上是配合远程配置中心使用的,告诉viper 当前的数据使用什么格式去解析
  59. //viper.SetConfigType("yaml")
  60. err = viper.ReadInConfig() // 查找并读取配置文件
  61. if err != nil { // 处理读取配置文件的错误
  62. fmt.Printf("viper.ReadInConfig failed, error: %v\n", err)
  63. return
  64. }
  65. // 把读取到的配置信息反序列化到 Conf 变量中
  66. if err = viper.Unmarshal(Conf); err != nil {
  67. fmt.Printf("viper unmarshal failed, error: %v\n", err)
  68. return
  69. }
  70. // 实时监控配置文件的变化 WatchConfig 开始监视配置文件的更改。
  71. viper.WatchConfig()
  72. // OnConfigChange设置配置文件更改时调用的事件处理程序。
  73. // 当配置文件变化之后调用的一个回调函数
  74. viper.OnConfigChange(func(e fsnotify.Event) {
  75. fmt.Println("Config file changed:", e.Name)
  76. if err = viper.Unmarshal(Conf); err != nil {
  77. fmt.Printf("viper unmarshal OnConfigChange failed, error: %v\n", err)
  78. }
  79. })
  80. return
  81. }

main.go

  1. package main
  2. import (
  3. "context"
  4. "fmt"
  5. "log"
  6. "net/http"
  7. "os"
  8. "os/signal"
  9. "syscall"
  10. "time"
  11. "web_app2/dao/mysql"
  12. "web_app2/dao/redis"
  13. "web_app2/logger"
  14. "web_app2/routes"
  15. "web_app2/settings"
  16. "go.uber.org/zap"
  17. )
  18. // Go Web 开发通用的脚手架模版
  19. func main() {
  20. if len(os.Args) < 2 {
  21. fmt.Println("please need config file.eg: web_app2 config.yaml")
  22. return
  23. }
  24. // 1. 加载配置
  25. if err := settings.Init(os.Args[1]); err != nil {
  26. fmt.Printf("init settings failed, error: %v\n", err)
  27. return
  28. }
  29. // 2. 初始化日志
  30. if err := logger.Init(settings.Conf.LogConfig); err != nil {
  31. fmt.Printf("init logger failed, error: %v\n", err)
  32. return
  33. }
  34. defer zap.L().Sync()
  35. zap.L().Debug("logger initialized successfully")
  36. // 3. 初始化 MySQL 连接
  37. if err := mysql.Init(settings.Conf.MySQLConfig); err != nil {
  38. fmt.Printf("init mysql failed, error: %v\n", err)
  39. return
  40. }
  41. defer mysql.Close()
  42. // 4. 初始化 Redis 连接
  43. if err := redis.Init(settings.Conf.RedisConfig); err != nil {
  44. fmt.Printf("init redis failed, error: %v\n", err)
  45. return
  46. }
  47. defer redis.Close()
  48. // 5. 注册路由
  49. router := routes.Setup()
  50. // 6. 启动服务(优雅关机)
  51. // 服务器定义运行HTTP服务器的参数。Server的零值是一个有效的配置。
  52. srv := &http.Server{
  53. // Addr可选地以“host:port”的形式指定服务器要监听的TCP地址。如果为空,则使用“:http”(端口80)。
  54. // 服务名称在RFC 6335中定义,并由IANA分配
  55. Addr: fmt.Sprintf(":%d", settings.Conf.Port),
  56. Handler: router,
  57. }
  58. go func() {
  59. // 开启一个goroutine启动服务,如果不用 goroutine,下面的代码 ListenAndServe 会一直接收请求,处理请求,进入无限循环。代码就不会往下执行。
  60. // ListenAndServe监听TCP网络地址srv.Addr,然后调用Serve来处理传入连接上的请求。接受的连接配置为使TCP能保持连接。
  61. // ListenAndServe always returns a non-nil error. After Shutdown or Close,
  62. // the returned error is ErrServerClosed.
  63. if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
  64. log.Fatalf("listen: %s\n", err) // Fatalf 相当于Printf()之后再调用os.Exit(1)。
  65. }
  66. }()
  67. // 等待中断信号来优雅地关闭服务器,为关闭服务器操作设置一个5秒的超时
  68. // make内置函数分配并初始化(仅)slice、map或chan类型的对象。
  69. // 与new一样,第一个参数是类型,而不是值。
  70. // 与new不同,make的返回类型与其参数的类型相同,而不是指向它的指针
  71. // Channel:通道的缓冲区用指定的缓冲区容量初始化。如果为零,或者忽略大小,则通道未被缓冲。
  72. // 信号 Signal 表示操作系统信号。通常的底层实现依赖于操作系统:在Unix上是syscall.Signal。
  73. quit := make(chan os.Signal, 1) // 创建一个接收信号的通道
  74. // kill 默认会发送 syscall.SIGTERM 信号
  75. // kill -2 发送 syscall.SIGINT 信号,Ctrl+C 就是触发系统SIGINT信号
  76. // kill -9 发送 syscall.SIGKILL 信号,但是不能被捕获,所以不需要添加它
  77. // signal.Notify把收到的 syscall.SIGINT或syscall.SIGTERM 信号转发给quit
  78. // Notify使包信号将传入的信号转发给c,如果没有提供信号,则将所有传入的信号转发给c,否则仅将提供的信号转发给c。
  79. // 包信号不会阻塞发送到c:调用者必须确保c有足够的缓冲空间来跟上预期的信号速率。对于仅用于通知一个信号值的通道,大小为1的缓冲区就足够了。
  80. // 允许使用同一通道多次调用Notify:每次调用都扩展发送到该通道的信号集。从集合中移除信号的唯一方法是调用Stop。
  81. // 允许使用不同的通道和相同的信号多次调用Notify:每个通道独立地接收传入信号的副本。
  82. signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) // 此处不会阻塞
  83. <-quit // 阻塞在此,当接收到上述两种信号时才会往下执行
  84. zap.L().Info("Shutdown Server ...")
  85. // 创建一个5秒超时的context
  86. ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  87. defer cancel()
  88. // 5秒内优雅关闭服务(将未处理完的请求处理完再关闭服务),超过5秒就超时退出
  89. // 关机将在不中断任何活动连接的情况下优雅地关闭服务器。
  90. // Shutdown的工作原理是首先关闭所有打开的侦听器,然后关闭所有空闲连接,然后无限期地等待连接返回空闲状态,然后关闭。
  91. // 如果提供的上下文在关闭完成之前过期,则shutdown返回上下文的错误,否则返回关闭服务器的底层侦听器所返回的任何错误。
  92. // 当Shutdown被调用时,Serve, ListenAndServe和ListenAndServeTLS会立即返回ErrServerClosed。确保程序没有退出,而是等待Shutdown返回。
  93. // 关闭不试图关闭或等待被劫持的连接,如WebSockets。如果需要的话,Shutdown的调用者应该单独通知这些长寿命连接关闭,并等待它们关闭。
  94. // 一旦在服务器上调用Shutdown,它可能不会被重用;以后对Serve等方法的调用将返回ErrServerClosed。
  95. if err := srv.Shutdown(ctx); err != nil {
  96. zap.L().Fatal("Server Shutdown", zap.Error(err))
  97. }
  98. zap.L().Info("Server exiting")
  99. }

运行

  1. web_app2 on main [!] via 🐹 v1.20.3 via 🅒 base
  2. go build
  3. web_app2 on main [!] via 🐹 v1.20.3 via 🅒 base
  4. ./web_app2
  5. please need config file.eg: web_app2 config.yaml
  6. web_app2 on main [!] via 🐹 v1.20.3 via 🅒 base
  7. ./web_app2 config.yaml
  8. [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
  9. - using env: export GIN_MODE=release
  10. - using code: gin.SetMode(gin.ReleaseMode)
  11. [GIN-debug] GET /version --> web_app2/routes.Setup.func1 (3 handlers)
  12. ~/Code/go via 🐹 v1.20.3 via 🅒 base
  13. web_app2/web_app2 web_app2/config.yaml
  14. [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
  15. - using env: export GIN_MODE=release
  16. - using code: gin.SetMode(gin.ReleaseMode)
  17. [GIN-debug] GET /version --> web_app2/routes.Setup.func1 (3 handlers)
使用 flag包 优化修改

main.go

  1. package main
  2. import (
  3. "context"
  4. "flag"
  5. "fmt"
  6. "log"
  7. "net/http"
  8. "os"
  9. "os/signal"
  10. "syscall"
  11. "time"
  12. "web_app2/dao/mysql"
  13. "web_app2/dao/redis"
  14. "web_app2/logger"
  15. "web_app2/routes"
  16. "web_app2/settings"
  17. "go.uber.org/zap"
  18. )
  19. // Go Web 开发通用的脚手架模版
  20. func main() {
  21. filename := flag.String("filename", "config.yaml", "config file")
  22. // 解析命令行参数
  23. flag.Parse()
  24. fmt.Println(*filename)
  25. //返回命令行参数后的其他参数
  26. fmt.Println(flag.Args())
  27. //返回命令行参数后的其他参数个数
  28. fmt.Println("NArg", flag.NArg())
  29. //返回使用的命令行参数个数
  30. fmt.Println("NFlag", flag.NFlag())
  31. if flag.NArg() != 1 || flag.NArg() != 1 {
  32. fmt.Println("please need config file.eg: web_app2 config.yaml")
  33. return
  34. }
  35. // 1. 加载配置
  36. if err := settings.Init(*filename); err != nil {
  37. fmt.Printf("init settings failed, error: %v\n", err)
  38. return
  39. }
  40. // 2. 初始化日志
  41. if err := logger.Init(settings.Conf.LogConfig); err != nil {
  42. fmt.Printf("init logger failed, error: %v\n", err)
  43. return
  44. }
  45. defer zap.L().Sync()
  46. zap.L().Debug("logger initialized successfully")
  47. // 3. 初始化 MySQL 连接
  48. if err := mysql.Init(settings.Conf.MySQLConfig); err != nil {
  49. fmt.Printf("init mysql failed, error: %v\n", err)
  50. return
  51. }
  52. defer mysql.Close()
  53. // 4. 初始化 Redis 连接
  54. if err := redis.Init(settings.Conf.RedisConfig); err != nil {
  55. fmt.Printf("init redis failed, error: %v\n", err)
  56. return
  57. }
  58. defer redis.Close()
  59. // 5. 注册路由
  60. router := routes.Setup()
  61. // 6. 启动服务(优雅关机)
  62. // 服务器定义运行HTTP服务器的参数。Server的零值是一个有效的配置。
  63. srv := &http.Server{
  64. // Addr可选地以“host:port”的形式指定服务器要监听的TCP地址。如果为空,则使用“:http”(端口80)。
  65. // 服务名称在RFC 6335中定义,并由IANA分配
  66. Addr: fmt.Sprintf(":%d", settings.Conf.Port),
  67. Handler: router,
  68. }
  69. go func() {
  70. // 开启一个goroutine启动服务,如果不用 goroutine,下面的代码 ListenAndServe 会一直接收请求,处理请求,进入无限循环。代码就不会往下执行。
  71. // ListenAndServe监听TCP网络地址srv.Addr,然后调用Serve来处理传入连接上的请求。接受的连接配置为使TCP能保持连接。
  72. // ListenAndServe always returns a non-nil error. After Shutdown or Close,
  73. // the returned error is ErrServerClosed.
  74. if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
  75. log.Fatalf("listen: %s\n", err) // Fatalf 相当于Printf()之后再调用os.Exit(1)。
  76. }
  77. }()
  78. // 等待中断信号来优雅地关闭服务器,为关闭服务器操作设置一个5秒的超时
  79. // make内置函数分配并初始化(仅)slice、map或chan类型的对象。
  80. // 与new一样,第一个参数是类型,而不是值。
  81. // 与new不同,make的返回类型与其参数的类型相同,而不是指向它的指针
  82. // Channel:通道的缓冲区用指定的缓冲区容量初始化。如果为零,或者忽略大小,则通道未被缓冲。
  83. // 信号 Signal 表示操作系统信号。通常的底层实现依赖于操作系统:在Unix上是syscall.Signal。
  84. quit := make(chan os.Signal, 1) // 创建一个接收信号的通道
  85. // kill 默认会发送 syscall.SIGTERM 信号
  86. // kill -2 发送 syscall.SIGINT 信号,Ctrl+C 就是触发系统SIGINT信号
  87. // kill -9 发送 syscall.SIGKILL 信号,但是不能被捕获,所以不需要添加它
  88. // signal.Notify把收到的 syscall.SIGINT或syscall.SIGTERM 信号转发给quit
  89. // Notify使包信号将传入的信号转发给c,如果没有提供信号,则将所有传入的信号转发给c,否则仅将提供的信号转发给c。
  90. // 包信号不会阻塞发送到c:调用者必须确保c有足够的缓冲空间来跟上预期的信号速率。对于仅用于通知一个信号值的通道,大小为1的缓冲区就足够了。
  91. // 允许使用同一通道多次调用Notify:每次调用都扩展发送到该通道的信号集。从集合中移除信号的唯一方法是调用Stop。
  92. // 允许使用不同的通道和相同的信号多次调用Notify:每个通道独立地接收传入信号的副本。
  93. signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) // 此处不会阻塞
  94. <-quit // 阻塞在此,当接收到上述两种信号时才会往下执行
  95. zap.L().Info("Shutdown Server ...")
  96. // 创建一个5秒超时的context
  97. ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  98. defer cancel()
  99. // 5秒内优雅关闭服务(将未处理完的请求处理完再关闭服务),超过5秒就超时退出
  100. // 关机将在不中断任何活动连接的情况下优雅地关闭服务器。
  101. // Shutdown的工作原理是首先关闭所有打开的侦听器,然后关闭所有空闲连接,然后无限期地等待连接返回空闲状态,然后关闭。
  102. // 如果提供的上下文在关闭完成之前过期,则shutdown返回上下文的错误,否则返回关闭服务器的底层侦听器所返回的任何错误。
  103. // 当Shutdown被调用时,Serve, ListenAndServe和ListenAndServeTLS会立即返回ErrServerClosed。确保程序没有退出,而是等待Shutdown返回。
  104. // 关闭不试图关闭或等待被劫持的连接,如WebSockets。如果需要的话,Shutdown的调用者应该单独通知这些长寿命连接关闭,并等待它们关闭。
  105. // 一旦在服务器上调用Shutdown,它可能不会被重用;以后对Serve等方法的调用将返回ErrServerClosed。
  106. if err := srv.Shutdown(ctx); err != nil {
  107. zap.L().Fatal("Server Shutdown", zap.Error(err))
  108. }
  109. zap.L().Info("Server exiting")
  110. }

运行

  1. web_app2 on main [!] via 🐹 v1.20.3 via 🅒 base
  2. go build
  3. web_app2 on main [!] via 🐹 v1.20.3 via 🅒 base
  4. ./web_app2 -filename config.yaml
  5. config.yaml
  6. []
  7. NArg 0
  8. NFlag 1
  9. [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
  10. - using env: export GIN_MODE=release
  11. - using code: gin.SetMode(gin.ReleaseMode)
  12. [GIN-debug] GET /version --> web_app2/routes.Setup.func1 (3 handlers)
  13. ^C%
  14. web_app2 on main [!] via 🐹 v1.20.3 via 🅒 base took 17.8s
  15. ./web_app2 -filename=config.yaml
  16. config.yaml
  17. []
  18. NArg 0
  19. NFlag 1
  20. [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
  21. - using env: export GIN_MODE=release
  22. - using code: gin.SetMode(gin.ReleaseMode)
  23. [GIN-debug] GET /version --> web_app2/routes.Setup.func1 (3 handlers)
  24. ^C%
  25. web_app2 on main [!] via 🐹 v1.20.3 via 🅒 base took 9.2s
  26. ./web_app2 --filename=config.yaml
  27. config.yaml
  28. []
  29. NArg 0
  30. NFlag 1
  31. [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
  32. - using env: export GIN_MODE=release
  33. - using code: gin.SetMode(gin.ReleaseMode)
  34. [GIN-debug] GET /version --> web_app2/routes.Setup.func1 (3 handlers)
  35. ^C%
  36. web_app2 on main [!] via 🐹 v1.20.3 via 🅒 base took 3.8s
  37. ./web_app2 --filename config.yaml
  38. config.yaml
  39. []
  40. NArg 0
  41. NFlag 1
  42. [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
  43. - using env: export GIN_MODE=release
  44. - using code: gin.SetMode(gin.ReleaseMode)
  45. [GIN-debug] GET /version --> web_app2/routes.Setup.func1 (3 handlers)
  46. ^C%
  47. web_app2 on main [!] via 🐹 v1.20.3 via 🅒 base took 2.7s
  48. ./web_app2 config.yaml
  49. config.yaml
  50. [config.yaml]
  51. NArg 1
  52. NFlag 0
  53. [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
  54. - using env: export GIN_MODE=release
  55. - using code: gin.SetMode(gin.ReleaseMode)
  56. [GIN-debug] GET /version --> web_app2/routes.Setup.func1 (3 handlers)

更多相关信息,, https://t.me/gtokentool


本文转载自: https://blog.csdn.net/2408_87746709/article/details/143400669
版权归原作者 加密新世界 所有, 如有侵权,请联系我们删除。

“Go 语言之搭建通用 Web 项目开发脚手架”的评论:

还没有评论