Gin框架学习笔记(六)——gin中的日志使用

news2024/11/19 19:39:11

gin内置日志组件的使用

前言

在之前我们要使用Gin框架定义路由的时候我们一般会使用Default方法来实现,我们来看一下他的实现:


func Default(opts ...OptionFunc) *Engine {
	debugPrintWARNINGDefault()
	engine := New()
	engine.Use(Logger(), Recovery())
	return engine.With(opts...)
}

我们可以看到它注册了两个中间件Logger()Recovery(),而Logger就是我们今天的主角:gin框架自带的日志组件。

输出日志到文件中

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"io"
	"os"
)

func main() {
	file, err := os.Create("ginlog")
	if err != nil {
		fmt.Println("Create file error! err:", err)
	}
	gin.DefaultWriter = io.MultiWriter(file)
	r := gin.Default()
	r.GET("/", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "Hello World!",
		})
	})
	r.Run()
}

运行上面代码我们会发现,控制台不再会有相关日志的输出,而是打印到了ginlog文件中:

[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:	export GIN_MODE=release
 - using code:	gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /                         --> main.main.func1 (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080

当然我们也可以选择既在控制台输出也在文件内输出:

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"io"
	"os"
)

func main() {
	file, err := os.Create("ginlog")
	if err != nil {
		fmt.Println("Create file error! err:", err)
	}
	gin.DefaultWriter = io.MultiWriter(file, os.Stdout)
	r := gin.Default()
	r.GET("/", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "Hello World!",
		})
	})
	r.Run()
}

在这里插入图片描述
我们可以看到无论是日志文件ginlog和控制台,都实现了对日志的打印

定义日志中的路由格式

当我们运行Gin框架的时候,它会自动打印当前所有被定义的路由,比如下面这样的格式:

[GIN-debug] GET    /                         --> main.main.func1 (3 handlers)

而在Gin框架中它允许我们去自己定义路由的输出格式,我们可以自己去定义我们的路由格式:

func _Router_print_init() {
	gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string,
		nuHandlers int) {
		fmt.Printf("[三玖]: %v  %v   %v   %v  \n",
			httpMethod, absolutePath, handlerName, nuHandlers)
	}
}

输出的路由格式是这样的:

[三玖]: GET  /   main.main.func1   3

生产模式与开发模式

在我们程序其实是有两种模式的:

  • debug:开发模式
  • release:生产模式

如果我们希望控制台不在显示日志,可以将模式切换到release模式:

	gin.SetMode(gin.ReleaseMode)
	r := gin.Default()

在这里插入图片描述
我们可以看到控制台已不再输出日志信息了。

第三方包logrus日志包的使用

logrus包的安装与基本使用

logrus包的安装

logrus的安装很简单,只需要终端输入以下命令即可:

go get github.com/sirupsen/logrus

logrus包的基本使用

logrus常用方法:
	logrus.Debug("debug")
	logrus.Info("info")
	logrus.Warn("warn")
	logrus.Error("error")
	logrus.Println("println")

当我们运行该代码的时候会发现打印结果只有四行:
在这里插入图片描述
这主要是因为logrus默认的打印等级是info,在这个等级之下的不会打印,在我们生产环境下一般会要求不打印Warn以下的日志,我们可以对打印等级进行调整:

logrus.SetLevel(logrus.WarnLevel)

再次运行上面的代码,运行结果就会有所不同:
在这里插入图片描述
我们还可以查看当前的打印等级:

fmt.Println(logrus.GetLevel())

设置特定字段

如果我们希望某条日志记录的打印中添加某一条特定的字段,我们可以使用WithField方法:

log1 := logrus.WithField("key1", "value1")
log1.Info("hello world")

通常,在一个应用中、或者应用的一部分中,都有一些固定的Field。比如我们在处理用户http请求时,上下文中,所有的日志都会有request_id和user_ip为了避免每次记录日志都要使用log.WithFields(log.Fields{“request_id”: request_id, “user_ip”: user_ip}),我们可以创建一个logrus.Entry实例,为这个实例设置默认Fields,在上下文中使用这个logrus.Entry实例记录日志即可,这里我写了一个demo,仅做参考:

package main

import (
	"github.com/sirupsen/logrus"
)

type DefaultLogger struct {
	*logrus.Entry
	defaultFields logrus.Fields
}

