Go实战项目【三】日志系统和编写路由文件

日志服务

在服务端开发中,日志系统必不可少的,能帮助我们更快的找到问题和运行记录,接下来就简单的做个日志文件系统。
日志是以文件的形式存放在项目的目录中,所以需要使用Go对文件操作,封装一下

pkg/file/file.go

package file

import (
    "io/ioutil"
    "mime/multipart"    //它主要实现了 MIME 的 multipart 解析,主要适用于 HTTP 和常见浏览器生成的 multipart 主体
    "os"
    "path"
)

//获取文件大小
func GetSize(f multipart.File)(int , error)  {
    content,err := ioutil.ReadAll(f)

    return len(content),err
}

//获取文件后缀
func GetExt(filename string) string {
    return path.Ext(filename)
}

//检查文件是否存在
/*
   如果返回的错误为nil,说明文件或文件夹存在
   如果返回的错误类型使用os.IsNotExist()判断为true,说明文件或文件夹不存在
   如果返回的错误为其它类型,则不确定是否在存在
*/
func CheckExist(src string)bool  {
    _, err := os.Stat(src)
    if err == nil {
        return true
    }
    if os.IsNotExist(err) {
        return false
    }
    return false
}

//检查文件权限
func CheckPermission(src string)bool  {
    _,err := os.Stat(src)
    return os.IsPermission(err)
}

//新建文件夹
func MKDir(src string)error  {
    err := os.MkdirAll(src,os.ModePerm)
    return err
}

//如果不存在则新建文件夹
func IsNotExistMkDir(src string)error  {
    if exist := CheckExist(src); exist == false {
        if err := MKDir(src);err !=nil  {
            return err
        }
    }
    return nil
}

/*
   调用文件,支持传入文件名称、指定的模式调用文件、文件权限,返回的文件的方法可以用于I/O。如果出现错误,则为*PathError
   const (
       // Exactly one of O_RDONLY, O_WRONLY, or O_RDWR must be specified.
       O_RDONLY int = syscall.O_RDONLY // 以只读模式打开文件
       O_WRONLY int = syscall.O_WRONLY // 以只写模式打开文件
       O_RDWR   int = syscall.O_RDWR   // 以读写模式打开文件
       // The remaining values may be or'ed in to control behavior.
       O_APPEND int = syscall.O_APPEND // 在写入时将数据追加到文件中
       O_CREATE int = syscall.O_CREAT  // 如果不存在,则创建一个新文件
       O_EXCL   int = syscall.O_EXCL   // 使用O_CREATE时,文件必须不存在
       O_SYNC   int = syscall.O_SYNC   // 同步IO
       O_TRUNC  int = syscall.O_TRUNC  // 如果可以,打开时
   )
*/
func Open(name string,flag int,perm os.FileMode)(*os.File,error)  {
    f,err := os.OpenFile(name,flag,perm)
    if err != nil {
        return nil,err
    }
    return f,err
}

这个文件把大部分文件操作的方法封装了。注释中也有讲解。而且以后再有对文件进行操作(比如用户上传/更改头像,上传文件)也都可以使用这些方法进行操作。

然后进行对log的日志文件进行操作
pkg/logging/file.go

package logging

import (
    "api/pkg/file"
    "api/pkg/setting"
    "fmt"
    "os"
    "time"
)

//获取日志文件路径
func getLogFilePath() string {
    return fmt.Sprintf("%s%s", setting.AppSetting.RuntimeRootPath, setting.AppSetting.LogSavePath)
}

//获取日志文件的名称
func getLogFileName() string {
    return fmt.Sprintf("%s%s.%s",
        setting.AppSetting.LogSaveName,
        time.Now().Format(setting.AppSetting.TimeFormat),
        setting.AppSetting.LogFileExt,
    )
}

