本文正在参与「金石计划」

flag:每月至少产出三篇高质量文章~

在之前现已依据 React18+TS4.x+Webpack5 从0到1建立了一个 React 根本项目架子,详细的过程见下面三篇:

  • 【脚手架】从0到1建立React18+TS4.x+Webpack5项目(一)项目初始化
  • 【脚手架】从0到1建立React18+TS4.x+Webpack5项目(二)根底功用装备
  • 【脚手架】从0到1建立React18+TS4.x+Webpack5项目(三)代码质量和git提交标准
  • 【脚手架】从0到1建立React18+TS4.x+Webpack5项目(四)发布脚手架

接下来,我将用几篇文章介绍怎么依据 Go 言语建立一个后端的根底架子。然后前后端同步进行开发,后端服务依据 Gin + Gorm + Casbin前端则是依据 React + antd,开发一套常见的依据 RBAC 权限控制的前后端分离的全栈办理后台项目,手把手带你入门前后端开发。第一篇:

  • 【Go】依据 Gin 从0到1建立 Web 办理后台体系后端服务(一)项目初始化、装备和日志
  • 【Go】依据 Gin 从0到1建立 Web 办理后台体系后端服务(二)衔接数据库
  • 【Go】依据 Gin 从0到1建立 Web 办理后台体系后端服务(三)路由、自界说校验器和 Redis

1、项目初始化

1.1 目录结构

├── api
|   ├── v1 # v1版本接口服务
|       ├── system # 体系级服务
|       └── enter.go # 一致进口
├── config # 装备相关
├── core # 中心模块
├── dao # dao层
├── global # 全局变量
├── initialize # 装备发动初始化
├── middleware # 中间件
├── model # 数据库结构体
├── router # 路由
├── service # 业务层
├── utils # 工具函数
├── config.yaml # 装备文件
├── go.mod # 包办理
├── main.go # 项目发动文件
└── README.md # 项目README

【Go】基于 Gin 从0到1搭建 Web 管理后台系统后端服务(一)项目初始化、配置和日志

1.2 go.mod

运用以下命令初始化:

mkdir ewa_admin_server
cd ewa_admin_server

# init go.mod
go mod init ewa_admin_server

就会在根目录下生成一个包办理装备文件 go.mod

一般来说,根本的后端服务都需求包括装备解析、日志、数据库衔接等流程,新建进口文件 main.go

package main
import "fmt"
func main() {
   fmt.Println("hello world")
   // TODO:1.装备初始化
   // TODO:2.日志
   // TODO:3.数据库衔接
   // TODO:4.其他初始化
   // TODO:5.发动服务
}

1.3 引进 gin

装置依靠

go get -u github.com/gin-gonic/gin

接着,咱们能够试试用gin开启一个服务:

package main
import (
   "net/http"
   "github.com/gin-gonic/gin"
)
func main() {
   // TODO:1.装备初始化
   // TODO:2.日志
   // TODO:3.数据库衔接
   // TODO:4.其他初始化
   // TODO:5.发动服务
   r := gin.Default()
   // 测验路由
   r.GET("/ping", func(c *gin.Context) {
      c.String(http.StatusOK, "pong")
   })
   // 发动服务器
   r.Run(":8080")
}

发动服务:

go run main.go

然后在浏览器中输入 http://127.0.0.1:8080/ping,就会在页面回来一个 pong

【Go】基于 Gin 从0到1搭建 Web 管理后台系统后端服务(一)项目初始化、配置和日志

2、装备初始化 & 全局变量

2.1 viper简介

装备文件是每个项目必不可少的部分,用来保存运用根本数据、数据库装备等信息,避免要修正一个装备项需求处处找的为难。这儿我运用 viper 作为装备办理计划。

ViperGo 言语中一个十分盛行的装备办理库,它能够协助程序员在运用程序中加载和解析各种装备格局,如 JSON、YAML、TOML、INI 等。Viper 库供给了一个简单的接口,允许开发人员经过各种办法来拜访和办理装备。