func NewDefaultLogger() *DefaultLogger {
	logger := logrus.New()
	entry := logrus.NewEntry(logger)
	return &DefaultLogger{
		Entry:         entry,
		defaultFields: logrus.Fields{},
	}
}

func (l *DefaultLogger) WithFields(fields logrus.Fields) *logrus.Entry {
	allFields := make(logrus.Fields, len(fields))
	for k, v := range fields {
		allFields[k] = v
	}
	return l.Entry.WithFields(allFields)
}

func (l *DefaultLogger) WithDefaultField() {
	l.Entry = l.Entry.WithFields(l.defaultFields)
}

func (l *DefaultLogger) Info(msg string) {
	l.WithDefaultField()
	l.Entry.Info(msg)
}

func (l *DefaultLogger) AddDefaultField(key string, value interface{}) {
	l.defaultFields[key] = value
}

func main() {
	defaultLogger := NewDefaultLogger()
	defaultLogger.AddDefaultField("request_id", "123")
	defaultLogger.AddDefaultField("user_ip", "127.0.0.1")

	// 使用默认字段记录日志
	defaultLogger.Info("This is a log message with default fields")

	// 添加额外字段记录日志
	defaultLogger.WithFields(logrus.Fields{
		"additional_field": "abc",
	}).Info("This is a log message with additional field")

}

输出结果为:
在这里插入图片描述

设置显示样式

虽然日志的打印默认是txt格式的,但是我们也可以将格式修改为json格式的:

logrus.SetFormatter(&logrus.TextFormatter{})

将日志输入到文件

package main

import (
	"github.com/sirupsen/logrus"
	"os"
)

func main() {
	file, err := os.OpenFile("./logrus.log", os.O_CREATE|os.O_WRONLY, 0666)
	if err != nil {
		panic(err)
	}
	logrus.SetOutput(file)
	logrus.Error("error")
}

我们还可以让控制台和日志文件一起输出:

package main

import (
	"github.com/sirupsen/logrus"
	"golang.org/x/sys/windows"
	"io"
	"os"
)

func main() {
	file, err := os.OpenFile("./logrus.log", os.O_CREATE|os.O_WRONLY|windows.O_APPEND, 0666)
	if err != nil {
		panic(err)
	}
	writers := []io.Writer{
		file,
		os.Stdout,
	}
	lod := io.MultiWriter(writers...)
	logrus.SetOutput(lod)
	logrus.Error("error")
	logrus.Info("info")
}

显示行号

logrus.SetReportCaller(true)

logus的Hook机制

在使用logrus这一第三方包的时候,我们可以基于Hook机制来为logrus添加一些拓展功能。

首先我们先定义Hook结构体:

type Hook struct {
	Levels() []logrus.Level  // 返回日志级别
	Fire(entry *logrus.Entry) error  // 日志处理
}

我们Hook结构体中一般会有两个成员:

  • Levels:Hook机制起作用的日志级别
  • Fire:对应的日志处理方式

这里我们举一个例子,如果我们希望将所有Error级别的日志单独拎出来,我们可以基于Hook机制来实现:

package main

import (
	"fmt"
	"github.com/sirupsen/logrus"
	"os"
)

type Hook struct {
	Writer *os.File
}

func (MyHook *Hook) Fire(entry *logrus.Entry) error {
	line, err := entry.String()
	if err != nil {
		fmt.Fprintf(os.Stderr, "Failed to write to log, %v\n", err)
	}
	MyHook.Writer.Write([]byte(line))
	return nil
}

func (MyHook *Hook) Levels() []logrus.Level {
	return []logrus.Level{
		logrus.ErrorLevel,
	}
}

