golang:微服务架构下的日志追踪系统(二)

背景

在使用Gin框架进行服务开发时,我们遇到了一个日志记录的问题。由于Gin的上下文(*gin.Context)实现了context.Context接口,在调用日志记录器的InfoWarnError等方法时,直接传递Gin的上下文通常不会导致编译错误。会导致我们在《golang:微服务架构下的日志追踪系统》一文中定义的日志统计维度信息无法正确串联。

为了解决这一问题,我们之前的解决方案是让业务方在使用日志记录器时,将Gin的上下文手动转换为context.Context

但这种方案带来了一个明显的弊端:它依赖于开发人员的主动转换,而开发人员往往会忘记进行这一步操作,直接传递Gin的上下文,从而导致日志并未按照预期格式打印。这不仅增加了开发人员的负担,也降低了日志系统的可靠性和准确性。

解决方案

针对上述问题,我们提出了一种更为优雅的解决方案:在日志记录器的方法内部封装Gin上下文到context.Context的转换逻辑。这样一来,开发人员在使用日志记录器时无需再关心上下文的转换问题,只需直接传递Gin的上下文即可。

接下来,我们将这一转换逻辑集成到日志记录器的各个方法中,确保无论传入的是Gin的上下文还是context.Context,都能正确记录日志信息。

在封装的方法中实现gin的context的转换即可。

// gin context to context
func GinContextToContext(ctx context.Context) context.Context {
	if v, ok := ctx.(*gin.Context); ok {
		if v == nil || v.Request == nil {
			return context.Background()
		}
		return v.Request.Context()
	}
	return ctx
}

完整的逻辑。

package logger

import (
	"context"
	"errors"
	"fmt"
	"time"

	"{{your module}}}/go-core/utils/constant"

	"github.com/gin-gonic/gin"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"gorm.io/gorm/utils"
)

// Logger logger for gorm2
type Logger struct {
	log *zap.Logger
	logger.Config
	customFields []func(ctx context.Context) zap.Field
}

// Option logger/recover option
type Option func(l *Logger)

// WithCustomFields optional custom field
func WithCustomFields(fields ...func(ctx context.Context) zap.Field) Option {
	return func(l *Logger) {
		l.customFields = fields
	}
}

// WithConfig optional custom logger.Config
func WithConfig(cfg logger.Config) Option {
	return func(l *Logger) {
		l.Config = cfg
	}
}

// SetGormDBLogger set db logger
func SetGormDBLogger(db *gorm.DB, l logger.Interface) {
	db.Logger = l
}

// New logger form gorm2
func New(zapLogger *zap.Logger, opts ...Option) logger.Interface {
	l := &Logger{
		log: zapLogger,
		Config: logger.Config{
			SlowThreshold:             200 * time.Millisecond,
			Colorful:                  false,
			IgnoreRecordNotFoundError: false,
			LogLevel:                  logger.Warn,
		},
	}
	for _, opt := range opts {
		opt(l)
	}
	return l
}

// NewDefault new default logger
// 初始化一个默认的 logger
func NewDefault(zapLogger *zap.Logger) logger.Interface {
	return New(zapLogger, WithCustomFields(
		func(ctx context.Context) zap.Field {
			v := ctx.Value("Request-Id")
			if v == nil {
				return zap.Skip()
			}
			if vv, ok := v.(string); ok {
				return zap.String("trace", vv)
			}
			return zap.Skip()
		}, func(ctx context.Context) zap.Field {
			v := ctx.Value("method")
			if v == nil {
				return zap.Skip()
			}
			if vv, ok := v.(string); ok {
				return zap.String("method", vv)
			}
			return zap.Skip()
		}, func(ctx context.Context) zap.Field {
			v := ctx.Value("path")
			if v == nil {
				return zap.Skip()
			}
			if vv, ok := v.(string); ok {
				return zap.String("path", vv)
			}
			return zap.Skip()
		}, func(ctx context.Context) zap.Field {
			v := ctx.Value("version")
			if v == nil {
				return zap.Skip()
			}
			if vv, ok := v.(string); ok {
				return zap.String("version", vv)
			}
			return zap.Skip()
		}),
		WithConfig(logger.Config{
			SlowThreshold:             200 * time.Millisecond,
			Colorful:                  false,
			IgnoreRecordNotFoundError: false,
			LogLevel:                  logger.Info,
		}))
}

