go语言后端开发学习(四) —— 在go项目中使用Zap日志库

news2024/9/22 15:32:47

一.前言

在之前的文章中我们已经介绍过如何使用logrus包来作为我们在gin框架中使用的日志中间件,而今天我们要介绍的就是我们如何在go项目中如何集成Zap来作为日志中间件

二.Zap的安装与快速使用

和安装其他第三方包没什么区别,我们下载Zap包只需要执行以下命令

go get -u go.uber.org/zap

在Zap的矿方说明中,给出了两种类型的日志记录器——LoggerSugared Logger,在不同场景下我们可以选择使用不同的日志记录器:

Logger:性能比较好,但是仅支持强类型输出的日志,适合在每一微秒和每一次内存分配都很重要的上下文中,使用Logger
Sugared Logger:它支持结构化和printf风格的日志记录。适合在性能很好但不是很关键的上下文中,使用SugaredLogger

接下来我将用两个简单的demo来展示一下我们如何使用这两种日志记录器:

//Logger
package main

import (
	"go.uber.org/zap"
	"net/http"
)

var logger *zap.Logger

func main() {
	InitLogger()
	defer logger.Sync() //等到全部日志写入,将缓冲区中的日志写入磁盘
	SimpleHttpGet("www.baidu.com") //这行代码会报错,仅做展示错误日志信息的打印
	SimpleHttpGet("https://www.kugou.com")
}

func InitLogger() {
	logger, _ = zap.NewProduction()
}

func SimpleHttpGet(url string) {
	re, err := http.Get(url)
	if err != nil {
		logger.Error("Error fetching url", zap.String("url", url), zap.Error(err))
	} else {
		logger.Info("Success fetching url", zap.String("statusCode", re.Status), zap.String("url", url))
	}
	re.Body.Close()
}

//SugarLogger
package main

import (
	"go.uber.org/zap"
	"net/http"
)

var sugarlogger *zap.SugaredLogger

func main() {
	InitLogger()
	defer sugarlogger.Sync() //等到全部日志写入,将缓冲区中的日志写入磁盘
	SimpleHttpGet("www.baidu.com")
	SimpleHttpGet("https://www.kugou.com")
}

func InitLogger() {
	logger, _ := zap.NewProduction() 
	sugarlogger = logger.Sugar()
}

func SimpleHttpGet(url string) {
	re, err := http.Get(url)
	if err != nil {
		sugarlogger.Error("Error fetching url", zap.String("url", url), zap.Error(err))
	} else {
		sugarlogger.Info("Success fetching url", zap.String("statusCode", re.Status), zap.String("url", url))
	}
	re.Body.Close()
}

三.Zap的配置

Zap的使用其实是比较简单的,但是如何去配置出一个适合我们自己项目的日志中间件其实也是比较困难,下面博主将一步步的实现的一个简单的日志中间件示例,下面开始吧!

1.让日志输入到文件中

在我们日常开发模式时,我们一般会将日志的错误信息打印在控制台上,这样可以方便我们去调试错误,但是在生产模式下,让错误信息打印在控制台上无疑是不大可能得了,我们一般会选择将日志信息录入到文件中,接下来让我们来尝试一下修改日志信息的输入路径。

为了修改日志信息的输出路径,我们这里就不会在通过NewProduction来自动创建logger对象了,而是我们自己通过New这一函数来手动传递配置了,在开始之前我们来看一下New这一函数:

func New(core zapcore.Core, options ...Option)

而这里我们所要配置的就是corezapcore.Core需要三个配置:

  • Encoder:它决定了我们以何种形式写入日志,比如我们可以使用Json格式来作为我们书写日志的格式
  • WriteSyncer:它决定了我们要将日志写到什么地方去
  • LogLevel:它决定了哪些级别的日志会被写入到日志文件中去

接下来我们尝试将日志打印在test.log中,并且用Jsontext两种格式来打印到文件中:

package main