func main() {
	logrus.SetFormatter(&logrus.TextFormatter{ForceColors: true, TimestampFormat: "2006-01-02 15:04:05", FullTimestamp: true})
	logrus.SetReportCaller(true)
	file, _ := os.OpenFile("./error.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0666)
	hook := &Hook{Writer: file}
	logrus.AddHook(hook)
	logrus.Error("error")
}

日志分割

按时间分割

  • Write写法
package main

import (
	"fmt"
	"github.com/sirupsen/logrus"
	"io"
	"os"
	"path/filepath"
	"strings"
	"time"
)

type LogFormatter struct{}

// Format 格式详情
func (s *LogFormatter) Format(entry *logrus.Entry) ([]byte, error) {
	timestamp := time.Now().Local().Format("2006-01-02 15:04:05")
	var file string
	var len int
	if entry.Caller != nil {
		file = filepath.Base(entry.Caller.File)
		len = entry.Caller.Line
	}
	//fmt.Println(entry.Data)
	msg := fmt.Sprintf("[%s] %s [%s:%d] %s\n", strings.ToUpper(entry.Level.String()), timestamp, file, len, entry.Message)
	return []byte(msg), nil
}

type LogWriter struct {
	Writer   *os.File
	logPath  string
	fileDate string //判断是否需要切换日志文件
	fileName string //日志文件名
}

func (writer *LogWriter) Write(p []byte) (n int, err error) {
	if writer == nil {
		logrus.Error("writer is nil")
		return 0, nil
	}
	if writer.Writer == nil {
		logrus.Error("writer.Writer is nil")
		return 0, nil
	}
	timer := time.Now().Format("2006-01-02 04:12")

	//需要切换日志文件
	if writer.fileDate != timer {
		writer.fileDate = timer
		writer.Writer.Close()
		err = os.MkdirAll(writer.logPath, os.ModePerm)
		if err != nil {
			logrus.Error(err)
			return 0, nil
		}
		filename := fmt.Sprintf("%s/%s.log", writer.logPath, writer.fileDate)
		writer.Writer, err = os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666)
		if err != nil {
			logrus.Error(err)
			return 0, nil
		}
	}
	return writer.Writer.Write(p)
}

func Initing(logPath string, fileName string) {
	fileDate := time.Now().Format("20060102")
	filepath := fmt.Sprintf("%s/%s", logPath, fileDate)
	err := os.MkdirAll(filepath, os.ModePerm)
	if err != nil {
		logrus.Error(err)
		return
	}

	filename := fmt.Sprintf("%s/%s.log", filepath, fileName)
	writer, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666)
	if err != nil {
		logrus.Error(err)
		return
	}

	Logwriter := LogWriter{logPath: logPath, fileDate: fileDate, fileName: fileName, Writer: writer}
	logrus.SetOutput(os.Stdout)
	writers := []io.Writer{
		Logwriter.Writer,
		os.Stdout,
	}
	multiWriter := io.MultiWriter(writers...)
	logrus.SetOutput(multiWriter)
	logrus.SetReportCaller(true)
	logrus.SetFormatter(new(LogFormatter))
}

func main() {
	Initing("./", "fengxu")
	logrus.Warn("fengxu")
	logrus.Error("fengxu")
	logrus.Info("fengxu")

}
  • Hook写法
package main

import (
	"fmt"
	"github.com/sirupsen/logrus"
	"os"
	"time"
)

type Hook struct {
	writer   *os.File
	logPath  string
	fileName string
	fileDate string
}

func (MyHook *Hook) Levels() []logrus.Level {
	return logrus.AllLevels
}

func (MyHook *Hook) Fire(entry *logrus.Entry) error {
	timer := time.Now().Format("2006-01-02")
	line, _ := entry.String()
	//需要切换日志文件
	if MyHook.fileDate != timer {
		MyHook.fileDate = timer
		MyHook.writer.Close()
		filepath := fmt.Sprintf("%s/%s", MyHook.logPath, MyHook.fileDate)
		err := os.MkdirAll(filepath, os.ModePerm)
		if err != nil {
			logrus.Error(err)
			return err
		}

		filename := fmt.Sprintf("%s/%s.log", filepath, MyHook.fileName)
		MyHook.writer, _ = os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666)
	}
	MyHook.writer.Write([]byte(line))
	return nil
}

func InitFile(logPath string, fileName string) {
	timer := time.Now().Format("2006-01-02")
	filepath := fmt.Sprintf("%s/%s", logPath, timer)
	err := os.MkdirAll(filepath, os.ModePerm)
	if err != nil {
		logrus.Error(err)
		return
	}

	filename := fmt.Sprintf("%s/%s.log", filepath, fileName)
	writer, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666)
	if err != nil {
		logrus.Error(err)
		return
	}
	logrus.AddHook(&Hook{
		writer:   writer,
		logPath:  logPath,
		fileName: fileName,
		fileDate: timer,
	})
}

func main() {
	InitFile("./log", "fengxu")
	logrus.Error("test")

}

按日志等级分割

package main

import (
	"fmt"
	"github.com/sirupsen/logrus"
	"os"
)