// 用于支持微服务架构下的链路追踪
func NewTracingLogger(zapLogger *zap.Logger) logger.Interface {
	return New(zapLogger, WithCustomFields(
		// trace是链路追踪的唯一标识
		// span是当前请求的唯一标识
		// parent_span是父请求的唯一标识
		func(ctx context.Context) zap.Field {
			v := ctx.Value(constant.CONTEXT_KEY_TRACE)
			if v == nil {
				return zap.Skip()
			}
			if vv, ok := v.(string); ok {
				return zap.String(constant.CONTEXT_KEY_TRACE, vv)
			}
			return zap.Skip()
		}, func(ctx context.Context) zap.Field {
			v := ctx.Value(constant.CONTEXT_KEY_SPAN)
			if v == nil {
				return zap.Skip()
			}
			if vv, ok := v.(string); ok {
				return zap.String(constant.CONTEXT_KEY_SPAN, vv)
			}
			return zap.Skip()
		}, func(ctx context.Context) zap.Field {
			v := ctx.Value(constant.CONTEXT_KEY_PARENT_SPAN)
			if v == nil {
				return zap.Skip()
			}
			if vv, ok := v.(string); ok {
				return zap.String(constant.CONTEXT_KEY_PARENT_SPAN, vv)
			}
			return zap.Skip()
		}, func(ctx context.Context) zap.Field {
			v := ctx.Value(constant.CONTEXT_KEY_METHOD)
			if v == nil {
				return zap.Skip()
			}
			if vv, ok := v.(string); ok {
				return zap.String(constant.CONTEXT_KEY_METHOD, vv)
			}
			return zap.Skip()
		}, func(ctx context.Context) zap.Field {
			v := ctx.Value(constant.CONTEXT_KEY_PATH)
			if v == nil {
				return zap.Skip()
			}
			if vv, ok := v.(string); ok {
				return zap.String(constant.CONTEXT_KEY_PATH, vv)
			}
			return zap.Skip()
		}, func(ctx context.Context) zap.Field {
			v := ctx.Value(constant.CONTEXT_KEY_VERSION)
			if v == nil {
				return zap.Skip()
			}
			if vv, ok := v.(string); ok {
				return zap.String(constant.CONTEXT_KEY_VERSION, vv)
			}
			return zap.Skip()
		}, func(ctx context.Context) zap.Field {
			// 用于标识调用方服务名
			v := ctx.Value(constant.CONTEXT_KEY_CALLER_SERVICE_NAME)
			if v == nil {
				return zap.Skip()
			}
			if vv, ok := v.(string); ok {
				return zap.String(constant.CONTEXT_KEY_CALLER_SERVICE_NAME, vv)
			}
			return zap.Skip()
		}, func(ctx context.Context) zap.Field {
			// 用于标识调用方ip
			v := ctx.Value(constant.CONTEXT_KEY_CALLER_IP)
			if v == nil {
				return zap.Skip()
			}
			if vv, ok := v.(string); ok {
				return zap.String(constant.CONTEXT_KEY_CALLER_IP, vv)
			}
			return zap.Skip()
		}),
		WithConfig(logger.Config{
			SlowThreshold:             200 * time.Millisecond,
			Colorful:                  false,
			IgnoreRecordNotFoundError: false,
			LogLevel:                  logger.Info,
		}))
}

// gin context to context
func GinContextToContext(ctx context.Context) context.Context {
	if v, ok := ctx.(*gin.Context); ok {
		if v == nil || v.Request == nil {
			return context.Background()
		}
		return v.Request.Context()
	}
	return ctx
}

// LogMode log mode
func (l *Logger) LogMode(level logger.LogLevel) logger.Interface {
	newLogger := *l
	newLogger.LogLevel = level
	return &newLogger
}