import (
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"net/http"
	"os"
)

var sugarlogger *zap.SugaredLogger

func main() {
	InitLogger()
	defer sugarlogger.Sync() //等到全部日志写入,将缓冲区中的日志写入磁盘
	SimpleHttpGet("www.baidu.com")
	SimpleHttpGet("https://www.kugou.com")
}

func InitLogger() {
	encoder := InitEncoder()
	level := InitLevel()
	writer := InitWriter()
	core := zapcore.NewCore(encoder, writer, level)
	sugarlogger = zap.New(core).Sugar()
}

func InitEncoder() zapcore.Encoder {
	return zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
}

func InitWriter() zapcore.WriteSyncer {
	file, _ := os.Create("G:\\bluebell\\src\\demo\\log\\test.log")
	return zapcore.AddSync(file)
}

func InitLevel() zapcore.Level {
	return zapcore.ErrorLevel
}
func SimpleHttpGet(url string) {
	re, err := http.Get(url)
	if err != nil {
		sugarlogger.Error("Error fetching url", zap.String("url", url), zap.Error(err))
	} else {
		sugarlogger.Info("Success fetching url", zap.String("statusCode", re.Status), zap.String("url", url))
	}
	re.Body.Close()
}

运行结果如下:
在这里插入图片描述
我们可以看到文件已经输入到json.log中了。

当然我们也可以使用正常的text格式

package main

import (
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"net/http"
	"os"
)

var sugarlogger *zap.SugaredLogger

func main() {
	InitLogger()
	defer sugarlogger.Sync() //等到全部日志写入,将缓冲区中的日志写入磁盘
	SimpleHttpGet("www.baidu.com")
	SimpleHttpGet("https://www.kugou.com")
}

func InitLogger() {
	encoder := InitEncoder()
	level := InitLevel()
	writer := InitWriter()
	core := zapcore.NewCore(encoder, writer, level)
	sugarlogger = zap.New(core).Sugar()
}

func InitEncoder() zapcore.Encoder {
	return zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig())
}

func InitWriter() zapcore.WriteSyncer {
	file, _ := os.Create("G:\\bluebell\\src\\demo\\log\\text.log")
	return zapcore.AddSync(file)
}

func InitLevel() zapcore.Level {
	return zapcore.ErrorLevel
}
func SimpleHttpGet(url string) {
	re, err := http.Get(url)
	if err != nil {
		sugarlogger.Error("Error fetching url", zap.String("url", url), zap.Error(err))
	} else {
		sugarlogger.Info("Success fetching url", zap.String("statusCode", re.Status), zap.String("url", url))
	}
	re.Body.Close()
}

运行结果:
在这里插入图片描述

2.修改时间编码,将调用函数信息记录在日志中

在上面日志输出中我们可以看到两个比较大的问题:

  • 时间是以非人类可读的方式展示,像1.7233697314262748e+09这样
  • 日志没有调用方的信息我们很难确定错误的位置

所以我们现在要做的就是以下修改:

  • 修改时间编码器
  • 让日志文件中存在调用者信息

首先是修改时间编码器:

func InitEncoder() zapcore.Encoder {
	encoderConfig := zap.NewProductionEncoderConfig()
	encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
	encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder //在日志文件中使用大写字母记录日志级别
	return zapcore.NewConsoleEncoder(encoderConfig)
}

运行结果如下:

