go goroutine + chan 异步写日志
go 写日志
使用文件目录
/zydhlog
log.go
console.go
log_to_file.go
#日志文件 - 初始时不可缺
error.txt
fatal.txt
normal.txt
warning.txt
/zydhfile
file.go
log.go 自己使用ini文件映射配置
package zydhlog
import (
"fmt"
"path"
"runtime"
"strconv"
"strings"
"time"
)
/**
日志库
接口,日志可以输出到终端,也可以输出到文件,输出到kafka
需求分析
1.可以不同地方写入
2.日志分级别
debug
trace
info
warning
error
fatal
3.日志要支持开关控制,比如开发的时候什么级别都能输出,但是上线后只有info级别的往下的才能输出
4.日志内容,要有时间,行号,文件名,日志级别,日志信息
5.日志文件要切割
*/
//日志等级
const(
ERR_LEVEL_NO int = iota
ERR_LEVEL_DEBUG
ERR_LEVEL_TRACE
ERR_LEVEL_INFO
ERR_LEVEL_WARNING
ERR_LEVEL_ERROR
ERR_LEVEL_FATAL
)
//日志存放路径
const(
LOG_FILE_PATH_NORMAL string = "./zydhlog/normal.txt" //普通日志路径
LOG_FILE_PATH_WARNING string = "./zydhlog/warning.txt" //警告日志路径
LOG_FILE_PATH_ERROR string = "./zydhlog/error.txt" //错误日志路径
LOG_FILE_PATH_FATAL string = "./zydhlog/fatal.txt" //500服务器内部致命错误日志路径
)
//调试等级,关闭打开 0 关闭,不写入 1 打开
const IS_OPEN_OR_CLOSE_LOG = 1
//打印位置 0 终端,1 文件 2 通道->文件
const LOG_TO_SITE = 2
//日志通道能放的最大队列数
const LOG_CHAN_MAX_CAP = 1000
//分隔文件的方式 -1 不分割 0 按照文件大小切割 1 按照时间日期切割
const CUT_FILE_TYPE = 0
//LogChan 日志通道
var LogChan chan *LogToFileByGoChan
//初始化
func init(){
//当开启日志 且 是使用go-chan方法时,初始化通道
if IS_OPEN_OR_CLOSE_LOG == 1 && LOG_TO_SITE == 2 {
LogChan = make(chan *LogToFileByGoChan,LOG_CHAN_MAX_CAP) //初始化通道
runtime.GOMAXPROCS(4)
//开启通道处理
for i:=0; i<=3;i++{
go getChanLogToFile(LogChan)
}
}
}
/*
func 组装日志的内容
param errLevel int 错误级别
param layer int time.Caller()的层
param errInfo string 错误信息
return string
*/
func LogContent(errLevel,layer int ,errInfo string,arg ...any)string{
timeString := time.Now().Format("2006-01-02 15:04:05.000")
txtMap := map[int]string{
ERR_LEVEL_NO : "[normal]",
ERR_LEVEL_DEBUG : "[debug]",
ERR_LEVEL_TRACE : "[trace]",
ERR_LEVEL_INFO : "[info]",
ERR_LEVEL_WARNING : "[warning]",
ERR_LEVEL_ERROR : "[error]",
ERR_LEVEL_FATAL : "[fatal]",
}
// fmt.Sprintf("%v",error) //把错误信息转换成string
return timeString + " " + getLayerCode(layer) + "\n" + txtMap[errLevel] + fmt.Sprintf("%v",errInfo) + " | " + fmt.Sprintf("%v",arg) + "\n"
}
/*
func 获取锚点代码的方法锚点位置 包名/文件名.go/ -> 方法名() | 行号
param layer int 层,调用的层级 详细看 study/runtime.go --> runtimeCallerStudy()
return anchorPosition string 调用此方法的锚点位置
analysis:
//pc:调用函数的一些信息 file :文件名,当前执行的文件名xxx.go/谁调用了当前函数 line : 调用的多少行 ok :如果能取到,ok == true
//fmt.Println(pc,file,line) //8726872 D:/G-workSpace/golang/src/go-restudy/study/test.go 71
//fmt.Println( path.Base(file),runtime.FuncForPC(pc).Name(),line ) //test.go go-restudy/study.zydhLog 71
//fmt.Println( pkgNameAndFuncName[0] + "/" + path.Base(file) + " -> " + pkgNameAndFuncName[1] + "()" + " | " + strconv.Itoa(line) + "行" )
//go-restudy/study/test.go -> zydhLog() | 71行
*/
func getLayerCode(layer int)string{
//传递参数,可以拿到当前执行程序执行隔了多少层
pc,file,line,ok := runtime.Caller(layer)
if !ok {
return "use runtime.caller() failed," + "no find layer file name," +"no find layer func name"
}
//获得报名和文件名
pkgNameAndFuncName := strings.Split(runtime.FuncForPC(pc).Name(),".")
return pkgNameAndFuncName[0] + "/" + path.Base(file) + " -> " + pkgNameAndFuncName[1] + "()" + " | " + strconv.Itoa(line) + "行"
//go-restudy/study/test.go -> zydhLog() | 71行
}
/*
func : 把日志写入到场景[通道]
param : errLevel int 错误级别
param : logFilePath string 日志存放位置
param : logMsg string 日志信息
param : arg ...any
*/
func logWriteToScene(errLevel int,logFilePath,logMsg string,arg ...any){
//是否开启日志
if IS_OPEN_OR_CLOSE_LOG > 0{
logmsg := LogContent(errLevel,4,logMsg,arg)
//开启后,日志写到哪的场景
switch LOG_TO_SITE{
case 1: //文件 no goroutine
writeLogToFile(errLevel,logFilePath,logmsg)
case 2: //发送到通道 ToChan
select{
case LogChan <- NewLogToFileByGoChan(errLevel,logFilePath,logmsg): //当通道没满时,走这
default: //当通道满了,走这,丢弃日志,保证不出现阻塞
}
default: //终端
outputToTerminal(logmsg)
}
}
}
//实现方法 - 不使用接口,接口每次都要先实例化一个对应的结构体,错误
func Trace(logInfo string,arg ...any){
logWriteToScene(ERR_LEVEL_TRACE,LOG_FILE_PATH_NORMAL,logInfo,arg)
}
func Debug(logInfo string,arg ...any){
logWriteToScene(ERR_LEVEL_DEBUG,LOG_FILE_PATH_NORMAL,logInfo,arg)
}
func Warning(logInfo string,arg ...any){
logWriteToScene(ERR_LEVEL_WARNING,LOG_FILE_PATH_WARNING,logInfo,arg)
}
func Info(logInfo string,arg ...interface{}){
logWriteToScene(ERR_LEVEL_INFO,LOG_FILE_PATH_NORMAL,logInfo,arg)
}
func Error(logInfo string,arg ...any){
logWriteToScene(ERR_LEVEL_ERROR,LOG_FILE_PATH_ERROR,logInfo,arg)
}
func Fatal(logInfo string,arg ...any){
logWriteToScene(ERR_LEVEL_FATAL,LOG_FILE_PATH_FATAL,logInfo,arg)
}
console.go
package zydhlog
import (
"fmt"
"os"
)
/*
.go : 把日志打印到终端
author : 朝游东海
*/
/*
func : 往终端输出
param : errLevel int 错误级别
param : logInfo string 日志信息
param : arg ...any
analysis:
//fmt.Println() //这个方法是往终端打印
//fmt.Fprintln("xx.file",logInfo) //这个方法是往文件写入 需要 OpenFile()文件
*/
func outputToTerminal(logMsg string){
//往终端写入
fmt.Fprint(os.Stdout,logMsg)
}
log_to_file.go
package zydhlog
import (
"fmt"
"go-restudy/zydhfile"
"os"
"time"
)
/*
.go : 把日志写入文件 - no goroutine
5.24 update:通过 goroutine + channel 写日志
author : 朝游东海
*/
//LogToFileByGoChan 写入通道的日志格式
type LogToFileByGoChan struct {
leveL int //错误级别
logFilePath string //日志存放位置
logMsg string //错误的信息
}
//NewLogToFileByGoChan 初始化
func NewLogToFileByGoChan(level int,logFilePath,logMsg string) *LogToFileByGoChan {
return &LogToFileByGoChan{
leveL : level,
logFilePath : logFilePath,
logMsg : logMsg,
}
}
/*
func : 启动时,开启goroutine处理通道数据
param : ch 只读通道 *LogToFileByGoChan类型
*/
func getChanLogToFile(ch <- chan *LogToFileByGoChan){
for{
select {
//当通道有值时
case logData := <- ch:
writeLogToFile(logData.leveL,logData.logFilePath,logData.logMsg)
default:
time.Sleep(time.Millisecond * 500) //当通道无值时,交出cpu控制权
}
}
}
/*
func 写[通道]日志到文件
param errLevel int 错误级别
param logFilePath string 日志文件存放路径
param errInfo string 错误信息
*/
func writeLogToFile(errLevel int,logFilePath ,logMsg string,arg ...any){
if errLevel >= 0 {
// 是否切割文件 获取日志文件地址
filePath := fileIsOpenCut(logFilePath)
//打开文件 //判断文件是否存在
f,err := zydhfile.CreatFile(filePath,os.O_WRONLY|os.O_CREATE|os.O_APPEND,0644);if err != nil{
fmt.Printf("creat file false,err %v \n",err)
return
}
defer f.Close()
//直接写入字符串数据
_,err =f.WriteString(logMsg);if err != nil{
fmt.Println("log to file fail,err:",err)
}
}
}
/*
是否开启文件切割
*/
func fileIsOpenCut(logFilePath string)(filePath string){
if CUT_FILE_TYPE >= 0{
//切割文件
switch CUT_FILE_TYPE {
case 0: //按照文件大小切割
filePath = zydhfile.FileCutBySize(logFilePath)
case 1: //按照时间切割
filePath = zydhfile.FileCutByTime(logFilePath)
default:
filePath = zydhfile.FileCutByTime(logFilePath)
}
}else{
filePath = logFilePath
}
return filePath
}
/zydhfile
file.go
package zydhfile
import (
"bufio"
"fmt"
"io"
"os"
"strings"
"time"
)
/*
.go : 自定义的一些文件操作
author 朝游东海
*/
var DefaultPerm = os.ModeDir|os.ModePerm
//按日期切割文件的名称时间格式
const CUT_FILE_BY_DATE_TIME_FORMAT = "200601021504"
//按文件大小切割文件的名称时间格式
const CUT_FILE_BY_SIZE_TIME_FORMAT = "20060102150405"
//文件超过多大时,切割文件
const LOG_FILE_MAX_SIZE = 1 *1024 // 1 * 1024 * 1024 //1m
//------------------------创建文件------------------------//
/*
func 创建目录
author zydh[朝游东海]
param dirPath string 文件路径
param perm uint32 文件权限 0644
return bool 返回信息
*/
func MkdirFile(dirPath string,perm os.FileMode ) (bool bool,errInfo error,){
if perm == 0{
perm = DefaultPerm
}
err := os.Mkdir(dirPath,perm)
if err !=nil {
return false,err
}
return true,errInfo
}
/*
func 创建完整目录路径,即中间目录不存在的话也一起创建
author zydh[朝游东海]
param dirPath string 文件路径
param perm uint32 文件权限 0644
return bool 返回信息
*/
func MkdirAll(dirPath string,perm os.FileMode) (bool bool,errInfo error,){
if perm == 0{
perm = DefaultPerm
}
err := os.MkdirAll(dirPath,perm)
if err !=nil {
return false,err
}
return true,errInfo
}
//创建文件
func CreatFile(path string,flag int,perm os.FileMode)(*os.File,error){
if flag == 0{
flag = os.O_CREATE|os.O_TRUNC|os.O_WRONLY
}
if perm == 0{
flag = 0666
}
file, err := os.OpenFile(path, flag, perm)
if err != nil {
return file, err
}
return file,nil
}
//------------------------读文件------------------------//
// GetFileConfigByPath 只读 - 获取文件信息 // 文件名称 //文件大小
func GetFileConfigByPath(path string)(fileName string, fileSize int){
//f,_ := os.Open(path)
//fmt.Println(f.Name()) //./zydhlog/warning.txt
//fmt.Println(f.Stat())
fo,err := os.Stat(path)
if err != nil{
//log.Error("open file fail ,file path:" + path,err)
return "",0
}
return fo.Name(),int(fo.Size())
//warning.txt //537 //单位,字节
}
// GetFileNameByPath 获取文件的大小 //单位,字节
func GetFileNameByPath(path string) string {
fo,err := os.Stat(path)
if err != nil{
fmt.Println("open file fail ,file path:" + path,err)
//log.Error("open file fail ,file path:" + path,err)
return ""
}
return fo.Name()
}
//GetFileSizeByPath获取文件的大小 单位,字节
func GetFileSizeByPath(path string) int {
fo,err := os.Stat(path)
if err != nil{
//log.Error("open file fail ,file path:" + path,err)
return 0
}
return int(fo.Size())
}
//------------------------bufio读取文件-----------------------------//
//bufio读取文件 bufio是在file的基础上封装了一层API,支持更多的功能。
//zydh:可自定义读行,分段读
//BufioReadFile 缓冲区读取文件
func BufioReadFile(path string,splitChar byte ){
if !FileOrDirIsExist(path){
fmt.Println("path is not exist")
return
}
if splitChar == 0{
splitChar = '\n'
}
file, err := os.Open(path)
if err != nil {
fmt.Println("open file failed, err:", err)
return
}
defer file.Close()
reader := bufio.NewReader(file) //创建读对象 bufio.NewReader()(带缓冲区的方式打开,适合打开较大的文件)
for {
line, err := reader.ReadString(splitChar) //注意是字符,单引号 //\n 一行一行的读取 reader.ReadString()(读取文件)
if err == io.EOF { //io.EOF表示读到文件末尾
if len(line) != 0 {
fmt.Println(line)
}
fmt.Println("文件读完了")
break
}
if err != nil {
fmt.Println("read file failed, err:", err)
return
}
fmt.Print(line) //这里不用fmt.Println()
}
}
//------------------------写文件------------------------//
func WriteFile(txt,path string,flag int,perm os.FileMode)(bool bool,errInfo error){
if flag == 0{
flag = os.O_CREATE|os.O_TRUNC|os.O_WRONLY
}
if perm == 0{
flag = 0666
}
file, err := os.OpenFile(path, flag, perm)
if err != nil {
fmt.Println("open file failed, err:", err)
return false,err
}
defer file.Close()
//str := "hello 沙河"
//file.Write([]byte(str)) //写入字节切片数据
//file.WriteString("hello 小王子") //直接写入字符串数据
_,err = file.WriteString(txt) //直接写入字符串数据
if err != nil{
return false,err
}
return true,errInfo
}
//------------------------判断文件是否存在------------------------//
//判断文件是否存在
//go中判断一个文件或者文件夹是否存在方法为:os.Stat() ,通过对返回的错误值进行判断
//1.如果err的值为nil,说明文件或文件夹存在
//2.如果返回的错误类型 使用 os.IsNotExist() 判断为true,说明文件或文件夹不存在
//3.如果返回的错误为其他类型,则不确定是否存在
//所以封装一个函数,用来判断 文件或文件夹是否存在
/*
func 判断文件或目录是否存在
link 原文链接:https://blog.csdn.net/leo_jk/article/details/118255913
author zydh[朝游东海]
param path string 文件路径
return bool 返回信息
*/
func FileOrDirIsExist(path string)(bool bool){
_,err:= os.Stat(path)
if err == nil{
return true
}
if os.IsNotExist(err){
return false
}
return false
//通过stat的isDir还可以判断一个路径是文件夹还是文件
//stat.IsDir()
}
//------------------------删除文件------------------------//
/*
func 根据文件路径删除文件
author zydh[朝游东海]
param path string 文件路径
return bool 返回信息
*/
func DelFileByPath(path string)(bool bool,errInfo error){
if FileOrDirIsExist(path){
err := os.Remove(path)
if err != nil{
return false,err
}
return true,errInfo
}
return true,errInfo
}
//文件重命名 - ? 毫无用处
func FileRename(oldName,newName string)(bool,error){
err := os.Rename(oldName,newName)
if err != nil{
return false,err
}
return true,nil
}
//GetFileLastPath 获取文件路径 ./zydh/log/log.txt --> ./zydh/log/
func GetFileLastPath(path string) string{
//分隔
splice := strings.Split(path, "/")
return strings.Join(splice[:len(splice)-1],"/")
}
//------------------------切割文件------------------------//
/*
1.按文件大小切割
2.按日期切割
*/
/*
func:FileCutBySize 按文件大小切割 每次记录前判断这个文件的大小
param : path string 文件路径
analysis:
////os.Rename("./aa/bb/c1/file.go", "./aa/bb/c2/file.go")
//err := os.Rename(pName,logName) //会把文件移动到/根目录下
*/
func FileCutBySize(path string)string{
//判断文件是否超出大小
if GetFileSizeByPath(path) >= LOG_FILE_MAX_SIZE{
//2.备份 xx.log -> xx.log.back202205071155 // warning.txt.bak20220507122541.973
//在原目录备份 / warning.txt warning.txt.bak20220507122541.973
err := os.Rename(GetFileLastPath(path) + "/" + GetFileNameByPath(path),getLogFilePathByTime(path,1));if err != nil{
fmt.Println("zydhfile/file.go FileCutBySize Backup failed")
}
}
return path
}
//FileCutByTime 按日期切割
/*
方法一如下
方法二,在LogToFile{}结构体中新增字段,oldPathFileName,把最新的文件名存入字段,然后在程序中和现在的时间比较,不用查
*/
func FileCutByTime(path string)string{
//返回路径
return getLogFilePathByTime(path,2)
}
/*
func : 获取新的日志文件路径
param : path string 原路径
param : scene int 场景 1 按文件大小 2 按日期
return : string 根据时间拼接的新路径
*/
func getLogFilePathByTime(path string,scene int) string{
//文件路径和名词
pName := GetFileNameByPath(path) //warning.txt
var nowStr,logName string
if scene == 1{
nowStr = time.Now().Format(CUT_FILE_BY_SIZE_TIME_FORMAT) //20220507122541.973
logName = fmt.Sprintf("%s.bak%s",pName,nowStr) //拼接一个备份
}else if scene == 2{
nowStr = time.Now().Format(CUT_FILE_BY_DATE_TIME_FORMAT) //20220507122541 //按秒分隔
logName = fmt.Sprintf("[%s]%s",nowStr,pName) //拼接一个备份
}
//[2022-05-07-12:25:41]warning.txt
return GetFileLastPath(path) + "/" +logName
// ./zydhlog/[2022-05-07-12:25:41]warning.txt
}

浙公网安备 33010602011771号