// Info print info
func (l Logger) Info(ctx context.Context, msg string, args ...interface{}) {
	if l.LogLevel >= logger.Info {
		ctx = GinContextToContext(ctx)
		//预留10个字段位置
		fields := make([]zap.Field, 0, 10+len(l.customFields))
		fields = append(fields, zap.String("file", utils.FileWithLineNum()))
		for _, customField := range l.customFields {
			fields = append(fields, customField(ctx))
		}
		now := time.Now().UnixMilli()
		// 从ctx中获取操作的开始时间
		if v := ctx.Value(constant.CONTEXT_KEY_EXECUTE_START_TIME); v != nil {
			if vv, ok := v.(int64); ok {
				// 计算操作的执行时间,以毫秒为单位
				duration := now - vv
				// 将操作的执行时间放入ctx
				fields = append(fields, zap.Int64(constant.CONTEXT_KEY_EXECUTE_DURATION, duration))
			}
		}
		for _, arg := range args {
			if vv, ok := arg.(zapcore.Field); ok {
				if len(vv.String) > 0 {
					fields = append(fields, zap.String(vv.Key, vv.String))
				} else if vv.Integer > 0 {
					fields = append(fields, zap.Int64(vv.Key, vv.Integer))
				} else {
					fields = append(fields, zap.Any(vv.Key, vv.Interface))
				}
			}
		}
		l.log.Info(msg, fields...)
	}
}

// Warn print warn messages
func (l Logger) Warn(ctx context.Context, msg string, args ...interface{}) {
	if l.LogLevel >= logger.Warn {
		ctx = GinContextToContext(ctx)
		//预留10个字段位置
		fields := make([]zap.Field, 0, 10+len(l.customFields))
		fields = append(fields, zap.String("file", utils.FileWithLineNum()))
		for _, customField := range l.customFields {
			fields = append(fields, customField(ctx))
		}
		for _, arg := range args {
			if vv, ok := arg.(zapcore.Field); ok {
				if len(vv.String) > 0 {
					fields = append(fields, zap.String(vv.Key, vv.String))
				} else if vv.Integer > 0 {
					fields = append(fields, zap.Int64(vv.Key, vv.Integer))
				} else {
					fields = append(fields, zap.Any(vv.Key, vv.Interface))
				}
			}
		}
		l.log.Warn(msg, fields...)
	}
}

// Error print error messages
func (l Logger) Error(ctx context.Context, msg string, args ...interface{}) {
	if l.LogLevel >= logger.Error {
		ctx = GinContextToContext(ctx)
		//预留10个字段位置
		fields := make([]zap.Field, 0, 10+len(l.customFields))
		fields = append(fields, zap.String("file", utils.FileWithLineNum()))
		for _, customField := range l.customFields {
			fields = append(fields, customField(ctx))
		}

		for _, arg := range args {
			if vv, ok := arg.(zapcore.Field); ok {
				if len(vv.String) > 0 {
					fields = append(fields, zap.String(vv.Key, vv.String))
				} else if vv.Integer > 0 {
					fields = append(fields, zap.Int64(vv.Key, vv.Integer))
				} else {
					fields = append(fields, zap.Any(vv.Key, vv.Interface))
				}
			}
		}
		l.log.Error(msg, fields...)
	}
}

// Trace print sql message
func (l Logger) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) {
	if l.LogLevel <= logger.Silent {
		return
	}
	ctx = GinContextToContext(ctx)
	fields := make([]zap.Field, 0, 6+len(l.customFields))
	elapsed := time.Since(begin)
	switch {
	case err != nil && l.LogLevel >= logger.Error && (!l.IgnoreRecordNotFoundError || !errors.Is(err, gorm.ErrRecordNotFound)):
		for _, customField := range l.customFields {
			fields = append(fields, customField(ctx))
		}
		fields = append(fields,
			zap.Error(err),
			zap.String("file", utils.FileWithLineNum()),
			zap.Duration("latency", elapsed),
		)

		sql, rows := fc()
		if rows == -1 {
			fields = append(fields, zap.String("rows", "-"))
		} else {
			fields = append(fields, zap.Int64("rows", rows))
		}
		fields = append(fields, zap.String("sql", sql))
		l.log.Error("", fields...)
	case elapsed > l.SlowThreshold && l.SlowThreshold != 0 && l.LogLevel >= logger.Warn:
		for _, customField := range l.customFields {
			fields = append(fields, customField(ctx))
		}
		fields = append(fields,
			zap.Error(err),
			zap.String("file", utils.FileWithLineNum()),
			zap.String("slow!!!", fmt.Sprintf("SLOW SQL >= %v", l.SlowThreshold)),
			zap.Duration("latency", elapsed),
		)

		sql, rows := fc()
		if rows == -1 {
			fields = append(fields, zap.String("rows", "-"))
		} else {
			fields = append(fields, zap.Int64("rows", rows))
		}
		fields = append(fields, zap.String("sql", sql))
		l.log.Warn("", fields...)
	case l.LogLevel == logger.Info:
		for _, customField := range l.customFields {
			fields = append(fields, customField(ctx))
		}
		fields = append(fields,
			zap.Error(err),
			zap.String("file", utils.FileWithLineNum()),
			zap.Duration("latency", elapsed),
		)

		sql, rows := fc()
		if rows == -1 {
			fields = append(fields, zap.String("rows", "-"))
		} else {
			fields = append(fields, zap.Int64("rows", rows))
		}
		fields = append(fields, zap.String("sql", sql))
		l.log.Info("", fields...)
	}
}

