likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 17

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

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

在之前已经基于 React18+TS4.x+Webpack5 从0到1搭建了一个 React 基本项目架子,具体的步骤见下面三篇:

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

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的使用,最好是看官方文档,也可以看看下面几篇不错的文章:

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~