下面是 Viper 库的一些主要特点:

  • 设置默许值
  • JSONTOMLYAMLHCLenvfileJava properties 格局的装备文件读取装备信息
  • 实时监控和从头读取装备文件(可选)
  • 从环境变量中读取
  • 从长途装备体系(etcd或Consul)读取并监控装备变化
  • 从命令行参数读取装备
  • 从buffer读取装备
  • 显式装备值

2.2 viper 根本运用

先装置依靠:

go get -u github.com/spf13/viper

然后在项目根目录下的 config.yaml 文件中增加根本的装备信息:

app: # 根本装备信息
  env: local # 环境
  port: 8889 # 服务监听端口
  app_name: ewa_admin_server # 运用称号
  app_url: http://localhost # 运用域名
  db_type: mysql # 运用的数据库

在项目根目录下新建文件夹 config,用于寄存一切装备对应的结构体,新建 config.go 文件,界说 Configuration 结构体,其 App 特点对应 config.yaml 中的 app

package config
type Configuration struct {
    App App `mapstructure:"app" json:"app" yaml:"app"`
}

新建 app.go 文件,界说 App 结构体,其一切特点别离对应 config.yaml 中 app 下的一切装备

package config
type App struct {
   Env     string `mapstructure:"env" json:"env" yaml:"env"`
   Port    int    `mapstructure:"port" json:"port" yaml:"port"`
   AppName string `mapstructure:"app_name" json:"app_name" yaml:"app_name"`
   AppUrl  string `mapstructure:"app_url" json:"app_url" yaml:"app_url"`
   DbType  string `mapstructure:"db_type" json:"db_type" yaml:"db_type"`
}

留意:装备结构体中 mapstructure 标签需对应 config.ymal 中的装备称号, viper 会依据标签 value 值把 config.yaml 的数据赋予给结构体

2.3 将装备放入全局变量

为什么要将装备信息放入全局变量中?

在Go言语中,将装备信息存储在全局变量中是一种常见的做法,这主要是因为全局变量的值能够在整个程序中拜访和共享,因而在某些情况下能够方便地进行装备办理和运用。

下面是一些常见的场景,或许会运用全局变量来存储装备信息:

  1. 在运用程序的不同模块中需求运用相同的装备信息时,运用全局变量能够方便地实现这一点。例如,在一个Web运用程序中,或许需求在多个处理程序中运用数据库的衔接信息,这时将衔接信息存储在全局变量中能够方便地在各个处理程序中运用。

  2. 在需求频频拜访装备信息的场景中,运用全局变量能够避免重复读取装备文件或重复创立装备目标的开支,进步程序的功用和功率。

不过,运用全局变量也或许会带来一些潜在的问题,比方:

  1. 全局变量的值能够在整个程序中被修正,这或许会导致意外的行为和过错。
  2. 全局变量或许会使程序的依靠关系更加复杂和难以办理,因为它们能够被程序中的任何模块拜访和修正。

因而,在运用全局变量存储装备信息时,应该仔细考虑其对程序的影响,并保证采取恰当的措施来削减潜在的问题。例如,能够运用只读全局变量或运用锁来保护全局变量的拜访。此外,也能够考虑运用依靠注入等技能来办理程序中的装备信息。

下面咱们在 global 中创立一个 global.go 文件来会集寄存全局变量:

package global
import (
   "ewa_admin_server/config"
   "github.com/spf13/viper"
)
var (
   EWA_CONFIG config.Configuration
   EWA_VIPER  *viper.Viper
)

考虑实践工作中多环境开发、测验的场景,咱们需求针对不同的环境运用不同的装备,在core中参加一个internal文件,增加一个constants.go,写入:

package internal
const (
   ConfigEnv         = "EWA_CONFIG"
   ConfigDefaultFile = "config.yaml"
   ConfigTestFile    = "config.test.yaml"
   ConfigDebugFile   = "config.debug.yaml"
   ConfigReleaseFile = "config.release.yaml"
)

然后在 core 中新建 viper.go,编写装备初始化办法:

package core
import (
   "ewa_admin_server/core/internal"
   "ewa_admin_server/global"
   "flag"
   "fmt"
   "os"
   "github.com/fsnotify/fsnotify"
   "github.com/gin-gonic/gin"
   "github.com/spf13/viper"
)
// InitializeViper 优先级: 命令行 > 环境变量 > 默许值
func InitializeViper(path ...string) *viper.Viper {
   var config string
   if len(path) == 0 {
      // 界说命令行flag参数,格局:flag.TypeVar(Type指针, flag名, 默许值, 协助信息)
      flag.StringVar(&config, "c", "", "choose config file.")
      // 界说好命令行flag参数后,需求经过调用flag.Parse()来对命令行参数进行解析。
      flag.Parse()
      // 判别命令行参数是否为空
      if config == "" {
         /*
            判别 internal.ConfigEnv 常量存储的环境变量是否为空
            比方咱们发动项目的时分,履行:GVA_CONFIG=config.yaml go run main.go
            这时分 os.Getenv(internal.ConfigEnv) 得到的便是 config.yaml
            当然,也能够经过 os.Setenv(internal.ConfigEnv, "config.yaml") 在初始化之前设置
         */
         if configEnv := os.Getenv(internal.ConfigEnv); configEnv == "" {
            switch gin.Mode() {
            case gin.DebugMode:
               config = internal.ConfigDefaultFile
               fmt.Printf("您正在运用gin形式的%s环境称号,config的途径为%s\n", gin.EnvGinMode, internal.ConfigDefaultFile)
            case gin.ReleaseMode:
               config = internal.ConfigReleaseFile
               fmt.Printf("您正在运用gin形式的%s环境称号,config的途径为%s\n", gin.EnvGinMode, internal.ConfigReleaseFile)
            case gin.TestMode:
               config = internal.ConfigTestFile
               fmt.Printf("您正在运用gin形式的%s环境称号,config的途径为%s\n", gin.EnvGinMode, internal.ConfigTestFile)
            }
         } else {
            // internal.ConfigEnv 常量存储的环境变量不为空 将值赋值于config
            config = configEnv
            fmt.Printf("您正在运用%s环境变量,config的途径为%s\n", internal.ConfigEnv, config)
         }
      } else {
         // 命令行参数不为空 将值赋值于config
         fmt.Printf("您正在运用命令行的-c参数传递的值,config的途径为%s\n", config)
      }
   } else {
      // 函数传递的可变参数的第一个值赋值于config
      config = path[0]
      fmt.Printf("您正在运用func Viper()传递的值,config的途径为%s\n", config)
   }
   vip := viper.New()
   vip.SetConfigFile(config)
   vip.SetConfigType("yaml")
   err := vip.ReadInConfig()
   if err != nil {
      panic(fmt.Errorf("Fatal error config file: %s \n", err))
   }
   vip.WatchConfig()
   vip.OnConfigChange(func(e fsnotify.Event) {
      fmt.Println("config file changed:", e.Name)
      if err = vip.Unmarshal(&global.EWA_CONFIG); err != nil {
         fmt.Println(err)
      }
   })
   if err = vip.Unmarshal(&global.EWA_CONFIG); err != nil {
      fmt.Println(err)
   }
   fmt.Println("====1-viper====: viper init config success")
   return vip
}

从头发动项目,就会在控制台打印:

====1-viper====: viper init config success
====app_name====:  ewa_admin_server

这儿面涉及到几个知识点:

2.3.1 命令行 flag

Go 供给了一个flag包,支撑根本的命令行标志解析,请看下面的示例:

package main
import (
    "flag"
    "fmt"
)
func main() {
    wordPtr := flag.String("word", "foo", "a string")
    numbPtr := flag.Int("numb", 42, "an int")
    forkPtr := flag.Bool("fork", false, "a bool")
    var svar string
    flag.StringVar(&svar, "svar", "bar", "a string var")
    flag.Parse()
    fmt.Println("word:", *wordPtr)
    fmt.Println("numb:", *numbPtr)
    fmt.Println("fork:", *forkPtr)
    fmt.Println("svar:", svar)
    fmt.Println("tail:", flag.Args())
}

运用:

$ go build command-line-flags.go

$ ./command-line-flags -word=opt -numb=7 -fork -svar=flag 
word: opt 
numb: 7 
fork: true 
svar: flag 
tail: []

