08.web脚手架
# 00.项目结构
# 0.1 新建项目
# 0.2 项目结构图
CLD + 模型
- controller
- logic
- dao
- models
日志、路由、三方库
- logger
- routes
- pkg
main.go、配置文件
- main.go
- conf
- settings
# 0.3 初始化main.go
func main() {
// 1. 加载配置(viper配置管理)
// 2. 初始化日志(zap日志库)
// 3. 初始化MySQL连接(sqlx)
// 4. 初始化Redis连接(go-redis)
// 5. 注册路由
// 6. 启动服务(优雅关机)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
# 01.第一: 加载配置
# 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
1
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
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
# 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
}
1
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
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
# 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. 启动服务(优雅关机)
}
1
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
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
# 02.第二: 初始化日志
# 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()
}
}
1
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
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
# 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. 启动服务(优雅关机)
}
1
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
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
# 03.第三: 初始化mysql连接
# 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()
}
1
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
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
# 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. 启动服务(优雅关机)
}
1
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
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
# 04.初始化redis连接
# 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()
}
1
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
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
# 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. 启动服务(优雅关机)
}
1
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
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
# 05.注册路由
# 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
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 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. 启动服务(优雅关机)
}
1
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
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
# 06.启动服务(优雅关机)
# 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")
}
1
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
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
# 6.2 启动测试
- http://127.0.0.1:8080/version
- 修改 config.yaml 中的version配置,保存后就会在页面看到自动修改
# 6.3 使用golang ide启动
上次更新: 2024/3/13 15:35:10