const (
	alllog   = "all"
	errorlog = "error"
	warnlog  = "warn"
)

type Hook struct {
	allLevel   *os.File
	errorLevel *os.File
	warnLevel  *os.File
}

func (MyHook *Hook) Levels() []logrus.Level {
	return logrus.AllLevels
}

func (MyHook *Hook) Fire(entry *logrus.Entry) error {
	line, _ := entry.String()
	switch entry.Level {
	case logrus.ErrorLevel:
		MyHook.errorLevel.Write([]byte(line))
	case logrus.WarnLevel:
		MyHook.warnLevel.Write([]byte(line))
	}
	MyHook.allLevel.Write([]byte(line))
	return nil
}

func InitLevel(logPath string) {
	err := os.MkdirAll(logPath, os.ModePerm)
	if err != nil {
		logrus.Error("创建目录失败")
		return
	}
	allFile, err := os.OpenFile((fmt.Sprintf("%s/%s", logPath, alllog)), os.O_CREATE|os.O_APPEND|os.O_RDWR, 0600)
	errFile, err := os.OpenFile((fmt.Sprintf("%s/%s", logPath, errorlog)), os.O_CREATE|os.O_APPEND|os.O_RDWR, 0600)
	warnFile, err := os.OpenFile((fmt.Sprintf("%s/%s", logPath, warnlog)), os.O_CREATE|os.O_APPEND|os.O_RDWR, 0600)
	logrus.AddHook(&Hook{allLevel: allFile, errorLevel: errFile, warnLevel: warnFile})
}

func main() {
	InitLevel("./log")
	logrus.SetReportCaller(true)
	logrus.Errorln("你好")
	logrus.Errorln("err")
	logrus.Warnln("warn")
	logrus.Infof("info")
	logrus.Println("print")

}

gin集成logrus

  • main函数(main.go)
package main

import (
	"gin/Logger/gin/gin_logrus/log"
	"gin/Logger/gin/gin_logrus/middleware"
	"github.com/gin-gonic/gin"
)

func main() {
	log.InitFile("./log", "fengxu")
	r := gin.New()
	r.Use(middleware.Logmiddleware())
	r.GET("/", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "pong",
		})
	})
	r.Run(":8080")
}

  • log.go
package log

import (
	"bytes"
	"fmt"
	"github.com/sirupsen/logrus"
	"os"
	"time"
)

type Hook struct {
	writer   *os.File
	logPath  string
	fileName string
	fileDate string
}

func (MyHook *Hook) Levels() []logrus.Level {
	return logrus.AllLevels
}

func (MyHook *Hook) Fire(entry *logrus.Entry) error {
	timer := time.Now().Format("2006-01-02")
	line, _ := entry.String()
	//需要切换日志文件
	if MyHook.fileDate != timer {
		MyHook.fileDate = timer
		MyHook.writer.Close()
		filepath := fmt.Sprintf("%s/%s", MyHook.logPath, MyHook.fileDate)
		err := os.MkdirAll(filepath, os.ModePerm)
		if err != nil {
			logrus.Error(err)
			return err
		}

		filename := fmt.Sprintf("%s/%s.log", filepath, MyHook.fileName)
		MyHook.writer, _ = os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666)
	}
	MyHook.writer.Write([]byte(line))
	return nil
}

type LogFormat struct {
}

func (l *LogFormat) Format(entry *logrus.Entry) ([]byte, error) {
	var buff *bytes.Buffer
	if entry.Buffer != nil {
		buff = entry.Buffer
	} else {
		buff = &bytes.Buffer{}
	}
	_, _ = fmt.Fprintf(buff, "%s\n", entry.Message) //这里可以自己去设置输出格式
	return buff.Bytes(), nil
}

func InitFile(logPath string, fileName string) {
	logrus.SetFormatter(&LogFormat{})
	timer := time.Now().Format("2006-01-02")
	filepath := fmt.Sprintf("%s/%s", logPath, timer)
	err := os.MkdirAll(filepath, os.ModePerm)
	if err != nil {
		logrus.Error(err)
		return
	}

	filename := fmt.Sprintf("%s/%s.log", filepath, fileName)
	writer, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666)
	if err != nil {
		logrus.Error(err)
		return
	}
	logrus.AddHook(&Hook{
		writer:   writer,
		logPath:  logPath,
		fileName: fileName,
		fileDate: timer,
	})
}

  • 中间件(lmiddleware.go)