$ ./command-line-flags -word=opt
word: opt
numb: 42
fork: false
svar: bar
tail: []

$ ./command-line-flags -word=opt a1 a2 a3
word: opt
...
tail: [a1 a2 a3]

$ ./command-line-flags -word=opt a1 a2 a3 -numb=7
word: opt
numb: 42
fork: false
svar: bar
tail: [a1 a2 a3 -numb=7]

$ ./command-line-flags -h
Usage of ./command-line-flags:
  -fork=false: a bool
  -numb=42: an int
  -svar="bar": a string var
  -word="foo": a string
  
$ ./command-line-flags -wat
flag provided but not defined: -wat
Usage of ./command-line-flags:
...

2.3.2 os.Setenv() & os.Getenv()

package main
import (
    "fmt"
    "os"
    "strings"
)
func main() {
    os.Setenv("FOO", "1")
    fmt.Println("FOO:", os.Getenv("FOO"))
    fmt.Println("BAR:", os.Getenv("BAR"))
    fmt.Println()
    for _, e := range os.Environ() {
        pair := strings.SplitN(e, "=", 2)
        fmt.Println(pair[0])
    }
}

运用:

$ go run environment-variables.go
FOO: 1
BAR:
TERM_PROGRAM
PATH
SHELL

$ BAR=2 go run environment-variables.go
FOO: 1
BAR: 2
...

2.3.3 gin.Mode()

在初始化本路由的时分运用,从源码可看出,经过给变量ginMode赋值的方式供给了三种形式:

  • DebugMode
  • ReleaseMode
  • TestMode

DebugModeReleaseMode多了一些额定的过错信息,出产环境不需求这些信息。而TestModegin用于自己的单元测验,用来快速开关DebugMode。对其它开发者没什么意义。能够经过gin.SetMode(AppMode)来设置mode。

需求留意的是:SetMode() 应该声明在 gin.New() 前,否则装备无法更新:

【Go】基于 Gin 从0到1搭建 Web 管理后台系统后端服务(一)项目初始化、配置和日志

关于viper的运用,最好是看官方文档,也能够看看下面几篇不错的文章:

  • Go 每日一库之viper – 大俊的博客
  • Go言语装备办理神器——Viper中文教程 – 李文周的博客
  • Go中运用viper来办理装备文件 –

2.4 运用装备

现在咱们现已将装备解析到了全局变量中,就能够将其运用到服务发动逻辑中了,在 core 中新建 server.go 文件,然后将服务发动的办法写在这儿:

package core
import (
   "ewa_admin_server/global"
   "fmt"
   "net/http"
   "time"
   "github.com/fvbock/endless"
   "github.com/gin-gonic/gin"
)
type server interface {
   ListenAndServe() error
}
func RunServer() {
   r := gin.Default()
   r.GET("/ping", func(c *gin.Context) {
      c.String(http.StatusOK, "pong")
   })
   address := fmt.Sprintf(":%d", global.EWA_CONFIG.App.Port)
   s := initServer(address, r)
   // 保证文本顺序输出
   time.Sleep(10 * time.Microsecond)
   fmt.Println(`address`, address)
   s.ListenAndServe()
}
func initServer(address string, router *gin.Engine) server {
   // 运用endless库创立一个HTTP服务器,其中address是服务器的监听地址(如:8080),router是HTTP恳求路由器。
   s := endless.NewServer(address, router)
   // 设置HTTP恳求头的读取超时时刻为20秒,如果在20秒内未读取到恳求头,则会回来一个超时过错。
   s.ReadHeaderTimeout = 20 * time.Second
   // 设置HTTP呼应体的写入超时时刻为20秒,如果在20秒内未将呼应体写入完成,则会回来一个超时过错。
   s.WriteTimeout = 20 * time.Second
   // 设置HTTP恳求头的最大字节数为1MB。如果恳求头超越1MB,则会回来一个过错。
   s.MaxHeaderBytes = 1 << 20
   return s
}