// Immutable custom immutable field
// Deprecated: use Any instead
func Immutable(key string, value interface{}) func(ctx context.Context) zap.Field {
	return Any(key, value)
}

// Any custom immutable any field
func Any(key string, value interface{}) func(ctx context.Context) zap.Field {
	field := zap.Any(key, value)
	return func(ctx context.Context) zap.Field { return field }
}

// String custom immutable string field
func String(key string, value string) func(ctx context.Context) zap.Field {
	field := zap.String(key, value)
	return func(ctx context.Context) zap.Field { return field }
}

// Int64 custom immutable int64 field
func Int64(key string, value int64) func(ctx context.Context) zap.Field {
	field := zap.Int64(key, value)
	return func(ctx context.Context) zap.Field { return field }
}

// Uint64 custom immutable uint64 field
func Uint64(key string, value uint64) func(ctx context.Context) zap.Field {
	field := zap.Uint64(key, value)
	return func(ctx context.Context) zap.Field { return field }
}

// Float64 custom immutable float32 field
func Float64(key string, value float64) func(ctx context.Context) zap.Field {
	field := zap.Float64(key, value)
	return func(ctx context.Context) zap.Field { return field }
}

测试用例

package logger

import (
	"context"
	"fmt"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"{{your module}}/go-core/utils/constant"
	"github.com/gin-gonic/gin"
	"go.uber.org/zap"
)



// Mock function to create a gin context
func createGinContext() *gin.Context {
	w := httptest.NewRecorder()

	// Create a mock request with an attached context
	req, _ := http.NewRequest(http.MethodPost, "/api/v1/users", nil)
	ctx := context.Background()

	// Here you can set values in the context
	ctx = context.WithValue(ctx, constant.CONTEXT_KEY_SPAN, "123456")
	ctx = context.WithValue(ctx, constant.CONTEXT_KEY_PARENT_SPAN, "parent_span_123456")
	ctx = context.WithValue(ctx, constant.CONTEXT_KEY_TRACE, "trace_id_123456")
	ctx = context.WithValue(ctx, constant.CONTEXT_KEY_METHOD, "POST")
	ctx = context.WithValue(ctx, constant.CONTEXT_KEY_PATH, "/api/v1/users")
	ctx = context.WithValue(ctx, constant.CONTEXT_KEY_VERSION, "v1.0.0")
	ctx = context.WithValue(ctx, constant.CONTEXT_KEY_CALLER_SERVICE_NAME, "user-service")
	ctx = context.WithValue(ctx, constant.CONTEXT_KEY_CALLER_IP, "172.0.0.3")
	req = req.WithContext(ctx)

	// Create the Gin context
	c, _ := gin.CreateTestContext(w)
	c.Request = req

	return c
}