package middleware

import (
	"github.com/gin-gonic/gin"
	"github.com/sirupsen/logrus"
	"time"
)

const ( //自定义状态码和方法的显示颜色
	status200 = 42
	status404 = 43
	status500 = 41
	methodGET = 44
)

func Logmiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		start := time.Now()
		path := c.Request.URL.Path
		raw := c.Request.URL.RawQuery
		if raw != "" {
			path = path + "?" + raw
		}

		c.Next() //执行其他中间件

		//end := time.Now()
		//timesub := end.Sub(start)  //响应所需时间
		//ClientIp := c.ClientIP()  //客户端ip
		statuscode := c.Writer.Status()
		//var statusColor string  
		//switch c.Writer.Status() {
		//case 200:
		//	statusColor = fmt.Sprintf("\033[%dm%d\033[0m", status200, statuscode)
		//case 404:
		//	statusColor = fmt.Sprintf("\033[%dm%d\033[0m", status404, statuscode)
		//default:
		//	statusColor = fmt.Sprintf("\033[%dm%d\033[0m", status500, statuscode)
		//}
		//
		//var methodColor string
		//switch c.Request.Method {
		//case "GET":
		//	methodColor = fmt.Sprintf("\033[%dm%s\033[0m", methodGET, c.Request.Method)
		//}

		logrus.Infof("[GIN] %s  |%d  |%s  |%s",
			start.Format("2006-01-02 15:04:06"),
			statuscode,
			c.Request.Method,
			path,
		)

	}
}

项目结构:
在这里插入图片描述

结语

至此我们对Gin框架的简单学习就到此为止了,更多的学习大家可以前去查看Gin框架官方文档:
Gin框架官方文档

后面就要开始对Gorm的学习了,下篇见!

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

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

相关文章

datasheet芯片数据手册—新手入门学习(二)【8-18】