运用 endless 的作用是实现无缝重载和高雅封闭 HTTP 服务器。自带高雅地重启或中止你的Web服务器,咱们能够运用fvbock/endless来替换默许的ListenAndServe,有关详细信息,请参阅问题#296。

endless 是一个能够用于从头加载和高雅封闭HTTP服务器的库。它能够在运转时更新服务器代码而无需中止正在运转的HTTP服务器。这使得服务器能够在出产环境下无缝地进行更新和保护,一起不影响当时正在运转的恳求和衔接。

运用 endless,能够在代码修正后,经过发送信号量通知服务器进行重载,新的代码会被加载并运转,旧的衔接会继续服务,新的衔接将运用新的代码进行处理。当需求封闭服务器时,endless 会等待一切当时处理的恳求完成后再封闭服务器,这样能够保证一切恳求都能得处处理,避免数据丢掉和用户体会下降。

Gin 中运用 endless 能够进步服务器的可靠性和稳定性,一起也能进步开发功率,削减服务器保护和更新的停机时刻。

这些装备能够协助咱们优化HTTP服务器的功用和安全性。经过设置超时时刻和最大字节数等参数,能够避免一些潜在的安全问题和功用问题。

例如,设置超时时刻能够避免客户端故意坚持衔接而导致的资源糟蹋,设置最大字节数能够避免客户端发送过大的恳求头而导致的资源糟蹋和安全问题。

重启项目,拜访 http://127.0.0.1:8889/ping,依然能在页面看到 pong,就说明咱们的初始化装备成功了。

【Go】基于 Gin 从0到1搭建 Web 管理后台系统后端服务(一)项目初始化、配置和日志

3、日志初始化

为什么需求记载日志?

在Go言语中记载日志是一个十分常见的行为。以下是几个原因:

  1. 问题排查:在运用程序出现毛病或过错时,日志记载能够协助咱们定位问题所在。咱们能够经过检查日志记载,了解运用程序在何时、何处出现问题,从而更快地进行毛病排查。
  2. 功用剖析:日志记载也能够用于剖析运用程序的功用。咱们能够在代码中记载运用程序履行过程中的要害事件和时刻戳,然后运用这些信息来剖析和优化运用程序的功用。
  3. 安全监控:日志记载还能够用于监控运用程序的安全性。咱们能够记载一切的拜访恳求和呼应,以便在运用程序遭到进犯时能够快速呼应。
  4. 后续剖析:日志记载还能够用于后续剖析和审计。咱们能够将日志记载存储在外部数据库中,以便在未来需求时能够查询和剖析。

值得留意的是,日志记载是一个十分重要的行为,能够协助咱们快速排查问题、优化功用、监控安全性以及进行后续剖析和审计。因而,在Go言语中记载日志是一个必要的过程。

这儿将运用 zap 作为日志库,一般来说,日志都是需求写入到文件保存的,这也是 zap 唯一缺少的部分,所以我将结合 lumberjack 来运用,实现日志切开归档的功用。运用Zap作为日志记载器,有以下几个原因:

  1. 高功用:Zap是一个十分高功用的日志库,其功用比其他日志库(如logrus和zap)更高。这主要是因为Zap运用了零内存分配的技能,能够在不影响功用的情况下削减废物收回。
  2. 丰厚的功用:Zap支撑多种日志等级、多种输出格局(如JSON、Console、Logfmt等)、多种输出位置(如控制台、文件、网络等),还支撑自界说日志字段和Hook。
  3. 安全性:Zap采用了严厉的日志记载战略,保证日志安全性。它能够避免因为多协程写入日志导致的竞态条件,还能够自动切分日志文件,避免单个日志文件过大导致的功用问题和磁盘空间糟蹋。
  4. 社区支撑:Zap是由Uber开发并保护的开源日志库,其开发和保护的活跃度和社区支撑度都十分高。因而,Zap能够得到广泛的支撑和运用,并有大量的第三方库和结构与之兼容。