//打开日志文件
func openLogFile(fileName, filePath string) (*os.File, error) {

    dir, err := os.Getwd()  //返回与当前目录对应的根路径名
    if err != nil {
        return nil, fmt.Errorf("os.Getwd err: %v", err)
    }

    src := dir + "/" + filePath
    perm := file.CheckPermission(src)   //检查文件权限
    if perm == true {
        return nil, fmt.Errorf("file.CheckPermission Permission denied src: %s", src)
    }

    err = file.IsNotExistMkDir(src)     //如果不存在则新建文件夹
    if err != nil {
        return nil, fmt.Errorf("file.IsNotExistMkDir src: %s, err: %v", src, err)
    }

    //调用文件,在写入时将数据追加到文件中 | 如果不存在,则创建一个新文件 | 以只写模式打开文件
    f, err := file.Open(src + fileName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
    if err != nil {
        return nil, fmt.Errorf("Fail to OpenFile :%v", err)
    }

    return f, nil
}

日志的文件操作,将会以log+日期.log为命名文件。日志文件夹保存的路径在是根据配置文件设置的,路径为runtime/logs/

最后对打印日志的格式进行封装
pkg/logging/log.go

package logging

import (
    "fmt"
    "log"
    "os"
    "path/filepath"
    "runtime"
)

type Level int

var (
    F * os.File             //文件

    DefaultPrefix = ""      //默认的前缀
    DefaultCallDepth = 2    //调用深度

    logger *log.Logger      //打印
    logPrefix = ""          //打印前缀
    levelFlags = []string{"DEBUG", "INFO", "WARN", "ERROR", "FATAL"}    //调试,信息,警告,错误,崩溃
)

const (
    DEBUG Level = iota
    INFO
    WARNING
    ERROR
    FATAL
)

func SetUp()  {

    var err error
    filePath := getLogFilePath()        //获取日志文件路径
    fileName := getLogFileName()        //获取日志文件名称
    F, err = openLogFile(fileName, filePath)    //打开日志文件
    if err != nil {
        log.Fatalln(err)
    }

    /*
       log.New创建一个新的日志记录器。out定义要写入日志数据的IO句柄。prefix定义每个生成的日志行的开头。flag定义了日志记录属性
       log.LstdFlags:日志记录的格式属性之一,其余的选项如下
       const (
         Ldate         = 1 << iota     // the date in the local time zone: 2009/01/23
         Ltime                         // the time in the local time zone: 01:23:23
         Lmicroseconds                 // microsecond resolution: 01:23:23.123123.  assumes Ltime.
         Llongfile                     // full file name and line number: /a/b/c/d.go:23
         Lshortfile                    // final file name element and line number: d.go:23. overrides Llongfile
         LUTC                          // if Ldate or Ltime is set, use UTC rather than the local time zone
         LstdFlags     = Ldate | Ltime // initial values for the standard logger
       )
    */
    logger = log.New(F, DefaultPrefix, log.LstdFlags)
}

//设置前缀
func setPrefix(level Level)  {
    _,file,line,ok := runtime.Caller(DefaultCallDepth)
    if ok {
        logPrefix = fmt.Sprintf("[%s][%s:%d]", levelFlags[level], filepath.Base(file), line)
    }else {
        logPrefix = fmt.Sprintf("[%s]", levelFlags[level])
    }

    logger.SetPrefix(logPrefix)
}

func Debug(v ...interface{})  {
    setPrefix(DEBUG)
    logger.Println(v)
}

func Info(v ...interface{}) {
    setPrefix(INFO)
    logger.Println(v)
}

func Warn(v ...interface{}) {
    setPrefix(WARNING)
    logger.Println(v)
}

func Error(v ...interface{}) {
    setPrefix(ERROR)
    logger.Println(v)
}

func Fatal(v ...interface{}) {
    setPrefix(FATAL)
    logger.Fatalln(v)
}

日志打印有5个级别,调试,信息,警告,错误,崩溃。可以根据不同环境下的信息进行调用不同的级别。
main.go文件中初始化

...
setting.SetUp() //初始化配置文件
logging.SetUp()     //设置日志文件
...

路由文件

用Gin框架启动服务没问题后,进行路由文件的编写。
创建routers/routers.go文件,在这个文件中创建初始化路由的方法
routers/routers.go

package routers

import (
    "api/pkg/e"
    "api/pkg/setting"
    "github.com/gin-gonic/gin"
)

/*
    初始化路由
*/
func InitRouter() *gin.Engine {
    r := gin.New()        //创建gin框架路由实例
    r.Use(gin.Logger())   //使用gin框架中的打印中间件
    r.Use(gin.Recovery()) //使用gin框架中的恢复中间件,可以从任何恐慌中恢复,如果有,则写入500

    gin.SetMode(setting.ServerSetting.RunMode) //设置运行模式,debug或release,如果放在gin.New或者gin.Default之后,还是会打印一些信息的。放之前则不会

    apiv1 := r.Group("/api/v1") //路由分组,apiv1代表v1版本的路由组
    {
        apiv1.GET("version",v1.GetAppVersionTest)   //app版本升级
    }

    return r
}

创建routers/v1/app_version.go

package v1

import (
    "api/pkg/e"
    "github.com/gin-gonic/gin"
)

//app更新接口
func GetAppVersionTest(c *gin.Context)  {

    c.JSON(e.SUCCESS,gin.H{
        "Code":e.SUCCESS,
        "Msg":e.GetMsg(e.SUCCESS),
        "Data":"返回数据成功",
    })
}

同样的,也必须把main方法中的初始化路由修改下
main.go

...
func main() {
    log.Println("Hello, api 正在启动中...")
    setting.SetUp() //初始化配置文件

    router := routers.InitRouter()  //初始化路由

    s := &http.Server{
        Addr:fmt.Sprintf(":%d", setting.ServerSetting.HttpPort),        //设置端口号
        Handler:router,                                         //http句柄,实质为ServeHTTP,用于处理程序响应HTTP请求
        ReadTimeout:setting.ServerSetting.ReadTimeout,          //允许读取的最大时间
        WriteTimeout:setting.ServerSetting.WriteTimeout,        //允许写入的最大时间
        MaxHeaderBytes: 1 << 20,                                //请求头的最大字节数
    }

    /*
       使用 http.Server - Shutdown() 优雅的关闭http服务
    */
    go func() {
        if err := s.ListenAndServe(); err != nil{
            log.Printf("Listen: %s\n", err)
        }
    }()

    quit := make(chan os.Signal)
    signal.Notify(quit,os.Interrupt)
    <- quit

    log.Println("Shutdown Server ...")
    ctx, cancel := context.WithTimeout(context.Background(), 5 * time.Second)
    defer cancel()
    if err := s.Shutdown(ctx); err != nil {
        log.Fatal("Server Shutdown:", err)
    }

    log.Println("程序服务关闭退出")
}

可以看到对路由分组以后,现在接口的url是http://127.0.0.1:9999/api/v1/version
重新进行go run后,浏览器查看是否返回数据

以后的路由代码都会在routers/v1目录下编写,比如app_version.go文件就是有关app升级相关的代码,这里就把路由服务分解开来了。

点关注,不迷路

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,881评论 4 368
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,052评论 1 301
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,598评论 0 250
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,407评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,823评论 3 294
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,872评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,037评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,778评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,505评论 1 247
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,745评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,233评论 1 264
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,568评论 3 260
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,231评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,141评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,939评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,954评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,784评论 2 275