1. 开篇
安装
查看是否安装成功
Monster:king chiang$ go version
go version go1.16.5 darwin/amd64
如果有一定的编程基础,如Java,Python,JavaScript等,借助 标准库中文文档 或 go语言中文网 直接上手撸代码,如果是小白建议从基础语法开始搞起。下面直接搞个web项目。
2. 概览
目录结构
.
└── king
├── handler
├── config
│ └── config.go
├── logger
│ └── zap.go
├── mysql
│ └── mysql.go
├── redis
│ └── redis.go
├── router
│ └── router.go
├── wrapper
│ └── wrapper.go
├── application.go
├── config.yaml
├── go.mod
├── LICENSE
└── README.md
为了直观展示,所有配置文件在创建的时候选择平铺,强迫症的小伙伴自行调整~
- application.go:项目启动入口。
- config.yaml:配置文件。
- go.mod:项目依赖,类似
mavenpom文件。 - handler:业务逻辑处理文件夹。
- config:配置读取
.yaml配置文件。 - logger:日志配置。
- mysql:数据库配置。
- redis:缓存配置
- router:路由配置
- wrapper:出入参封装。
项目依赖
module king
go 1.16
require (
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d
github.com/fsnotify/fsnotify v1.4.9
github.com/gin-gonic/gin v1.7.2
github.com/go-redis/redis v6.15.9+incompatible
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect
github.com/jinzhu/gorm v1.9.16
github.com/jonboulle/clockwork v0.2.2 // indirect
github.com/kuangchanglang/graceful v1.0.2
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
github.com/lestrrat-go/strftime v1.0.4 // indirect
github.com/onsi/gomega v1.14.0 // indirect
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 // indirect
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.8.1
go.uber.org/zap v1.17.0
)
- gin:Web容器
- graceful:Gin间接依赖,负责端口监听
- pflag:替换原生
flag - viper:
配置库用于读取配置文件 - fsnotify:文件系统监控工具
- gorm:数据库ORM关系映射
- redis:Redis配置与CURD操作
- zap:日志管理
- govalidator:参数校验
3. 配置
3.1. yaml
config.go 读取配置文件需要符合以下几种需求:
- 无任何启动参数设定默认文件。
- 不同应用环境可以切换不同文件。
- 项目运行时可动态修改配置。
package config
import (
"fmt"
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
)
type Config struct {
Name string
}
// Run 加载配置文件, 并监视配置文件变化
func Run(configName string) error {
config := Config{
Name: configName,
}
if err := config.init(); err != nil {
return err
}
config.watcher()
return nil
}
func (config *Config) init() error {
if config.Name != "" {
viper.SetConfigFile(config.Name)
} else {
// 默认配置文件名称 ./config.yaml
viper.AddConfigPath(".")
viper.SetConfigName("config")
}
viper.SetConfigType("yaml")
// 解析配置文件
err := viper.ReadInConfig()
if err != nil {
panic(fmt.Errorf("Fatal error config file: %s \n", err))
}
return nil
}
// 监听配置变化,使配置动态生效
func (config *Config) watcher() {
viper.WatchConfig()
viper.OnConfigChange(func(in fsnotify.Event) {
fmt.Printf("Config file changed. [%s]", in.Name)
})
}
3.2. zap
日志配置通常可以满足:按日期切割、按类别切割、保存周期等等。Go标准库包含Log相关操作,但在上述需求上还是有所欠缺,所以选择了第三方库替代原生Log,技术选型方面除了 zap 也配置 logrus 但最终也未实现想要的效果,果断放弃~
package logger
import (
"github.com/lestrrat-go/file-rotatelogs"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"io"
"os"
"time"
)
var Log *zap.SugaredLogger
func init() {
// 设置日志格式
config := zapcore.EncoderConfig{
MessageKey: "msg",
LevelKey: "level",
TimeKey: "ts",
CallerKey: "file",
StacktraceKey: "trace",
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.LowercaseLevelEncoder,
EncodeCaller: zapcore.ShortCallerEncoder,
EncodeTime: func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
enc.AppendString(t.Format("2006-01-02 15:04:05"))
},
EncodeDuration: func(d time.Duration, enc zapcore.PrimitiveArrayEncoder) {
enc.AppendInt64(int64(d) / 1000000)
},
}
encoder := zapcore.NewConsoleEncoder(config)
info := zap.LevelEnablerFunc(func(level zapcore.Level) bool {
return level == zapcore.InfoLevel
})
err := zap.LevelEnablerFunc(func(level zapcore.Level) bool {
return level >= zapcore.ErrorLevel
})
infoWriter := writer("./log/info", "%Y%m%d")
errorWriter := writer("./log/error", "%Y%m%d")
core := zapcore.NewTee(
zapcore.NewCore(encoder, zapcore.AddSync(os.Stdout), info),
zapcore.NewCore(encoder, zapcore.AddSync(infoWriter), info),
zapcore.NewCore(encoder, zapcore.AddSync(os.Stdout), err),
zapcore.NewCore(encoder, zapcore.AddSync(errorWriter), err),
)
Log = zap.New(core, zap.AddCaller(), zap.AddStacktrace(zap.ErrorLevel)).Sugar()
}
func writer(filename, fileFormat string) io.Writer {
hook, err := rotatelogs.New(
filename+"_"+fileFormat+".log",
rotatelogs.WithLinkName(filename+".log"),
// 保存天数
rotatelogs.WithMaxAge(time.Hour*24*30),
// 切割频率24小时
rotatelogs.WithRotationTime(time.Hour*24),
)
if err != nil {
panic(err)
}
return hook
}
3.3. mysql
数据库配置也是老生常谈,像用户名、密码、IP、端口... 其中 db.LogMode 为是否启用Logger,显示详细日志,true / false 来配置,其他的都能看懂~
package mysql
import (
"fmt"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
"github.com/spf13/viper"
"sync"
)
var once sync.Once
var db *gorm.DB
// Get 获取MySQL连接实例
func Get() *gorm.DB {
once.Do(func() {
db = initialize()
})
return db
}
func initialize() *gorm.DB {
config := fmt.Sprintf("%s:%s@tcp(%s%s)/%s?charset=%s",
viper.GetString("mysql.username"),
viper.GetString("mysql.password"),
viper.GetString("mysql.host"),
viper.GetString("mysql.port"),
viper.GetString("mysql.name"),
viper.GetString("mysql.charset"))
db, err := gorm.Open("mysql", config)
if err != nil {
panic(err)
}
db.DB().SetMaxIdleConns(viper.GetInt("mysql.max-idle-connections"))
db.DB().SetMaxOpenConns(viper.GetInt("mysql.max-open-connections"))
db.LogMode(viper.GetBool("mysql.log-mode"))
return db
}
// Close 关闭MySQL连接
func Close(db *gorm.DB) {
if err := db.Close(); err != nil {
panic(err)
}
}
3.4. redis
Redis单机配置更加简单,IP、端口、密码、db。集群使用 redis.NewClusterClient(...)相关资料很多~
package redis
import (
"fmt"
"github.com/go-redis/redis"
"github.com/spf13/viper"
)
var Client *redis.Client
// Initialize 初始化Redis客户端连接
func Initialize() {
Client = redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s%s",
viper.GetString("redis.host"),
viper.GetString("redis.port")),
Password: viper.GetString("redis.password"),
DB: viper.GetInt("redis.db"),
})
_, err := Client.Ping().Result()
if err != nil {
panic("can not link redis server")
}
}
3.5. wrapper
为了保障web系统出参统一,避免抛出异常导致上下游系统解析失败,并且系统自身也可以快速定位问题,通常会对参数进行统一封装。
package wrapper
import (
"github.com/gin-gonic/gin"
"net/http"
)
type Response struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
}
// gin web出参封装
func wrap(context *gin.Context, code int, message string, data interface{}) {
context.JSON(http.StatusOK, Response{
Code: code,
Message: message,
Data: data,
})
}
func Custom(context *gin.Context, code int, message string) {
wrap(context, code, message, nil)
}
// Default 默认出参
func Default(context *gin.Context) {
wrap(context, http.StatusOK, "success", nil)
}
// Success 执行成功出参
func Success(context *gin.Context, data interface{}) {
wrap(context, http.StatusOK, "success", data)
}
// Error 执行异常出参
func Error(context *gin.Context, message string) {
wrap(context, http.StatusInternalServerError, message, nil)
}
3.6. router
- 对
404405500等异常进行拦截,输出标准格式。 - 对业务方法入口进行装载,类似
Spring的Controller
package router
import (
"github.com/gin-gonic/gin"
"king/handler"
"king/wrapper"
"net/http"
)
// Load 加载所有路由信息
func Load(engine *gin.Engine) {
engine.Use(serverErrorRecover)
// 404
engine.NoRoute(func(context *gin.Context) {
context.String(http.StatusNotFound, "404")
})
// 405
engine.NoMethod(func(context *gin.Context) {
context.String(http.StatusMethodNotAllowed, "405")
})
engine.GET("/", wrapper.Default)
// 业务逻辑分组路由
handler.Group(engine)
}
// 服务端异常出参
func serverErrorRecover(context *gin.Context) {
defer func() {
if r := recover(); r != nil {
context.JSON(http.StatusOK, wrapper.Response{
Code: 999,
Message: "Server error",
Data: nil,
})
// 终止后续接口调用
context.Abort()
}
}()
// defer recover后接口继续
context.Next()
}
4. 启动类与配置文件
application.go
首先使配置文件生效,然后配置MySQL、Redis、Log,最后启动Web容器,并监听端口。启动之后可以做健康检查,系统崩溃警报等等操作,简单的Golang Web结束~
package main
import (
"github.com/gin-gonic/gin"
"github.com/kuangchanglang/graceful"
"github.com/spf13/pflag"
"github.com/spf13/viper"
"king/config"
"king/logger"
"king/mysql"
"king/router"
"net/http"
)
var (
conf = pflag.StringP("config", "c", "", "config filepath")
log = logger.Log
)
func main() {
// 命令行参数解析为对应变量的值
pflag.Parse()
// 加载配置文件
if err := config.Run(*conf); err != nil {
panic(err)
}
// 获取MySQL实例
db := mysql.Get()
// 异常时关闭
defer mysql.Close(db)
// 初始化Redis连接
//redis.Initialize()
// gin运行模式
gin.SetMode(viper.GetString("gin.mode"))
engine := gin.Default()
// 加载路由信息
router.Load(engine)
log.Infof("http server port %s", viper.GetString("gin.address"))
// 监听端口并启动服务端
if err := graceful.ListenAndServe(viper.GetString("gin.address"), engine); err != nil && err != http.ErrServerClosed {
log.Errorf("http server fail to start: %s", err)
}
}
配置文件
name: king
mysql:
name: king
host: 127.0.0.1
port: :3306
username: root
password: tinychiang
charset: utf8mb4
max-idle-connections: 10
max-open-connections: 2
log-mode: true
redis:
host: 127.0.0.1
port: :6379
password:
db: 0
gin:
mode: debug #release, debug
address: :8000
Gitee:gitee.com/tinychiang/…