而Lumberjack是一个用于高功用、轮转和翻滚日志文件的库。在Zap中运用Lumberjack有以下几个优点:

  1. 高功用:Lumberjack运用了缓冲区和异步写入等技能,能够削减IO的次数,进步写入日志的功用。
  2. 轮转和翻滚:Lumberjack能够自动进行日志文件的轮转和翻滚,能够避免单个日志文件变得过大而影响功用,也能够方便地办理和备份前史日志。
  3. 稳定性和可靠性:Lumberjack能够保证即便在写入日志的一起发生了毛病,也不会丢掉日志数据。此外,Lumberjack还能够处理文件体系的过错和日志文件权限等问题。

在Zap中运用Lumberjack能够进步日志的功用、稳定性和可靠性,并且方便办理和备份前史日志。

装置zap:

go get -u go.uber.org/zap
go get -u gopkg.in/natefinch/lumberjack.v2

3.1 界说装备项

config.yarml 中新增日志装备:

# ...
zap: # 日志装备
  level: info # 日志等级
  prefix: '[east_white_common_admin/server]' # 日志前缀
  format: console # 输出
  director: log # 日志寄存的文件
  encode_level: LowercaseColorLevelEncoder # 编码等级
  stacktrace_key: stacktrace # 栈名
  max_age: 0 # 日志留存时刻
  show_line: true # 显示行
  log_in_console: true # 输出控制台

然后新建 config/zap.go,在文件中增加对应的结构体和日志等级转换办法:

package config
import (
   "strings"
   "go.uber.org/zap/zapcore"
)
type Zap struct {
   Level         string `mapstructure:"level" json:"level" yaml:"level"`                            // 等级
   Prefix        string `mapstructure:"prefix" json:"prefix" yaml:"prefix"`                         // 日志前缀
   Format        string `mapstructure:"format" json:"format" yaml:"format"`                         // 输出
   Director      string `mapstructure:"director" json:"director"  yaml:"director"`                  // 日志文件夹
   EncodeLevel   string `mapstructure:"encode_level" json:"encode_level" yaml:"encode_level"`       // 编码级
   StacktraceKey string `mapstructure:"stacktrace_key" json:"stacktrace_key" yaml:"stacktrace_key"` // 栈名
   MaxAge       int  `mapstructure:"max_age" json:"max_age" yaml:"max_age"`                      // 日志留存时刻
   ShowLine     bool `mapstructure:"show_line" json:"show_line" yaml:"show_line"`                // 显示行
   LogInConsole bool `mapstructure:"log_in_console" json:"log_in_console" yaml:"log_in_console"` // 输出控制台
}
// ZapEncodeLevel 依据 EncodeLevel 回来 zapcore.LevelEncoder
func (z *Zap) ZapEncodeLevel() zapcore.LevelEncoder {
   switch {
   case z.EncodeLevel == "LowercaseLevelEncoder": // 小写编码器(默许)
      return zapcore.LowercaseLevelEncoder
   case z.EncodeLevel == "LowercaseColorLevelEncoder": // 小写编码器带颜色
      return zapcore.LowercaseColorLevelEncoder
   case z.EncodeLevel == "CapitalLevelEncoder": // 大写编码器
      return zapcore.CapitalLevelEncoder
   case z.EncodeLevel == "CapitalColorLevelEncoder": // 大写编码器带颜色
      return zapcore.CapitalColorLevelEncoder
   default:
      return zapcore.LowercaseLevelEncoder
   }
}
// TransportLevel 依据字符串转化为 zapcore.Level
func (z *Zap) TransportLevel() zapcore.Level {
   z.Level = strings.ToLower(z.Level)
   switch z.Level {
   case "debug":
      return zapcore.DebugLevel
   case "info":
      return zapcore.InfoLevel
   case "warn":
      return zapcore.WarnLevel
   case "error":
      return zapcore.WarnLevel
   case "dpanic":
      return zapcore.DPanicLevel
   case "panic":
      return zapcore.PanicLevel
   case "fatal":
      return zapcore.FatalLevel
   default:
      return zapcore.DebugLevel
   }
}

别忘了在 config.go 中参加 zap

package config
type Configuration struct {
   App App `mapstructure:"app" json:"app" yaml:"app"`
   Zap Zap `mapstructure:"zap" json:"zap" yaml:"zap"`
}

3.2 日志初始化办法