参考芯片手册已经上传,可自行下载 因为芯片参考手册内容比较多,故再一次介绍本文内容主要讲解章节。 目录 8、内容介绍 命令真值表 9、Command Definitions 10、READ Operations (1)页面读取操作 (2&#xff…

Orangepi Zero2 linux系统摄像头设备文件名固定

文章目录 1. 寻找设备规则2. 使用udev规则修改挂载设备文件名称 问题: 在多次插拔usb摄像头或者在使用中不小心碰到或松了会导致设备文件名称变化,如从/dev/video1和/dev/video2变为/dev/video2和/dev/video3, 所以每次发生变化后都要充型修改代码或者重…

2024.5组队学习——MetaGPT(0.8.1)智能体理论与实战(下):多智能体开发

传送门: 《2024.5组队学习——MetaGPT(0.8.1)智能体理论与实战(上):MetaGPT安装、单智能体开发》《2024.5组队学习——MetaGPT(0.8.1)智能体理论与实战(中)&…

高校网站群及融媒体中心建设方案

一、项目背景 随着信息技术的飞速发展,互联网已成为高校展示形象、传播信息、服务师生、沟通社会的重要渠道。然而,目前许多高校在网站建设和媒体传播方面存在以下问题: 网站分散、缺乏统一规划:各高校内部往往存在多个部门或学院…

移动云——让每个人都能享受云技术的魅力

一、引言 云技术的起源可以追溯到20世纪60年代和70年代,随着科技的发展,现在早就和所有人息息相关。在云技术的浪潮中,有这么一家厂商通过自己的努力,深耕云计算市场,不仅有各种各样的产品为开发者提供服务&#xff0…

E1载波:一种2.048Mbps速率的PCM载波

E1载波的基本帧由32个子信道组成 帧长为256个bit,分为32个相等时隙,一个时隙为8个bit。256/328 时隙的编号为CH0~CH31 全帧包含256位,且每一帧用 125us时间传送 E1载波支持的数据传输效率为2.048Mbps,用PCM编码(即 256bit/125us2.048Mbps…

融媒宝:群发自媒体平台的神器,注册送7天中级会员

近几年自媒体比较火,做自媒体往往需要发布文章或视频到多个平台,如手工复制粘贴逐一发布,委实费时费力、效率不高。今天就给大家分享一款提高自媒体运营效率的神器--融媒宝: 融媒宝简介 融媒宝是一款可免费使用的高效自媒体工具…

axios如何传递数组作为参数,后端又如何接收呢????

前端的参数是一个数组。 前端编写: 后端接收:

“大数据建模、分析、挖掘技术应用研修班”的通知!

随着2015年9月国务院发布了《关于印发促进大数据发展行动纲要的通知》,各类型数据呈现出了指数级增长,数据成了每个组织的命脉。今天所产生的数据比过去几年所产生的数据大好几个数量级,企业有了能够轻松访问和分析数据以提高性能的新机会&am…

力扣刷题---3146. 两个字符串的排列差

题目描述 给你两个字符串 s 和 t,每个字符串中的字符都不重复,且 t 是 s 的一个排列。 排列差 定义为 s 和 t 中每个字符在两个字符串中位置的绝对差值之和。 返回 s 和 t 之间的 排列差 。 示例 1: 输入:s “abc”, t “b…

新手做视频号小店,常见问题解答,看懂就明白做店逻辑了!

大家好,我是电商糖果 商家做视频号小店前期一定会反复咨询一些问题。 因为他们要提前做好调查,以防店铺运营的过程中出现问题。 糖果因为经常在网上分享自己做店的经验,所以就有很多朋友前来咨询过。 其实大家咨询的问题,反反…

利用AI技术做电商网赚,这些百万级赛道流量,你还不知道?!

大家好,我是向阳 AI技术的飞速扩展已经势不可挡,不管你承不承认,AI 已经毫无争议的在互联网中占有一席之地了 无论你是做内容产业的,还是做电商的,你现在都躲不开 AI。 现在互联网行业的竞争就是这么残酷 互联网行业…

YOLOv10最全使用教程(含ONNX和TensorRT推理)

论文题目:YOLOv10: Real-Time End-to-End Object Detection 研究单位:清华大学 论文链接:http://arxiv.org/abs/2405.14458 代码链接:https://github.com/THU-MIG/yolov10 作者提供的模型性能评价图,如下:…

【数据结构】神奇的二叉树

文章目录 前言1. 树形结构1.1 什么是树1.2 名词概念1.3 树的表现形式 2. 二叉树2.1 概念2.2 两种特殊的二叉树2.3 二叉树的性质 3. 二叉树的存储结构3.1 顺序存储3.2 链式存储 4. 二叉树的遍历4.1 前序遍历4.2 中序遍历4.3 后序遍历4.4 层序遍历 5. 遍历的代码实现5.1 递归实现…

机器学习模型可视化分析和诊断神器Yellowbrick

大家好,机器学习(ML)作为人工智能的核心,近来得到巨大应用,ML是使计算机能够在无需显式编程的情况下进行学习和预测或决策。ML算法通过学习历史数据模式,来对新的未见数据做出明智的预测或决策。然而,构建和训练ML模型…

ROS2学习——节点话题通信(2)

目录 一、ROS2节点 1.概念 2.实例 (1)ros2 run (2)ros2 node list (3)remapping重映射 (4)ros2 node info 二、话题 (1) ros2 topic list &#xf…

C语言内存函数(与上篇字符函数及字符串函数一起食用效果更佳哦~)

顾名思义,内存函数就是针对内存块(即一块内存)来处理的。 因此本篇所讲的四种内存函数: memcpy(内存拷贝)memmove(内存移动)memset(内存设置)memcmp&#x…

RocketMQ使用(3):消息重复

一、问题说明 发送时消息重复 当一条消息已被成功发送到服务端并完成持久化,此时出现了网络闪断或者客户端宕机,导致服务端对客户端应答失败。如果此时生产者意识到消息发送失败并尝试再次发送消息,消费者后续会收到两条内容相同并且Message…

vue项目elementui刷新页面弹窗问题

bug:每次刷新页面都有这个鬼弹窗。 刚开始以为是自己的代码问题,于是我翻遍了每一行代码,硬是没找出问题。 后来在网上找了些资料,原来是引入的问题。 解决方案: 改一下引入方式即可。 错误姿势 import Vue from …

Autodesk 3ds Max下载,3ds MAX 2024三维建模渲染软件安装包下载安装

3ds MAX中文版,其强大的功能和灵活的操作为广大用户提供了无限的创意空间,使得高质量动画、最新游戏、设计效果等领域的制作需求得以完美满足。 ​ 作为一款三维建模软件,3ds MAX中文版具备极高的建模精度和渲染质量。它支持多种建模方式&am…