Golang Web - 实战篇

1,669 阅读5分钟

1. 开篇

安装

studygolang.com/dl

  查看是否安装成功

Monster:king chiang$ go version
go version go1.16.5 darwin/amd64

  如果有一定的编程基础,如Java,Python,JavaScript等,借助 标准库中文文档go语言中文网 直接上手撸代码,如果是小白建议从基础语法开始搞起。下面直接搞个web项目。

studygolang.com/

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:项目依赖,类似 maven pom 文件。
  • 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 读取配置文件需要符合以下几种需求:

  1. 无任何启动参数设定默认文件。
  2. 不同应用环境可以切换不同文件。
  3. 项目运行时可动态修改配置。
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

  1. 404 405 500等异常进行拦截,输出标准格式。
  2. 对业务方法入口进行装载,类似 SpringController
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/…