接下来便是写日志初始化办法了,在 core 中新建 zap.go

package core
import (
   "ewa_admin_server/core/internal"
   "ewa_admin_server/global"
   "ewa_admin_server/utils"
   "fmt"
   "os"
   "go.uber.org/zap"
   "go.uber.org/zap/zapcore"
)
// InitializeZap Zap 获取 zap.Logger
func InitializeZap() (logger *zap.Logger) {
   if ok, _ := utils.PathExists(global.EWA_CONFIG.Zap.Director); !ok { // 判别是否有Director文件夹
      fmt.Printf("create %v directory\n", global.EWA_CONFIG.Zap.Director)
      _ = os.Mkdir(global.EWA_CONFIG.Zap.Director, os.ModePerm)
   }
   cores := internal.Zap.GetZapCores()
   logger = zap.New(zapcore.NewTee(cores...))
   if global.EWA_CONFIG.Zap.ShowLine {
      logger = logger.WithOptions(zap.AddCaller())
   }
   fmt.Println("====2-zap====: zap log init success")
   return logger
}

core/internal 中新建 zap.go

package internal
import (
   "ewa_admin_server/global"
   "fmt"
   "time"
   "go.uber.org/zap"
   "go.uber.org/zap/zapcore"
)
var Zap = new(_zap)
type _zap struct{}
// GetEncoder 获取 zapcore.Encoder
func (z *_zap) GetEncoder() zapcore.Encoder {
   if global.EWA_CONFIG.Zap.Format == "json" {
      return zapcore.NewJSONEncoder(z.GetEncoderConfig())
   }
   return zapcore.NewConsoleEncoder(z.GetEncoderConfig())
}
// GetEncoderConfig 获取zapcore.EncoderConfig
func (z *_zap) GetEncoderConfig() zapcore.EncoderConfig {
   return zapcore.EncoderConfig{
      MessageKey:     "message",
      LevelKey:       "level",
      TimeKey:        "time",
      NameKey:        "logger",
      CallerKey:      "caller",
      StacktraceKey:  global.EWA_CONFIG.Zap.StacktraceKey,
      LineEnding:     zapcore.DefaultLineEnding,
      EncodeLevel:    global.EWA_CONFIG.Zap.ZapEncodeLevel(),
      EncodeTime:     z.CustomTimeEncoder,
      EncodeDuration: zapcore.SecondsDurationEncoder,
      EncodeCaller:   zapcore.FullCallerEncoder,
   }
}
// GetEncoderCore 获取Encoder的 zapcore.Core
func (z *_zap) GetEncoderCore(l zapcore.Level, level zap.LevelEnablerFunc) zapcore.Core {
   writer, err := FileRotateLogs.GetWriteSyncer(l.String()) // 运用file-rotatelogs进行日志分割
   if err != nil {
      fmt.Printf("Get Write Syncer Failed err:%v", err.Error())
      return nil
   }
   return zapcore.NewCore(z.GetEncoder(), writer, level)
}
// CustomTimeEncoder 自界说日志输出时刻格局
func (z *_zap) CustomTimeEncoder(t time.Time, encoder zapcore.PrimitiveArrayEncoder) {
   encoder.AppendString(global.EWA_CONFIG.Zap.Prefix + t.Format("2006/01/02 - 15:04:05.000"))
}
// GetZapCores 依据装备文件的Level获取 []zapcore.Core
func (z *_zap) GetZapCores() []zapcore.Core {
   cores := make([]zapcore.Core, 0, 7)
   for level := global.EWA_CONFIG.Zap.TransportLevel(); level <= zapcore.FatalLevel; level++ {
      cores = append(cores, z.GetEncoderCore(level, z.GetLevelPriority(level)))
   }
   return cores
}
// GetLevelPriority 依据 zapcore.Level 获取 zap.LevelEnablerFunc
func (z *_zap) GetLevelPriority(level zapcore.Level) zap.LevelEnablerFunc {
   switch level {
   case zapcore.DebugLevel:
      return func(level zapcore.Level) bool { // 调试等级
         return level == zap.DebugLevel
      }
   case zapcore.InfoLevel:
      return func(level zapcore.Level) bool { // 日志等级
         return level == zap.InfoLevel
      }
   case zapcore.WarnLevel:
      return func(level zapcore.Level) bool { // 警告等级
         return level == zap.WarnLevel
      }
   case zapcore.ErrorLevel:
      return func(level zapcore.Level) bool { // 过错等级
         return level == zap.ErrorLevel
      }
   case zapcore.DPanicLevel:
      return func(level zapcore.Level) bool { // dpanic等级
         return level == zap.DPanicLevel
      }
   case zapcore.PanicLevel:
      return func(level zapcore.Level) bool { // panic等级
         return level == zap.PanicLevel
      }
   case zapcore.FatalLevel:
      return func(level zapcore.Level) bool { // 停止等级
         return level == zap.FatalLevel
      }
   default:
      return func(level zapcore.Level) bool { // 调试等级
         return level == zap.DebugLevel
      }
   }
}

