文末有福利领取哦~
👉一、Python所有方向的学习路线
Python所有方向的技术点做的整理,形成各个领域的知识点汇总,它的用处就在于,你可以按照上面的知识点去找对应的学习资源,保证自己学得较为全面。
👉二、Python必备开发工具
👉三、Python视频合集
观看零基础学习视频,看视频学习是最快捷也是最有效果的方式,跟着视频中老师的思路,从基础到深入,还是很容易入门的。
👉 四、实战案例
光学理论是没用的,要学会跟着一起敲,要动手实操,才能将自己的所学运用到实际当中去,这时候可以搞点实战案例来学习。(文末领读者福利)
👉五、Python练习题
检查学习结果。
👉六、面试资料
我们学习Python必然是为了找到高薪的工作,下面这些面试题是来自阿里、腾讯、字节等一线互联网大厂最新的面试资料,并且有阿里大佬给出了权威的解答,刷完这一套面试资料相信大家都能找到满意的工作。
👉因篇幅有限,仅展示部分资料,这份完整版的Python全套学习资料已经上传
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
日志组件在我们日常开发中是必不可少的, 可以在系统运行时为我们记录系统的运行状态.
使用日志的好处:
- 打印调试:日志可以记录变量或者某一段逻辑,记录程序运行的流程。
虽然用日志来调试通常被人认为是技术手段落后,但它确实能够解决某些难题。
例如,一个场景线下无法复现,我们又不希望对线上系统产生破坏性的影响,这时打印调试就派上用场了。 - 问题定位:有时候系统或者业务出现问题,我们需要快速排查原因,这时我们就要用到日志的功能了。
例如,Go 程序突然 panic,被 recover 捕获之后,打印出当前的详细堆栈信息,就需要通过日志来定位。 - 用户行为分析:日志的大量数据可以作为大数据分析的基础,例如可以分析用户的行为偏好等。
- 监控:日志数据通过流处理生成的连续指标数据,可以存储起来并对接监控告警平台,这有助于我们快速发现系统的异常。
监控的指标可能包括:核心接口调用量是否突然下降或上升,核心的业务指标(GMV 是否同比和环比稳定,是否出现了不合理的订单,是否出现了零元或者天价账单)等。
Go语言常用的日志库
- log包:Go语言标准库中自带的日志库,可以方便地记录日志信息,同时可以输出到控制台和文件中。
- zap:一个高性能、结构化的日志库,支持多种输出格式和日志级别,可以满足大多数应用的需求。
- logrus:一个功能丰富、高度可定制化的日志库,支持多种输出格式和钩子机制,可以满足各种应用场景的需求。
- zerolog:一个轻量级、快速的日志库,支持结构化日志输出和JSON格式化,可以方便地集成到现有的应用中。
- 以及在实验中的结构化的日志库slog
标准log库
标准库 log 提供了 3 个打印日志的接口,分别为log.Println
、log.Fatalln
和log.Panicln
。
另外 log 还可以通过log.SetPrefix
设置打印日志时的前缀,通过log.SetFlags
设置打印的时间和文件的格式。
func init() {
log.SetPrefix("TRACE: ")
// log.Ldate 代表日志会打印日期
// log.Lmicroseconds 代表日志会打印微秒
// log.Llongfile 的意思是日志会输出长文件名的形式。
log.SetFlags(log.Ldate | log.Lmicroseconds | log.Llongfile)
}
func main() {
log.Println("message")
log.Fatalln("fatal message") //会调用os.Exit(1)强制退出,所以下面的代码不会被执行
log.Panicln("panic message") //会panic,可以通过recover捕获
}
//outpu:
TRACE: 2022/10/12 22:22:36.540776 a/b/main.go:19: message
TRACE: 2022/10/12 22:22:36.541046 a/b/main.go:20: fatal message
标准 log 库的不足之处在于,无法对日志进行分级,在生产环境中日志可能会有 DEBUG、INFO 等级别。并且我们也并不希望在打印日志时触发 panic,或者让程序直接退出。
log 提供了一些扩展能力,它让我们可以自定义不同级别的日志,例如借助 log.New 完成日志分级的功能,log.New 可以指定新 log 的输出位置、日志的前缀和格式。Error.Println 最终会将数据输出到文件中。
var (
Error \*log.Logger
Warning \*log.Logger
)
func init() {
file, err := os.OpenFile("errors.txt",
os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
log.Fatalln("Failed to open error log file:", err)
}
Warning = log.New(os.Stdout,
"WARNING: ",
log.Ldate|log.Ltime|log.Lshortfile)
Error = log.New(file,
"ERROR: ",
log.Ldate|log.Ltime|log.Lshortfile)
}
func main() {
Warning.Println("There is something you need to know about")
Error.Println("Something has failed")
}
虽然提供了有限的扩展能力,标准库 log 仍然存在一些不足之处(例如对参数的处理借助了 fmt.Sprintxxx 函数,中间包含了大量反射,性能比较低下;而且 log 不会输出类似 JSON 的结构化数据,要想实现文件切割也比较困难),所以我们一般是在本地开发环境中使用它。
一个工业级的日志组件应该具备什么特性呢?
- 不同的环境有不同的输出行为,比如测试和开发环境要输出到控制台,而生产环境则要输出到文件中;
- 日志分等级;
- 类似 JSON 的结构化输出,这会使日志更容易阅读,也有助于后续查找和存储日志;
- 支持日志文件切割 (可以按照日期、时间间隔或者文件大小对日志进行切割);
- 可以定义输出的格式,例如打印日志的函数、所在的文件、行号、记录时间等;
- 速度快。
zap
Zap 是 Uber 开源的日志组件,当前在社区非常受欢迎。在满足了日志库应该具备的基本功能的基础上,Zap 在内存分配量与速度方面相比其他日志组件都有较大的优势。
Zap 提供了两种类型的日志,分别是 Logger 与 SugaredLogger。
Logger 是默认的,每个要写入的字段都得指定对应类型的方法,这种方式可以不使用反射,因此效率更高。
为了避免在异常情况下丢失日志(尤其是在崩溃时),在进程退出之前执行logger.Sync() 会将所有位于缓冲区中的日志条目落盘。
package main
import "go.uber.org/zap"
func main() {
logger, \_ := zap.NewProduction()
defer logger.Sync()
url := "www.google.com"
logger.Info("failed to fetch URL",
zap.String("url", url),
zap.Int("attempt", 3),
zap.Duration("backoff", time.Second))
}
而 SugaredLogger 的性能稍微低于 Logger,但是它提供了一种更灵活的打印方式:
func main() {
logger, \_ := zap.NewProduction()
defer logger.Sync()
sugar := logger.Sugar()
url := "www.google.com"
sugar.Infow("failed to fetch URL",
"url", url,
"attempt", 3,
"backoff", time.Second,
)
}
//output
{"level":"info","ts":1665669124.251897,"caller":"a/b.go:20","msg":"failed to fetch URL","url":"www.google.com","attempt":3,"backoff":1}
Zap 预置了 3 种格式的 Logger:
- zap.NewExample()
- zap.NewDevelopment()
- zap.NewProduction()
这 3 个函数可以传入若干类型为 zap.Option 的选项,从而扩展 Logger 的行为。
选项包括 zap.WithCaller 打印文件、行号、zap.AddStacktrace 打印堆栈信息。
除此之外,我们还可以定制自己的 Logger,提供比预置的 Logger 更灵活的能力。
举一个例子,zap.NewProduction() 实际调用了 NewProductionConfig().Build(),而 NewProductionConfig() 生成的 zap.Config 可以被定制化。
func NewProduction(options ...Option) (\*Logger, error) {
return NewProductionConfig().Build(options...)
}
修改时间打印格式:
func main() {
loggerConfig := zap.NewProductionConfig()
loggerConfig.EncoderConfig.TimeKey = "timestamp"
loggerConfig.EncoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout(time.RFC3339)
logger, err := loggerConfig.Build()
if err != nil {
log.Fatal(err)
}
sugar := logger.Sugar()
sugar.Info("Hello from zap logger")
}
//output
{"level":"info","timestamp":"2022-10-13T23:12:17+08:00","caller":"a/main.go:169","msg":"Hello from zap logger"}
日志切割
在 Zap 中,我们也可以通过底层的 Zap.New 函数的扩展能力完成更加定制化的操作。例如,指定日志输出的 Writer 行为。
不同的 Writer 可能有不同的写入行为,像是输出到文件还是控制台,是否需要根据时间和文件的大小对日志进行切割等。Zap 将日志切割的能力开放了出来,只要日志切割组件实现了 zapcore.WriteSyncer 接口,就可以集成到 Zap 中。比较常用的日志切割组件为 lumberjack.v2 。下面这个例子将 lumberjack.v2 组件集成到了 Zap 中:
w := &lumberjack.Logger{
Filename: "/var/log/myapp/foo.log",
MaxSize: 500, // 日志的最大大小,以M为单位
MaxBackups: 3, // 保留的旧日志文件的最大数量
MaxAge: 28, // 保留旧日志文件的最大天数
}
core := zapcore.NewCore(
zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()),
w,
zap.InfoLevel,
)
logger := zap.New(core)
日志分级
在输出日志时,我们需要根据日志的用途进行分级。目前最佳的实践是将日志级别分为了五类。
- DEBUG : 主要是用来调试的。
通过打印当前程序的调用链,我们可以知道程序运行的逻辑,了解关键分支中详细的变量信息、上下游请求参数和耗时等,帮助开发者调试错误,判断逻辑是否符合预期。
一般用在开发和测试初期。不过,由于太多的 DEBUG 日志会降低线上程序的性能、同时导致成本上升,因此,在生产环境中一般不会打印 DEBUG 日志。 - INFO : 记录了系统中核心的指标。
例如,初始化时配置文件的路径,程序所处集群的环境和版本号,核心指标和状态的变化;
再比如,监听到外部节点数量的变化,或者外部数据库地址的变化。
INFO 信息有助于我们了解程序的整体运行情况,快速排查问题。 - WARNING : 输出程序中预知的,程序目前仍然能够处理的问题。
例如,打车服务中,行程信息会存储在缓存中方便我们快速查找。但是如果在缓存中查不到用户的行程信息,这时我们可以用一些兜底的策略重建行程,继续完成后续的流程。
这种不影响正常流程,但又不太符合预期的情况就适合用 WARNING。WARNING 还可以帮助我们在事后分析异常情况出现的原因。 - ERROR: 针对一些不可预知的问题,例如网络通信或者数据库连接的异常等。
- FATAL : 针对程序遇到的严重问题,意味着需要立即终止程序。
例如遇到了不能容忍的并发冲突时,就应该使用 FATAL 级别的日志。
Zap 日志库在上面五个分级的基础上还增加了 Panic 和 DPanic 级别,如下所示。Panic 级别打印后会触发 panic。而 DPanic 级别比较特殊,它在 development 模式下会 panic,相当于 PanicLevel 级别,而在其他模式下相当于 ErrorLevel 级别。
const (
DebugLevel = zapcore.DebugLevel
InfoLevel = zapcore.InfoLevel
WarnLevel = zapcore.WarnLevel
ErrorLevel = zapcore.ErrorLevel
DPanicLevel = zapcore.DPanicLevel
PanicLevel = zapcore.PanicLevel
FatalLevel = zapcore.FatalLevel
)
日志格式规范
对于打印出来的日志格式,我们希望它尽量符合通用的规范,以便公共的采集通道进行进一步的日志处理。
一个合格的日志至少应该有:
- 具体的时间
- 打印的行号
- 日志级别等重要信息。
同时,在大规模的集群中,还可能包含机器的 IP 地址,调用者 IP 和其他业务信息。规范日志示例如下:
{"level":"info","timestamp":"2022-10-13T23:29:10+08:00","caller":"a/main.go:168","msg":"data","orderid":1414281843,"traceid":124111424,"appversion":"v1.1","caller\_ip":"10.8.0.1","reason\_type":30}
下面列出一个实际中在使用的日志规范,可以作为一个参考:
- 每一行日志必须至少包含日志 Key、TimeStamp、打印的行号、日志级别字段;
- 单条日志长度在 1MB 以内,单个服务节点每秒的日志量小于 20MB;
- 建议使用 UTF-8 字符集,避免入库时出现乱码;
- Key 为每个字段的字段名,一行示例中的 Key 是唯一的;
- Key 的命名只能包含数字、字母、下划线,且必须以字母开头;
- Key 的长度不可大于 80 个字符;
- Key 中的字母均为小写。
构建项目日志组件
我们把 zapcore.Core 作为一个自定义类型 Plugin ,zapcore.Core 定义了日志的编码格式以及输出位置等核心功能。
作为一个通用的库,下面我们实现了 NewStdoutPlugin、NewStderrPlugin、NewFilePlugin 这三个函数,分别对应了输出日志到 stdout、stderr 和文件中。这三个函数最终都调用了 zapcore.NewCore 函数。
type Plugin = zapcore.Core
func NewStdoutPlugin(enabler zapcore.LevelEnabler) Plugin {
return NewPlugin(zapcore.Lock(zapcore.AddSync(os.Stdout)), enabler)
}
func NewStderrPlugin(enabler zapcore.LevelEnabler) Plugin {
return NewPlugin(zapcore.Lock(zapcore.AddSync(os.Stderr)), enabler)
}
// Lumberjack logger虽然持有File但没有暴露sync方法,所以没办法利用zap的sync特性
// 所以额外返回一个closer,需要保证在进程退出前close以保证写入的内容可以全部刷到磁盘
func NewFilePlugin(
filePath string, enabler zapcore.LevelEnabler) (Plugin, io.Closer) {
var writer = DefaultLumberjackLogger()
writer.Filename = filePath
return NewPlugin(zapcore.AddSync(writer), enabler), writer
}
func NewPlugin(writer zapcore.WriteSyncer, enabler zapcore.LevelEnabler) Plugin {
return zapcore.NewCore(DefaultEncoder(), writer, enabler)
}
NewFilePlugin 中暴露出了两个参数:filePath 和 enabler。
- filePath 表示输出文件的路径
- enabler 代表当前环境中要打印的日志级别。
刚才我们梳理了 Zap 中的七种日志级别,它们是一个整数,按照等级从上到下排列的,等级最低的是 Debug,等级最高的为 Fatal。
在生产环境中,我们并不希望打印 Debug 日志,因此我们可以在生产环境中指定 enabler 参数为 InfoLevel 级别,这样,就只有大于等于 enabler 的日志等级才会被打印了。
const (
DebugLevel = zapcore.DebugLevel
InfoLevel = zapcore.InfoLevel
WarnLevel = zapcore.WarnLevel
ErrorLevel = zapcore.ErrorLevel
DPanicLevel = zapcore.DPanicLevel
PanicLevel = zapcore.PanicLevel
FatalLevel = zapcore.FatalLevel
)
下一步是日志切割,在 NewFilePlugin 中,我们借助 lumberjack.v2 来完成日志的切割。
// 1.不会自动清理backup
// 2.每200MB压缩一次,不按时间切割
func DefaultLumberjackLogger() \*lumberjack.Logger {
return &lumberjack.Logger{
MaxSize: 200,
LocalTime: true,
Compress: true,
}
}
最后,我们要暴露一个通用的函数 NewLogger 来生成 logger。
默认的选项会打印调用时的文件与行号,并且只有当日志等级在 DPanic 等级之上时,才输出函数的堆栈信息。
func NewLogger(plugin zapcore.Core, options ...zap.Option) \*zap.Logger {
return zap.New(plugin, append(DefaultOption(), options...)...)
}
func DefaultOption() []zap.Option {
var stackTraceLevel zap.LevelEnablerFunc = func(level zapcore.Level) bool {
return level >= zapcore.DPanicLevel
}
return []zap.Option{
zap.AddCaller(),
zap.AddStacktrace(stackTraceLevel),
}
}
现在让我们在 main 函数中集成 log 组件,文件名和日志级别现在是写死的,后续我们会统一放入到配置文件中。
func main() {
plugin, c := log.NewFilePlugin("./log.txt", zapcore.InfoLevel)
defer c.Close()
logger := log.NewLogger(plugin)
logger.Info("log init end")
}
// output
{"level":"INFO","ts":"2022-10-14T23:59:15.701+0800","caller":"crawler/main.go:17","msg":"log init end"}
slog
前世今生
针对Go标准库log包,Go社区要求改进的声音始终不断,主流声音聚焦在以下几点:
- log包是为了方便人类可读而设计的,不支持便于机器解析的结构化日志(比如像zap那样输出json格式的日志
- 不支持日志级别(log level)
- log包采用专有日志输出格式,又没有提供可供Go社区共同遵循的Logger接口类型,导致Go社区项目使用的log输出格式五花八门,相互之间又难以兼容。
Go社区曾经尝试过合力改进标准库log包,并撰写了Proposal设计初稿,但最终因各种原因都没有被Go核心团队接受。
2022年8月末,Go团队的Jonathan Amsterdam发起discussion,意在和社区讨论为Go标准库添加结构化的、支持日志级别的日志包相关事宜,并形成一个一致的Proposal。
Jonathan Amsterdam将该结构化日志包命名为slog,计划放在log/slog下。他还在golang.org/x/exp下面给出了slog的初始实现,该Proposal正式进入review阶段。至于何时能正式落地到Go正式版本中还不可知。
设计
slog的设计之初对社区目前的一些应用广泛的log包进行了详细调研,比如uber zap、zerolog等,因此slog的设计也算是“站在前人的肩膀上”,尤其是uber zap。
Jonathan Amsterdam为此次slog的设计设定了如下目标(摘自slog的proposal):
- 易用性: 通过对现有日志包的调查发现,程序员们希望有一套简洁且易于理解的logging API。在此proposal中,我们将采用目前最流行的方式来表达键值对,即交替传入键和值。
- 高性能: 该log API的设计将尽量减少内存分配和加锁。它提供了一个交替输入键和值的方法,虽略繁琐,但速度更快
- 可以与运行时跟踪(tracing)集成: Go团队正在开发一个改进的运行时跟踪(runtime tracing)系统。本软件包的日志将可以被无缝集成到这个跟踪系统中,使开发者能够将他们的程序行为与运行时的行为联系起来。
slog从逻辑上分为前端(front)和后端(backend)。
- slog前端就是slog提供给使用者的API,不过,很遗憾slog依旧像log那样没有抽取出Logger接口,而是定义了一个Logger结构体,并提供了如图中的那些方法,这也意味着我们依旧无法在整个Go社区统一前端API;
- 通过前端方法,slog将日志内容以及相关属性信息封装成一个slog.Record类型实例,然后传递给slog的后端。
- 如果你使用的是Go社区的第三方log包的前端方法,比如zap,如果要使用slog后端,你同样需要对zap等进行封装,让其输出slog.Record并传递给slog的后端(目前尚没有这方面示例)。
slog将后端抽象为slog.Handler接口,接口如下:
type Handler interface {
// Enabled reports whether the handler handles records at the given level.
// The handler ignores records whose level is lower.
// It is called early, before any arguments are processed,
// to save effort if the log event should be discarded.
// If called from a Logger method, the first argument is the context
// passed to that method, or context.Background() if nil was passed
// or the method does not take a context.
// The context is passed so Enabled can use its values
// to make a decision.
Enabled(context.Context, Level) bool
// Handle handles the Record.
// It will only be called Enabled returns true.
// The Context argument is as for Enabled.
// It is present solely to provide Handlers access to the context's values.
// Canceling the context should not affect record processing.
// (Among other things, log messages may be necessary to debug a
// cancellation-related problem.)
//
// Handle methods that produce output should observe the following rules:
// - If r.Time is the zero time, ignore the time.
// - If r.PC is zero, ignore it.
// - If an Attr's key is the empty string and the value is not a group,
// ignore the Attr.
// - If a group's key is empty, inline the group's Attrs.
// - If a group has no Attrs (even if it has a non-empty key),
// ignore it.
Handle(context.Context, Record) error
// WithAttrs returns a new Handler whose attributes consist of
// both the receiver's attributes and the arguments.
// The Handler owns the slice: it may retain, modify or discard it.
// [Logger.With] will resolve the Attrs.
WithAttrs(attrs []Attr) Handler
// WithGroup returns a new Handler with the given group appended to
// the receiver's existing groups.
// The keys of all subsequent attributes, whether added by With or in a
// Record, should be qualified by the sequence of group names.
//
// How this qualification happens is up to the Handler, so long as
// this Handler's attribute keys differ from those of another Handler
// with a different sequence of group names.
//
// A Handler should treat WithGroup as starting a Group of Attrs that ends
// at the end of the log event. That is,
//
// logger.WithGroup("s").LogAttrs(level, msg, slog.Int("a", 1), slog.Int("b", 2))
//
// should behave like
//
// logger.LogAttrs(level, msg, slog.Group("s", slog.Int("a", 1), slog.Int("b", 2)))
//
// If the name is empty, WithGroup returns the receiver.
WithGroup(name string) Handler
}
接口类型的存在,让slog的后端扩展性更强,我们除了可以使用slog提供的两个内置Handler实现:TextHandler和JSONHandler之外,还可以基于第三方log包定义或完全自定义后端Handler的实现。
slog内置两个最常用的Handler:TextHandler和JSONHandler。TextHandler顾名思义,像标准库log包那样将日志以一行文本那样输出;而JSONHandler则是以JSON格式输出log内容与各个属性,我们看一下作者给的例子:
// github.com/bigwhite/experiments/tree/master/slog-examples/demo1/main.go
package main
import (
"net"
"golang.org/x/exp/slog"
)
func main() {
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr)))
slog.Info("hello", "name", "Al")
slog.Error("oops", net.ErrClosed, "status", 500)
slog.LogAttrs(slog.ErrorLevel, "oops",
slog.Int("status", 500), slog.Any("err", net.ErrClosed))
}
//outpu
time=2023-03-30T20:43:24.559+08:00 level=INFO msg=hello name=Al
time=2023-03-30T20:43:24.575+08:00 level=ERROR msg=oops status=500 err="use of closed network connection"
time=2023-03-30T20:43:24.575+08:00 level=ERROR msg=oops status=500 err="use of closed network connection"
将NewTextHandler改成NewJsonHandler:
{"time":"2023-03-30T20:45:22.5032552+08:00","level":"INFO","msg":"hello","name":"Al"}
{"time":"2023-03-30T20:45:22.5204582+08:00","level":"ERROR","msg":"oops","status":500,"err":"use of closed network connection"}
{"time":"2023-03-30T20:45:22.5204582+08:00","level":"ERROR","msg":"oops","status":500,"err":"use of closed network connection"}
去掉SetDefault
2023/03/30 20:46:50 INFO hello name=Al
2023/03/30 20:46:50 ERROR oops status=500 err="use of closed network connection"
2023/03/30 20:46:50 ERROR oops status=500 err="use of closed network connection"
新创建的Logger设置为默认Logger,slog会使用defaultHandler,而defaultHandler的output函数就是log.Output:
slog的前端是“固定格式”的,因此没什么可定制的。但后端这块倒是有不少玩法,接下来我们重点看一下后端。
Handler选项(HandlerOptions)
type HandlerOptions struct {
// When AddSource is true, the handler adds a ("source", "file:line")
// attribute to the output indicating the source code position of the log
// statement. AddSource is false by default to skip the cost of computing
// this information.
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**[需要这份系统化学习资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618317507)**
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**