2024-08-11T18:14:02.328+0800	INFO	Success fetching url{statusCode 15 0 403 Forbidden <nil>} {url 15 0 https://www.kugou.com <nil>}

最后我们添加将调用函数信息记录到日志中的功能,这里我们需要修改一下代码:

func InitLogger() {
	encoder := InitEncoder()
	level := InitLevel()
	writer := InitWriter()
	core := zapcore.NewCore(encoder, writer, level)
	sugarlogger = zap.New(core,zap.AddCaller()).Sugar()
}

这样我们就能看到调用信息了:

2024-08-11T19:00:52.831+0800	INFO	main/main.go:47	Success fetching url{statusCode 15 0 403 Forbidden <nil>} {url 15 0 https://www.kugou.com <nil>}

拓展:AddCallerSkip
在日志记录过程中,我们通常希望在日志消息中包含准确的调用信息(即,记录日志的代码行)。例如,如果你在 main.go 文件的第 10 行调用了 logger.Info(“Message”),你希望日志记录显示调用发生在 main.go:10。
但是,如果你将日志记录封装到另一个函数中,例如:

func logInfo(msg string) {
    logger.Info(msg)
}

这样返回的值就不是准确的日志记录问题了,因为日志记录的调用栈就会增加一层,因为实际上 logger.Info 是由 logInfo 函数调用的。如果不调整调用栈深度,日志中可能会显示 logInfo 函数的调用位置,而不是实际的日志记录位置。

AddCallerSkip 函数用于调整日志记录库中记录调用信息的调用栈深度。它可以让你指定跳过多少层调用栈,从而准确获取实际的日志记录位置。

所以我们最后的InitLogger函数是这样的:

func InitLogger() {
	encoder := InitEncoder()
	level := InitLevel()
	writer := InitWriter()
	core := zapcore.NewCore(encoder, writer, level)
	sugarlogger = zap.New(core, zap.AddCaller(),zap.AddCallerSkip(1)).Sugar()
}

3.如何将日志输出到多个位置或将特定级别日志输入到单独文件

  • 将日志输出到多个位置
func InitWriter() zapcore.WriteSyncer {
	file, _ := os.Create("G:\\bluebell\\src\\demo\\log\\text.log")
	os := io.MultiWriter(os.Stdout, file)  //既输入到控制台也输入到日志文件中
	return zapcore.AddSync(os)
}
  • 将特定级别日志输入到单独文件(以error为例)
func InitLogger() {
	encoder := InitEncoder()
	level := InitLevel()
	writer := InitWriter()
	c1 := zapcore.NewCore(encoder, writer, level) //记录全部日志
	errF, _ := os.Create("G:\\bluebell\\src\\demo\\log\\err.log")
	c2 := zapcore.NewCore(encoder, zapcore.AddSync(errF), zap.ErrorLevel) //记录错误日志
	core := zapcore.NewTee(c1, c2)                                        // tee将日志输出到多个目的地
	sugarlogger = zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1)).Sugar()
}

以上完整代码如下:

package main

import (
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"io"
	"net/http"
	"os"
)

var sugarlogger *zap.SugaredLogger

func main() {
	InitLogger()
	defer sugarlogger.Sync() //等到全部日志写入,将缓冲区中的日志写入磁盘
	SimpleHttpGet("https://www.kugou.com")
	SimpleHttpGet("www.baidu.com")
}

func InitLogger() {
	encoder := InitEncoder()
	level := InitLevel()
	writer := InitWriter()
	c1 := zapcore.NewCore(encoder, writer, level) //记录全部日志
	errF, _ := os.Create("G:\\bluebell\\src\\demo\\log\\err.log")
	c2 := zapcore.NewCore(encoder, zapcore.AddSync(errF), zap.ErrorLevel) //记录错误日志
	core := zapcore.NewTee(c1, c2)                                        // tee将日志输出到多个目的地
	sugarlogger = zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1)).Sugar()
}

func InitEncoder() zapcore.Encoder {
	encoderConfig := zap.NewProductionEncoderConfig()
	encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
	encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder //在日志文件中使用大写字母记录日志级别
	return zapcore.NewConsoleEncoder(encoderConfig)
}

func InitWriter() zapcore.WriteSyncer {
	file, _ := os.Create("G:\\bluebell\\src\\demo\\log\\text.log")
	os := io.MultiWriter(os.Stdout, file)
	return zapcore.AddSync(os)
}