以及 file_rotate_logs.go

package internal
import (
   "ewa_admin_server/global"
   "os"
   "path"
   "time"
   rotatelogs "github.com/lestrrat-go/file-rotatelogs"
   "go.uber.org/zap/zapcore"
)
var FileRotateLogs = new(fileRotateLogs)
type fileRotateLogs struct{}
// GetWriteSyncer 获取 zapcore.WriteSyncer
func (r *fileRotateLogs) GetWriteSyncer(level string) (zapcore.WriteSyncer, error) {
   fileWriter, err := rotatelogs.New(
      path.Join(global.EWA_CONFIG.Zap.Director, "%Y-%m-%d", level+".log"),
      rotatelogs.WithClock(rotatelogs.Local),
      rotatelogs.WithMaxAge(time.Duration(global.EWA_CONFIG.Zap.MaxAge)*24*time.Hour), // 日志留存时刻
      rotatelogs.WithRotationTime(time.Hour*24),
   )
   if global.EWA_CONFIG.Zap.LogInConsole {
      return zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout), zapcore.AddSync(fileWriter)), err
   }
   return zapcore.AddSync(fileWriter), err
}

新建 utils/directory.go 文件,编写 PathExists 函数,用于判别途径是否存在:

package utils
import (
   "errors"
   "os"
)
//@function: PathExists
//@description: 文件目录是否存在
//@param: path string
//@return: bool, error
func PathExists(path string) (bool, error) {
   fi, err := os.Stat(path)
   if err == nil {
      if fi.IsDir() {
         return true, nil
      }
      return false, errors.New("存在同名文件")
   }
   if os.IsNotExist(err) {
      return false, nil
   }
   return false, err
}

3.3 界说全局变量

global/global.go 中,增加 Log 成员特点

package global
import (
   "ewa_admin_server/config"
   "go.uber.org/zap"
   "github.com/spf13/viper"
)
var (
   EWA_CONFIG config.Configuration
   EWA_VIPER  *viper.Viper
   EWA_LOG    *zap.Logger
)

3.4 测验

main.go 中调用日志初始化函数,并尝试写入日志:

package main
import (
   "ewa_admin_server/core"
   "ewa_admin_server/global"
   "go.uber.org/zap"
   "github.com/gin-gonic/gin"
)
const AppMode = "debug" // 运转环境,主要有三种:debug、test、release
func main() {
   gin.SetMode(AppMode)
   // TODO:1.装备初始化
   global.EWA_VIPER = core.InitializeViper()
   // TODO:2.日志
   global.EWA_LOG = core.InitializeZap()
   zap.ReplaceGlobals(global.EWA_LOG)
   global.EWA_LOG.Info("server run success on ", zap.String("zap_log", "zap_log"))
   //  TODO:3.数据库衔接
   // TODO:4.其他初始化
   // TODO:5.发动服务
   core.RunServer()
}

重启项目,就会发现在根目录下生成了一个 log 文件夹,作为咱们日后开发用的日志记载文件。

【Go】基于 Gin 从0到1搭建 Web 管理后台系统后端服务(一)项目初始化、配置和日志

完好的代码见我的 github repo。下一篇将实现多种常见数据库的衔接。

end~