CLD模式
# CLD模式
# 链接资料
- 博客
08.web脚手架 | 不做大哥好多年 (opens new window)
# 00.项目结构
# # (opens new window) 0.1 新建项目
# # (opens new window) 0.2 项目结构图
CLD + 模型
controller
logic
dao
models
日志、路由、三方库
logger
routes
pkg
main.go、配置文件
main.go
conf
settings
# # (opens new window) 0.3 初始化main.go
func main() {
// 1. 加载配置(viper配置管理)
// 2. 初始化日志(zap日志库)
// 3. 初始化MySQL连接(sqlx)
// 4. 初始化Redis连接(go-redis)
// 5. 注册路由
// 6. 启动服务(优雅关机)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1234567891011121314
# # (opens new window) 01.第一: 加载配置
# # (opens new window) 1.1 config.yaml
name: "web_app"
mode: "dev"
port: 8080
version: "v0.1.4"
# 雪花算法:开始时间 机器ID
start_time: "2020-07-01"
machine_id: 1
log:
level: "debug"
filename: "web_app.log"
max_size: 200
max_age: 30
max_backups: 7
mysql:
host: "127.0.0.1"
port: 3306
user: "root"
password: "chnsys@2016"
dbname: "gin_bbs"
max_open_conns: 200
max_idle_conns: 50
redis:
host: "127.0.0.1"
port: 6379
password: ""
db: 0
pool_size: 100
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
1234567891011121314151617181920212223242526272829
# # (opens new window) 1.2 settings/settings.go
package settings
import (
"fmt"
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
)
// Conf 全局变量,用来保存程序的所有配置信息
var Conf = new(AppConfig)
type AppConfig struct {
Name string `mapstructure:"name"`
Mode string `mapstructure:"mode"`
Version string `mapstructure:"version"`
Port int `mapstructure:"port"`
StartTime string `mapstructure:"start_time"`
MachineID int64 `mapstructure:"machine_id"`
*LogConfig `mapstructure:"log"`
*MySQLConfig `mapstructure:"mysql"`
*RedisConfig `mapstructure:"redis"`
}
type LogConfig struct {
Level string `mapstructure:"level"`
Filename string `mapstructure:"filename"`
MaxSize int `mapstructure:"max_size"`
MaxAge int `mapstructure:"max_age"`
MaxBackups int `mapstructure:"max_backups"`
}
type MySQLConfig struct {
Host string `mapstructure:"host"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
DbName string `mapstructure:"dbname"`
Port int `mapstructure:"port"`
MaxOpenConns int `mapstructure:"max_open_conns"`
MaxIdleConns int `mapstructure:"max_idle_conns"`
}
type RedisConfig struct {
Host string `mapstructure:"host"`
Password string `mapstructure:"password"`
Port int `mapstructure:"port"`
DB int `mapstructure:"db"`
PoolSize int `mapstructure:"pool_size"`
}
func Init() (err error) {
viper.SetConfigFile("conf/config.yaml")
//viper.SetConfigName("config") // 指定配置文件名称(不需要带后缀)
//viper.SetConfigType("yaml") // 指定配置文件类型(专用于从远程获取配置信息时指定配置文件类型的)
viper.AddConfigPath(".") // 指定查找配置文件的路径(这里使用相对路径)
err = viper.ReadInConfig() // 读取配置信息
if err != nil {
// 读取配置信息失败
fmt.Printf("viper.ReadInConfig() failed, err:%v\n", err)
return
}
// 把读取到的配置信息反序列化到 Conf 变量中
if err := viper.Unmarshal(Conf); err != nil {
fmt.Printf("viper.Unmarshal failed, err:%v\n", err)
}
viper.WatchConfig()
viper.OnConfigChange(func(in fsnotify.Event) {
fmt.Println("配置文件修改了,重新加载到全局Conf ...")
if err := viper.Unmarshal(Conf); err != nil {
fmt.Printf("viper.Unmarshal failed, err:%v\n", err)
}
})
return
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374
# # (opens new window) 1.3 main.go
package main
import (
"fmt"
"my_web/settings"
)
func main() {
// 1. 加载配置(viper配置管理)
if err := settings.Init(); err != nil {
fmt.Printf("init settings failed, err:%v\n", err)
return
}
fmt.Println(settings.Conf)
fmt.Println(settings.Conf.Name) // web_app
fmt.Println(settings.Conf.LogConfig == nil)
// 2. 初始化日志(zap日志库)
// 3. 初始化MySQL连接(sqlx)
// 4. 初始化Redis连接(go-redis)
// 5. 注册路由
// 6. 启动服务(优雅关机)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
12345678910111213141516171819202122232425262728
# # (opens new window) 02.第二: 初始化日志
# # (opens new window) 2.1 logger/logger.go
package logger
import (
"github.com/gin-gonic/gin"
"github.com/natefinch/lumberjack"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"net"
"net/http"
"net/http/httputil"
"os"
"runtime/debug"
"strings"
"time"
"web_app/settings"
)
var lg *zap.Logger
// Init 初始化lg
func Init(cfg *settings.LogConfig, mode string) (err error) {
writeSyncer := getLogWriter(cfg.Filename, cfg.MaxSize, cfg.MaxBackups, cfg.MaxAge)
encoder := getEncoder()
var l = new(zapcore.Level)
err = l.UnmarshalText([]byte(cfg.Level))
if err != nil {
return
}
var core zapcore.Core
if mode == "dev" {
// 进入开发模式,日志输出到终端
consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig())
core = zapcore.NewTee(
zapcore.NewCore(encoder, writeSyncer, l),
zapcore.NewCore(consoleEncoder, zapcore.Lock(os.Stdout), zapcore.DebugLevel),
)
} else {
// 如果不是dev模式,就记录日志到日志文件中
core = zapcore.NewCore(encoder, writeSyncer, l)
}
lg = zap.New(core, zap.AddCaller())
zap.ReplaceGlobals(lg)
zap.L().Info("init logger success")
return
}
func getEncoder() zapcore.Encoder {
encoderConfig := zap.NewProductionEncoderConfig()
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
encoderConfig.TimeKey = "time"
encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
encoderConfig.EncodeDuration = zapcore.SecondsDurationEncoder
encoderConfig.EncodeCaller = zapcore.ShortCallerEncoder
return zapcore.NewJSONEncoder(encoderConfig)
}
func getLogWriter(filename string, maxSize, maxBackup, maxAge int) zapcore.WriteSyncer {
lumberJackLogger := &lumberjack.Logger{
Filename: filename,
MaxSize: maxSize,
MaxBackups: maxBackup,
MaxAge: maxAge,
}
return zapcore.AddSync(lumberJackLogger)
}
// GinLogger 接收gin框架默认的日志
func GinLogger() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
query := c.Request.URL.RawQuery
c.Next()
cost := time.Since(start)
lg.Info(path,
zap.Int("status", c.Writer.Status()),
zap.String("method", c.Request.Method),
zap.String("path", path),
zap.String("query", query),
zap.String("ip", c.ClientIP()),
zap.String("user-agent", c.Request.UserAgent()),
zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()),
zap.Duration("cost", cost),
)
}
}
// GinRecovery recover掉项目可能出现的panic,并使用zap记录相关日志
func GinRecovery(stack bool) gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
// Check for a broken connection, as it is not really a
// condition that warrants a panic stack trace.
var brokenPipe bool
if ne, ok := err.(*net.OpError); ok {
if se, ok := ne.Err.(*os.SyscallError); ok {
if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
brokenPipe = true
}
}
}
httpRequest, _ := httputil.DumpRequest(c.Request, false)
if brokenPipe {
lg.Error(c.Request.URL.Path,
zap.Any("error", err),
zap.String("request", string(httpRequest)),
)
// If the connection is dead, we can't write a status to it.
c.Error(err.(error)) // nolint: errcheck
c.Abort()
return
}
if stack {
lg.Error("[Recovery from panic]",
zap.Any("error", err),
zap.String("request", string(httpRequest)),
zap.String("stack", string(debug.Stack())),
)
} else {
lg.Error("[Recovery from panic]",
zap.Any("error", err),
zap.String("request", string(httpRequest)),
)
}
c.AbortWithStatus(http.StatusInternalServerError)
}
}()
c.Next()
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
# # (opens new window) 2.2 main.go
package main
import (
"fmt"
"go.uber.org/zap"
"my_web/logger"
"my_web/settings"
)
func main() {
// 1. 加载配置(viper配置管理)
if err := settings.Init(); err != nil {
fmt.Printf("init settings failed, err:%v\n", err)
return
}
fmt.Println(settings.Conf)
fmt.Println(settings.Conf.Name) // web_app
fmt.Println(settings.Conf.LogConfig == nil)
// 2. 初始化日志(zap日志库)
if err := logger.Init(settings.Conf.LogConfig, settings.Conf.Mode); err != nil {
fmt.Printf("init logger failed, err:%v\n", err)
return
}
defer zap.L().Sync()
zap.L().Debug("logger init success...")
// 3. 初始化MySQL连接(sqlx)
// 4. 初始化Redis连接(go-redis)
// 5. 注册路由
// 6. 启动服务(优雅关机)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
12345678910111213141516171819202122232425262728293031323334
# # (opens new window) 03.第三: 初始化mysql连接
# # (opens new window) 3.1 dao/mysql/mysql.go
package mysql
import (
"fmt"
_ "github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
"go.uber.org/zap"
"web_app/settings"
)
var db *sqlx.DB
func Init(cfg *settings.MySQLConfig) (err error) {
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True",
cfg.User,
cfg.Password,
cfg.Host,
cfg.Port,
cfg.DbName,
)
// 也可以使用MustConnect连接不成功就panic
db, err = sqlx.Connect("mysql", dsn)
if err != nil {
zap.L().Error("connect DB failed", zap.Error(err))
return
}
db.SetMaxOpenConns(cfg.MaxOpenConns)
db.SetMaxIdleConns(cfg.MaxIdleConns)
return
}
func Close() {
_ = db.Close()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
12345678910111213141516171819202122232425262728293031323334
# # (opens new window) 3.2 main.go
package main
import (
"fmt"
"go.uber.org/zap"
"my_web/dao/mysql"
"my_web/logger"
"my_web/settings"
)
func main() {
// 1. 加载配置(viper配置管理)
if err := settings.Init(); err != nil {
fmt.Printf("init settings failed, err:%v\n", err)
return
}
fmt.Println(settings.Conf)
fmt.Println(settings.Conf.Name) // web_app
fmt.Println(settings.Conf.LogConfig == nil)
// 2. 初始化日志(zap日志库)
if err := logger.Init(settings.Conf.LogConfig, settings.Conf.Mode); err != nil {
fmt.Printf("init logger failed, err:%v\n", err)
return
}
defer zap.L().Sync()
zap.L().Debug("logger init success...")
// 3. 初始化MySQL连接(sqlx)
if err := mysql.Init(settings.Conf.MySQLConfig); err != nil {
fmt.Printf("init mysql failed, err:%v\n", err)
return
}
defer mysql.Close()
// 4. 初始化Redis连接(go-redis)
// 5. 注册路由
// 6. 启动服务(优雅关机)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
123456789101112131415161718192021222324252627282930313233343536373839
# # (opens new window) 04.初始化redis连接
# # (opens new window) 4.1 dao/redis/redis.go
package redis
import (
"fmt"
"web_app/settings"
"github.com/go-redis/redis"
)
// 声明一个全局的rdb变量
var rdb *redis.Client
// Init 初始化连接
func Init(cfg *settings.RedisConfig) (err error) {
rdb = redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%d",
cfg.Host,
cfg.Port,
),
Password: cfg.Password, // no password set
DB: cfg.DB, // use default DB
PoolSize: cfg.PoolSize,
})
_, err = rdb.Ping().Result()
return
}
func Close() {
_ = rdb.Close()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
1234567891011121314151617181920212223242526272829
# # (opens new window) 4.2 mian.go
package main
import (
"fmt"
"go.uber.org/zap"
"my_web/dao/mysql"
"my_web/dao/redis"
"my_web/logger"
"my_web/settings"
)
func main() {
// 1. 加载配置(viper配置管理)
if err := settings.Init(); err != nil {
fmt.Printf("init settings failed, err:%v\n", err)
return
}
fmt.Println(settings.Conf)
fmt.Println(settings.Conf.Name) // web_app
fmt.Println(settings.Conf.LogConfig == nil)
// 2. 初始化日志(zap日志库)
if err := logger.Init(settings.Conf.LogConfig, settings.Conf.Mode); err != nil {
fmt.Printf("init logger failed, err:%v\n", err)
return
}
defer zap.L().Sync()
zap.L().Debug("logger init success...")
// 3. 初始化MySQL连接(sqlx)
if err := mysql.Init(settings.Conf.MySQLConfig); err != nil {
fmt.Printf("init mysql failed, err:%v\n", err)
return
}
defer mysql.Close()
// 4. 初始化Redis连接(go-redis)
if err := redis.Init(settings.Conf.RedisConfig); err != nil {
fmt.Printf("init redis failed, err:%v\n", err)
return
}
defer redis.Close()
// 5. 注册路由
// 6. 启动服务(优雅关机)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
1234567891011121314151617181920212223242526272829303132333435363738394041424344
# # (opens new window) 05.注册路由
# # (opens new window) 5.1 routes/routes.go
package routes
import (
"net/http"
"web_app/logger"
"web_app/settings"
"github.com/gin-gonic/gin"
)
func Setup(mode string) *gin.Engine {
if mode == gin.ReleaseMode {
gin.SetMode(gin.ReleaseMode)
}
r := gin.New()
r.Use(logger.GinLogger(), logger.GinRecovery(true))
r.GET("/version", func(c *gin.Context) {
c.String(http.StatusOK, settings.Conf.Version)
})
return r
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1234567891011121314151617181920
# # (opens new window) 5.2 main.go
package main
import (
"fmt"
"go.uber.org/zap"
"my_web/dao/mysql"
"my_web/dao/redis"
"my_web/logger"
"my_web/routes"
"my_web/settings"
)
func main() {
// 1. 加载配置(viper配置管理)
if err := settings.Init(); err != nil {
fmt.Printf("init settings failed, err:%v\n", err)
return
}
fmt.Println(settings.Conf)
fmt.Println(settings.Conf.Name) // web_app
fmt.Println(settings.Conf.LogConfig == nil)
// 2. 初始化日志(zap日志库)
if err := logger.Init(settings.Conf.LogConfig, settings.Conf.Mode); err != nil {
fmt.Printf("init logger failed, err:%v\n", err)
return
}
defer zap.L().Sync()
zap.L().Debug("logger init success...")
// 3. 初始化MySQL连接(sqlx)
if err := mysql.Init(settings.Conf.MySQLConfig); err != nil {
fmt.Printf("init mysql failed, err:%v\n", err)
return
}
defer mysql.Close()
// 4. 初始化Redis连接(go-redis)
if err := redis.Init(settings.Conf.RedisConfig); err != nil {
fmt.Printf("init redis failed, err:%v\n", err)
return
}
defer redis.Close()
// 5. 注册路由
r := routes.Setup(settings.Conf.Mode)
err := r.Run(fmt.Sprintf(":%d", settings.Conf.Port))
if err != nil {
fmt.Printf("run server failed, err:%v\n", err)
return
}
// 6. 启动服务(优雅关机)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
# # (opens new window) 06.启动服务(优雅关机)
# # (opens new window) 6.1 main.go
package main
import (
"context" // 这个包需要手动的导入
"fmt"
"go.uber.org/zap"
"log"
"my_web/dao/mysql"
"my_web/dao/redis"
"my_web/logger"
"my_web/routes"
"my_web/settings"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
// 1. 加载配置(viper配置管理)
if err := settings.Init(); err != nil {
fmt.Printf("init settings failed, err:%v\n", err)
return
}
fmt.Println(settings.Conf)
fmt.Println(settings.Conf.Name) // web_app
fmt.Println(settings.Conf.LogConfig == nil)
// 2. 初始化日志(zap日志库)
if err := logger.Init(settings.Conf.LogConfig, settings.Conf.Mode); err != nil {
fmt.Printf("init logger failed, err:%v\n", err)
return
}
defer zap.L().Sync()
zap.L().Debug("logger init success...")
// 3. 初始化MySQL连接(sqlx)
if err := mysql.Init(settings.Conf.MySQLConfig); err != nil {
fmt.Printf("init mysql failed, err:%v\n", err)
return
}
defer mysql.Close()
// 4. 初始化Redis连接(go-redis)
if err := redis.Init(settings.Conf.RedisConfig); err != nil {
fmt.Printf("init redis failed, err:%v\n", err)
return
}
defer redis.Close()
// 5. 注册路由
// 5. 注册路由
r := routes.Setup(settings.Conf.Mode)
r.Run()
// 6. 启动服务(优雅关机)
fmt.Println(settings.Conf.Port)
srv := &http.Server{
Addr: fmt.Sprintf(":%d", settings.Conf.Port),
Handler: r,
}
go func() {
// 开启一个goroutine启动服务
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
}
}()
// 等待中断信号来优雅地关闭服务器,为关闭服务器操作设置一个5秒的超时
quit := make(chan os.Signal, 1) // 创建一个接收信号的通道
// kill 默认会发送 syscall.SIGTERM 信号
// kill -2 发送 syscall.SIGINT 信号,我们常用的Ctrl+C就是触发系统SIGINT信号
// kill -9 发送 syscall.SIGKILL 信号,但是不能被捕获,所以不需要添加它
// signal.Notify把收到的 syscall.SIGINT或syscall.SIGTERM 信号转发给quit
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) // 此处不会阻塞
<-quit // 阻塞在此,当接收到上述两种信号时才会往下执行
zap.L().Info("Shutdown Server ...")
// 创建一个5秒超时的context
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 5秒内优雅关闭服务(将未处理完的请求处理完再关闭服务),超过5秒就超时退出
if err := srv.Shutdown(ctx); err != nil {
zap.L().Fatal("Server Shutdown", zap.Error(err))
}
zap.L().Info("Server exiting")
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
# # (opens new window) 6.2 启动测试
http://127.0.0.1:8080/version
修改 config.yaml 中的version配置,保存后就会在页面看到自动修改
# # (opens new window) 6.3 使用golang ide启动
编辑 (opens new window) (opens new window)
上次