func InitLevel() zapcore.Level {
	return zapcore.DebugLevel
}
func SimpleHttpGet(url string) {
	re, err := http.Get(url)
	if err != nil {
		sugarlogger.Error("Error fetching url", zap.String("url", url), zap.Error(err))
	} else {
		sugarlogger.Info("Success fetching url", zap.String("statusCode", re.Status), zap.String("url", url))
	}
	re.Body.Close()
}

四.Zap实现日志分割

日志切割可以使用Lumberjack这一第三方包,可以按照下面这个命令下载:

go get gopkg.in/natefinch/lumberjack.v2

最后我们来开一下怎么加入支持:

func InitWriter() zapcore.WriteSyncer {
	lumberjackLogger := &lumberjack.Logger{
		Filename:   "G:\\bluebell\\src\\demo\\log\\app.log", //日志文件路径
		MaxSize:    1,                                       //每个日志文件保存的最大尺寸 单位:MB
		MaxBackups: 5,                                       //最多保存多少个日志文件
		MaxAge:     30,                                      //日志文件最多保存多少天
		Compress:   false,                                   //是否压缩
	}
	return zapcore.AddSync(lumberjackLogger)
}

这样就实现了一个简单的日志分割了。

五.在gin框架中集成Zap日志库

在很早之前博主就写过gin.Default会调用Logger(), Recovery()这两个中间件,,所我们想在gin框架中集成Zap日志库只需要重写一下这两个中间件就可以了:

func GinLogger() gin.HandlerFunc {
	return func(c *gin.Context) {
		start := time.Now()
		path := c.Request.URL.Path
		query := c.Request.URL.RawQuery
		c.Next()
		cost := time.Since(start)
		logger.Info(path,
			zap.Int("status", c.Writer.Status()),
			zap.String("method", c.Request.Method),
			zap.String("path", path),
			zap.String("query", query),
			zap.String("ip", c.ClientIP()),
			zap.String("user-agent", c.Request.UserAgent()),
			zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()),
			zap.Duration("cost", cost),
		)
	}
}

// GinRecovery recover掉项目可能出现的panic
func GinRecovery(stack bool) gin.HandlerFunc {
	return func(c *gin.Context) {
		defer func() {
			if err := recover(); err != nil {
				// Check for a broken connection, as it is not really a
				// condition that warrants a panic stack trace.
				var brokenPipe bool
				if ne, ok := err.(*net.OpError); ok {
					if se, ok := ne.Err.(*os.SyscallError); ok {
						if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
							brokenPipe = true
						}
					}
				}

				httpRequest, _ := httputil.DumpRequest(c.Request, false)
				if brokenPipe {
					logger.Error(c.Request.URL.Path,
						zap.Any("error", err),
						zap.String("request", string(httpRequest)),
					)
					// If the connection is dead, we can't write a status to it.
					c.Error(err.(error)) // nolint: err check
					c.Abort()
					return
				}

				if stack {
					logger.Error("[Recovery from panic]",
						zap.Any("error", err),
						zap.String("request", string(httpRequest)),
						zap.String("stack", string(debug.Stack())),
					)
				} else {
					logger.Error("[Recovery from panic]",
						zap.Any("error", err),
						zap.String("request", string(httpRequest)),
					)
				}
				c.AbortWithStatus(http.StatusInternalServerError)
			}
		}()
		c.Next()
	}
}

最后得到的就是我们的最终log文件代码:

package main

import (
	"github.com/gin-gonic/gin"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"gopkg.in/natefinch/lumberjack.v2"
	"net"
	"net/http"
	"net/http/httputil"
	"os"
	"runtime/debug"
	"strings"
	"time"
)

var logger *zap.Logger

func main() {
	r := gin.New()
	r.Use(GinLogger(), GinRecovery(true))
}