// 测试ginContextWithLogger
func TestGinContextWithLogger(t *testing.T) {
	// 创建一个 zap logger 实例
	zapLogger, _ := zap.NewProduction()
	defer zapLogger.Sync() // 确保日志被刷新

	// 创建一个带有自定义字段和配置的 Logger 实例
	customLogger := NewTracingLogger(zapLogger)
	// 创建一个 Gin context
	c := createGinContext()

	// 测试 Info 方法
	customLogger.Info(c, "This is an info message")

	// 测试 Warn 方法
	customLogger.Warn(c, "This is a warning message")

	// 测试 Error 方法
	customLogger.Error(c, "This is an error message")

	// 测试 Trace 方法,模拟一个慢查询
	slowQueryBegin := time.Now()
	slowQueryFunc := func() (string, int64) {
		return "SELECT * FROM users", 100
	}
	time.Sleep(2 * time.Second) // 模拟一个慢查询
	customLogger.Trace(c, slowQueryBegin, slowQueryFunc, nil)

	// 测试 Trace 方法,模拟一个错误查询
	errorQueryBegin := time.Now()
	errorQueryFunc := func() (string, int64) {
		return "SELECT * FROM non_existent_table", 0
	}
	customLogger.Trace(c, errorQueryBegin, errorQueryFunc, fmt.Errorf("table not found"))

	// 由于日志是异步的,我们需要在测试结束时等待一段时间以确保所有日志都被输出
	time.Sleep(500 * time.Millisecond)
}

 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Vue项目整合与优化

前几篇文章&#xff0c;我们讲述了 Vue 项目构建的整体流程&#xff0c;从无到有的实现了单页和多页应用的功能配置&#xff0c;但在实现的过程中不乏一些可以整合的功能点及可行性的优化方案&#xff0c;就像大楼造完需要进行最后的项目验收改进一样&#xff0c;有待我们进一步…

网关的介绍

网关&#xff08;Gateway&#xff09;在网络技术中扮演着举足轻重的角色。为了让你更好地理解网关及其相关术语&#xff0c;我会尽量用简洁明了的语言来解释&#xff0c;同时也会穿插一些专业术语以便你深入学习。 网关的基本概念 网关&#xff0c;顾名思义&#xff0c;是网络的…

【C语言程序设计——循环程序设计】枚举法换硬币(头歌实践教学平台习题)【合集】

目录&#x1f60b; 任务描述 相关知识 一、循环控制 / 跳转语句的使用 1. 循环控制语句&#xff08;for 循环&#xff09; 2. 循环控制语句&#xff08;while 循环&#xff09; 3. 跳转语句&#xff08;break 语句&#xff09; 4. 跳转语句&#xff08;continue 语句&…

SD-WAN怎样减少异地组网的网络延迟?

在经济全球化的推动下&#xff0c;许多企业的业务已经扩展到多个国家或地区。这种情况下&#xff0c;企业需要搭建高效、稳定的网络连接&#xff0c;以确保异地的分支机构之间能够顺畅地交流。网络延迟是拉低异地组网数据传输效率的重要因素&#xff0c;直接影响到企业的运营和…

小程序学习06——uniapp组件常规引入和easycom引入语法

目录 一 组件注册 1.1 组件全局注册 1.2 组件全局引入 1.3 组件局部引入 页面引入组件方式 1.3.1 传统vue规范&#xff1a; 1.3.2 通过uni-app的easycom 二 组件的类型 2.1 基础组件列表 一 组件注册 1.1 组件全局注册 &#xff08;a&#xff09;新建compoents文件…

uniapp 微信小程序 自定义日历组件

效果图 功能&#xff1a;可以记录当天是否有某些任务或者某些记录 具体使用&#xff1a; 子组件代码 <template><view class"Accumulate"><view class"bx"><view class"bxx"><view class"plank"><…

上升沿下降沿递增

沿指令&#xff1a;P&#xff1a;上升沿 从01 导通一个扫描周期 N&#xff1a;下降沿 从10 导通一个扫描周期

大数据-268 实时数仓 - ODS层 将 Kafka 中的维度表写入 DIM

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; Java篇开始了&#xff01; MyBatis 更新完毕目前开始更新 Spring&#xff0c;一起深入浅出&#xff01; 目前已经更新到了&#xff1a; H…

微博_14.12.2-内置猪手 会员版

微博猪手是一款作用于微博的 XposedLsposed 模块&#xff0c;可以支持未root用户和已root用户使用。进入【我的】页面&#xff0c;点击【右上角的设置】&#xff0c;点击【微博猪手】即可进一步设置其他功能。通过微博猪手模块可以实现去除各种广告&#xff08;开屏、信息流等&…

计算机网络 (21)网络层的几个重要概念

前言 计算机网络中的网络层是OSI&#xff08;开放系统互连&#xff09;模型中的第三层&#xff0c;也是TCP/IP模型中的第二层&#xff0c;它位于数据链路层和传输层之间&#xff0c;负责数据包从源主机到目的主机的路径选择和数据转发。 一、网络层的主要功能 路由选择&#xf…