func GinLogger() gin.HandlerFunc {
	return func(c *gin.Context) {
		start := time.Now()
		path := c.Request.URL.Path
		query := c.Request.URL.RawQuery
		c.Next()
		cost := time.Since(start)
		logger.Info(path,
			zap.Int("status", c.Writer.Status()),
			zap.String("method", c.Request.Method),
			zap.String("path", path),
			zap.String("query", query),
			zap.String("ip", c.ClientIP()),
			zap.String("user-agent", c.Request.UserAgent()),
			zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()),
			zap.Duration("cost", cost),
		)
	}
}

// GinRecovery recover掉项目可能出现的panic
func GinRecovery(stack bool) gin.HandlerFunc {
	return func(c *gin.Context) {
		defer func() {
			if err := recover(); err != nil {
				// Check for a broken connection, as it is not really a
				// condition that warrants a panic stack trace.
				var brokenPipe bool
				if ne, ok := err.(*net.OpError); ok {
					if se, ok := ne.Err.(*os.SyscallError); ok {
						if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
							brokenPipe = true
						}
					}
				}

				httpRequest, _ := httputil.DumpRequest(c.Request, false)
				if brokenPipe {
					logger.Error(c.Request.URL.Path,
						zap.Any("error", err),
						zap.String("request", string(httpRequest)),
					)
					// If the connection is dead, we can't write a status to it.
					c.Error(err.(error)) // nolint: err check
					c.Abort()
					return
				}

				if stack {
					logger.Error("[Recovery from panic]",
						zap.Any("error", err),
						zap.String("request", string(httpRequest)),
						zap.String("stack", string(debug.Stack())),
					)
				} else {
					logger.Error("[Recovery from panic]",
						zap.Any("error", err),
						zap.String("request", string(httpRequest)),
					)
				}
				c.AbortWithStatus(http.StatusInternalServerError)
			}
		}()
		c.Next()
	}
}

func InitLogger() {
	encoder := InitEncoder()
	level := InitLevel()
	writer := InitWriter()
	c1 := zapcore.NewCore(encoder, writer, level) //记录全部日志
	errF, _ := os.Create("G:\\bluebell\\src\\demo\\log\\err.log")
	c2 := zapcore.NewCore(encoder, zapcore.AddSync(errF), zap.ErrorLevel) //记录错误日志
	core := zapcore.NewTee(c1, c2)                                        // tee将日志输出到多个目的地
	logger = zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1))
}

func InitEncoder() zapcore.Encoder {
	encoderConfig := zap.NewProductionEncoderConfig()
	encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
	encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder //在日志文件中使用大写字母记录日志级别
	return zapcore.NewConsoleEncoder(encoderConfig)
}

func InitWriter() zapcore.WriteSyncer {
	lumberjackLogger := &lumberjack.Logger{
		Filename:   "G:\\bluebell\\src\\demo\\log\\app.log", //日志文件路径
		MaxSize:    1,                                       //每个日志文件保存的最大尺寸 单位:MB
		MaxBackups: 5,                                       //最多保存多少个日志文件
		MaxAge:     30,                                      //日志文件最多保存多少天
		Compress:   false,                                   //是否压缩
	}
	return zapcore.AddSync(lumberjackLogger)
}

func InitLevel() zapcore.Level {
	return zapcore.DebugLevel
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2043283.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Github Actions自动发布release

目录 说明正文1.设置仓库密钥2.打开仓库权限3.配置自动化文件4.写在最后 说明 GitHub Actions 是 GitHub 的持续集成服务&#xff0c;于2018年10月推出。通过Github Actions可以实现诸多自动化功能&#xff0c;比如自动打包&#xff0c;自动发布Release等等。除此之外&#xf…

实验10-3 递归计算Ackermenn函数

本题要求实现Ackermenn函数的计算&#xff0c;其函数定义如下&#xff1a; 函数接口定义&#xff1a; int Ack( int m, int n ); 其中m和n是用户传入的非负整数。函数Ack返回Ackermenn函数的相应值。题目保证输入输出都在长整型范围内。 输入样例&#xff1a; 2 3输出样例&am…

8 个最佳 Java IDE 和文本编辑器

从 2024 年使用的最佳 Java IDE 和代码编辑器中进行选择&#xff0c;并提高您的 Java 生产力。 Java 是世界上最流行的编程语言之一&#xff0c;于 1995 年首次推出&#xff0c;它确实践行了“编写一个&#xff0c;随处运行”的座右铭。该语言用途广泛&#xff0c;可用于构建从…

浅谈物理集中式数据中台架构在数据开发中的不足

在传统数据开发和生产领域&#xff0c;企业面临的问题主要源于数据规模的急剧增长&#xff0c;多源异构数据的不断扩张&#xff0c;以及数据消费端看数、用数的人员越来越多。初期&#xff0c;数据仓库的主要功能聚焦于支持经营仪表盘的视图构建&#xff0c;旨在为企业提供直观…

ubuntu:最新安装使用docker

前言 系统&#xff1a;ubuntu 22.04 desktop 目的&#xff1a;安装使用docker 安装小猫猫 没有安装包的&#xff0c;可以自己去瞅瞅&#xff0c;这里不提供下载方式 sudo dpkg -i ./cat-verge_1.7.5_amd64.deb 在应用里&#xff0c;打开这个软件&#xff0c;并开启系统猫猫 配…

单元训练08:外部中断的基本操作

蓝桥杯 小蜜蜂 单元训练08&#xff1a;外部中断的基本操作 #include "stc15f2k60s2.h"#define LED(x) \{ \P0 x; \P2 P2 & 0x1f | 0x80; \P2 & 0x1f; \}#define L1 0xFE; // 定义L1亮…

SpringBoot集成MyBatis和FlyWay

一、什么是FlyWay 一个开源的数据库迁移工具&#xff0c;用于管理和执行数据库结构的版本变更。通俗来说&#xff0c;它帮助开发者跟踪和应用数据库中的更改&#xff0c;比如表的创建、列的修改等。主要的功能为&#xff1a; 数据库版本控制&#xff1a; Flyway 使用一组迁移…

硬件I2C和软件I2C(模拟I2C)的区别

硬件I2C和软件I2C是两种不同的实现I2C&#xff08;Inter-Integrated Circuit&#xff0c;集成电路间&#xff09;通信协议的方式&#xff0c;它们在实现方式、性能特点以及应用场景上存在显著差异。 一、实现方式 硬件I2C&#xff1a;通过专门的硬件电路实现&#xff0c;这些…

泛交通领域的可视化大屏作品欣赏,各个都相当惊艳。

各位新老朋友大家好&#xff0c;本次给大家带来泛交通领域的可视化大屏&#xff0c;供大家鉴赏。 泛交通领域是指综合利用各种交通方式和资源&#xff0c;提供全方位、多元化的出行选择和服务的交通体系。 它包括以下几个方面&#xff1a; 1. 公共交通&#xff1a;包括地铁、…

花钱买不到系列之—linux系统调用

关于系统调用是什么&#xff0c;为什么用系统调用? 也是通过生活的例子来说明白。 我们生活中有一种东西叫银行&#xff0c;银行是不是有存钱的仓库对不对&#xff1f;好银行有存钱的仓库&#xff0c;银行有桌椅板凳啊&#xff0c;银行还有电脑&#xff0c;设备啊&#xff0c;…

文华财经期货DK多空提示指标源码

N1:40; A:(COHL)/4; AA0:MA(A,N1),LINETHICK3;//中 MA1:MA(CLOSE,5), NODRAW; MA2:MA(CLOSE,10), NODRAW; MA3:MA(C,60), NODRAW,LINETHICK1; 转折线:MA3, NODRAW,COLORCYAN; 顺势线:MA(CLOSE,10), NODRAW; MA20:MA(C,20), NODRAW; MA30:MA(C,30), NODRAW; ZD:MA3>…

网络 基础

目录 1、协议&#xff1a; 2、OSI 七层 模型&#xff1a; 3、TCP/IP 五层 / 四层 协议 3.1、为什么要有TCP / IP 协议&#xff1f; 3.1.1、主机之间变远产生的问题&#xff1a; 3.1.2、TCP/IP协议于操作系统的关系 4、局域网 4.1、Mac 4.1.1 在Linux内使用指令 …

leetCode - - - 哈希表

目录 1.模拟行走机器人&#xff08;LeetCode 874&#xff09; 2.数组的度&#xff08;LeetCode 697&#xff09; 3.子域名访问次数&#xff08;LeetCode 811&#xff09; 4.字母异位词分组&#xff08;LeetCode 49&#xff09; 5.小结 1.常见的哈希表实现 2.遍历Map 1.模…

基于Java中的SSM框架实现医院收费系统项目【项目源码+论文说明】计算机毕业设计

基于Java中的SSM框架实现医院收费系统演示 摘要 随着医疗体制改革的不断深入&#xff0c;医院收费系统成为医院信息化建设的重点内容。医院收费系统是利用计算机、网络技术和数据库技术&#xff0c;实现病人在医疗机构的诊疗信息的电子化存储、传递和分析&#xff0c;从而提高…

[Meachines] [Medium] Mango PHP弱比较绕过+MongoDB注入+TRP00F自动化权限提升+JJS权限提升

信息收集 IP AddressOpening Ports10.10.10.162TCP:22&#xff0c;80&#xff0c;443 $ nmap -p- 10.10.10.162 --min-rate 1000 -sC -sV PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0) | ssh-host…

<数据集>快递识别数据集<目标检测>

数据集格式&#xff1a;VOCYOLO格式 图片数量&#xff1a;5382张 标注数量(xml文件个数)&#xff1a;5382 标注数量(txt文件个数)&#xff1a;5382 标注类别数&#xff1a;1 标注类别名称&#xff1a;[Box-Packet] 序号类别名称图片数框数1Box-Packet53828965 使用标注工…

8.15-配置mysql5.7环境+使用python管理数据库+使用中间件mycat配置读写分离

一、配置mysql5.7的环境 1.基础配置 # 将mysql5.7的包拖入xshell [rootmysql_5 ~]# ls anaconda-ks.cfg mysql-5.7.44-linux-glibc2.12-x86_64.tar.gz ​ # 解压 [rootmysql_5 ~]# tar -xf mysql-5.7.44-linux-glibc2.12-x86_64.tar.gz ​ # 备份文件 [rootmysql_5 ~]# cp…

短说V4.2.0测试版发布|字体更换、小名片及个人中心UI更换

Hi 大家好&#xff0c; 我是给你们带来惊喜的运营小番茄。 本期更新为短说V4.2.0测试版&#xff0c;本次更新涉及平台有H5、App、微信小程序。 4.2.0版本除功能优化外&#xff0c;新增了如下功能&#xff1a; 一、新增功能 通用版&#xff1a; ①全站默认字体全部更换为…

淘宝到一个墨水屏,成功实现显示经历记录

一&#xff0c;淘一个墨水屏的原因 在一些小的PCB设计和编程中发现&#xff0c;许多程序控制运行情况如果能够显示出来&#xff0c;会很完美。大学时期使用LCD1602&#xff08;经典&#xff09;显示了一个称重传感器的课程设计&#xff0c;后来尝试OLED显示。在过程中发现墨水…

【嵌入式linux开发】智能家居入门5(QT、微信小程序、HTTP协议、ONENET云平台、旭日x3派)

智能家居入门5&#xff08;QT、微信小程序、HTTP协议、ONENET云平台、旭日x3派&#xff09; 前言一、QT界面设计二、云平台产品创建与连接三、下位机端QT代码总览&#xff1a;四、微信小程序端代码总览五、板端测试 前言 前四篇智能家居相关文章都是使用STM32作为主控&#xf…