openwrt nginx UCI配置过程

openwrt 中nginx有2种配置方法&#xff0c;uci nginx uci /etc/config/nginx 如下&#xff1a; option uci_enable true‘ 如果是true就是使用UCI配置&#xff0c;如果 是false&#xff0c;就要使用/etc/nginx/nginx.conf&#xff0c;一般不要修改。 如果用UCI&#xff0c;其…

【深度学习进阶】基于CNN的猫狗图片分类项目

介绍 基于卷积神经网络&#xff08;CNN&#xff09;的猫狗图片分类项目是机器学习领域中的一种常见任务&#xff0c;它涉及图像处理和深度学习技术。以下是该项目的技术点和流程介绍&#xff1a; 技术点 卷积神经网络 (CNN): CNN 是一种专门用于处理具有类似网格结构的数据的…

uni-app 页面生命周期及组件生命周期汇总(Vue2、Vue3)

文章目录 一、前言&#x1f343;二、页面生命周期三、Vue2 页面及组件生命周期流程图四、Vue3 页面及组件生命周期流程图4.1 页面加载时序介绍4.2 页面加载常见问题4.3 onShow 和 onHide4.4 onInit4.5 onLoad4.6 onReachBottom4.7 onPageScroll4.8 onBackPress4.9 onTabItemTap…

缓存淘汰算法:次数除以时间差

记录缓存中的每一项的访问次数、最后访问时间&#xff0c;获取当前时间&#xff0c;可算出时间差&#xff0c;然后&#xff0c;用次数除以时间差&#xff0c;取最小的淘汰。 这一算法比较慢&#xff0c;需配合多级缓存。一级缓存不很大&#xff0c;使用此算法。二级缓存可以大…

uniapp 微信小程序开发使用高德地图、腾讯地图

一、高德地图 1.注册高德地图开放平台账号 &#xff08;1&#xff09;创建应用 这个key 第3步骤&#xff0c;配置到项目中locationGps.js 2.下载高德地图微信小程序插件 &#xff08;1&#xff09;下载地址 高德地图API | 微信小程序插件 &#xff08;2&#xff09;引入项目…

Mac iTerm2集成DeepSeek AI

1. 去deepseek官网申请api key&#xff0c;DeepSeek 2. 安装iTerm2 AI Plugin插件&#xff0c;https://iterm2.com/ai-plugin.html&#xff0c;插件解压后直接放到和iTerms相同的位置&#xff0c;默认就在/Applications 下 3. 配置iTerm2 4. 重启iTerm2,使用快捷键呼出AI对话…

树莓派 Pico RP2040 教程点灯 双核编程案例

双核点亮不同的 LED 示例&#xff0c;引脚分别是GP0跟GP1。 #include "pico/stdlib.h" #include "pico/multicore.h"#define LED1 0 // 核心 0 控制的 LED 引脚 #define LED2 1 // 核心 1 控制的 LED 引脚// the setup function runs once when you press …

简单使用linux

1.1 Linux的组成 Linux 内核&#xff1a;内核是系统的核心&#xff0c;是运行程序和管理 像磁盘和打印机等硬件设备的核心程序。 文件系统 : 文件存放在磁盘等存储设备上的组织方法。 Linux 能支持多种目前浒的文件系统&#xff0c;如 ext4 、 FAT 、 VFAT 、 ISO9660 、 NF…

ACM算法模板

ACM算法模板 起手式基础算法前缀和与差分二分查找三分查找求极值分治法&#xff1a;归并排序 动态规划基本线性 d p dp dp最长上升子序列I O ( n 2 ) O(n ^ 2) O(n2)最长上升子序列II O ( n l o g n ) O(nlogn) O(nlogn) 贪心二分最长公共子序列 背包背包求组合种类背包求排列…

《Vue3实战教程》19:Vue3组件 v-model

如果您有疑问&#xff0c;请观看视频教程《Vue3实战教程》 组件 v-model​ 基本用法​ v-model 可以在组件上使用以实现双向绑定。 从 Vue 3.4 开始&#xff0c;推荐的实现方式是使用 defineModel() 宏&#xff1a; vue <!-- Child.vue --> <script setup> co…