Golang日志框架lumberjack包源码分析

Go 学习笔记(54)— Go 第三方库之 uber-go/zap/lumberjack(记录日志到文件、支持自动分割日志、支持日志级别、打印调用文件、函数和行号) 1. 简要说明 zap 是 uber 开源的 Go 高性能日志库,支持不同的日志级别, 能够打印基本信息等,但不支持日志的分割,这里我们可以使用 lumberjack 也是 zap 官方推荐用于日志分割,结合这两个库我们就可以实现以下功能的日志机制: 能够将事件记录到文件中,而不是应用程序控制台; 日志切割能够根据文件大小、时间或间隔等来切割日志文件; 支持不同的日志级别,例如 DEBUG , INFO , WARN , ERROR 等; 能够打印基本信息,如调用文件、函数名和行号,日志时间等; 阅读详情

github地址:  https://github.com/natefinch/lumberjack

 

获取源码

go get gopkg.in/natefinch/lumberjack.v2

 

介绍

       lumberjack用于记录日志,但是它可以控制每个日志文件的大小,以及可以按规则删除历史的日志文件,甚至可以对历史的日志文件进行压缩.

       Logger会首先打开或创建logFile文件,如果logFile文件已存在并且该文件的大小没有超过设置的MaxSize,就会在打开该文件并进行追加日志。否则会创建新的日志文件。

      当前日志文件超过MaxSize MB,就会关闭当前文件,并将其重命名,并使用原始名称创建一个新的日志文件。因此,最新的日志输出都在原始名称的文件中。

 

保留的日志文件

存留的历史文件名称为:name-timestamp.ext  [name是给定的文件名,timestamp是日志轮换格式的时间(2006-01-02T15-04-05.000)]

 

清理旧的日志文件策略

 每当创建新的日志文件时,旧的日志文件都可能被删除。删除会根据MaxAge和MaxBackups的参数设置

 1. 时间戳早于MaxAge天的文件都会被删除,如果MaxAge为0,则不会根据MaxAge删除日志文件

 2. MaxBackups是要保留的最大旧日志文件数,用来控制该程序日志文件的最大大小。早于MaxBackups数之前的文件都会被删除,如果MaxBackups为0,则不会根据MaxBackups进行删除日志文件

 3. 如果MaxAge 和 MaxBackups都为0,则不会删除日志文件

 

源码分析

    核心结构体Logger

type Logger struct {
	// Filename is the file to write logs to.  Backup log files will be retained
	// in the same directory.  It uses <processname>-lumberjack.log in
	// os.TempDir() if empty.
	//写日志的文件名称
	Filename string `json:"filename" yaml:"filename"`

	// MaxSize is the maximum size in megabytes of the log file before it gets
	// rotated. It defaults to 100 megabytes.
	//每个日志文件长度的最大大小,默认100M。
	MaxSize int `json:"maxsize" yaml:"maxsize"`

	// MaxAge is the maximum number of days to retain old log files based on the
	// timestamp encoded in their filename.  Note that a day is defined as 24
	// hours and may not exactly correspond to calendar days due to daylight
	// savings, leap seconds, etc. The default is not to remove old log files
	// based on age.
	//日志保留的最大天数(只保留最近多少天的日志)
	MaxAge int `json:"maxage" yaml:"maxage"`

	// MaxBackups is the maximum number of old log files to retain.  The default
	// is to retain all old log files (though MaxAge may still cause them to get
	// deleted.)
	//只保留最近多少个日志文件,用于控制程序总日志的大小
	MaxBackups int `json:"maxbackups" yaml:"maxbackups"`

	// LocalTime determines if the time used for formatting the timestamps in
	// backup files is the computer's local time.  The default is to use UTC
	// time.
	//是否使用本地时间,默认使用UTC时间
	LocalTime bool `json:"localtime" yaml:"localtime"`

	// Compress determines if the rotated log files should be compressed
	// using gzip.
	// 是否压缩日志文件,压缩方法gzip
	Compress bool `json:"compress" yaml:"compress"`

	size int64   //记录当前日志文件的字节数
	file *os.File  //当前的日志文件
	mu   sync.Mutex

	millCh    chan bool
	startMill sync.Once
}

  

核心方法Write

func (l *Logger) Write(p []byte) (n int, err error) {
	l.mu.Lock()
	defer l.mu.Unlock()

	writeLen := int64(len(p))
	if writeLen > l.max() {
		return 0, fmt.Errorf(
			"write length %d exceeds maximum file size %d", writeLen, l.max(),
		)
	}

	if l.file == nil {
		if err = l.openExistingOrNew(len(p)); err != nil {
			return 0, err
		}
	}

	//如果写入将导致日志文件大于MaxSize,则调用rotate方法进行日志文件的切换
	if l.size+writeLen > l.max() {
		if err := l.rotate(); err != nil {
			return 0, err
		}
	}

	n, err = l.file.Write(p)  //将数据写入日志文件
	l.size += int64(n)

	return n, err
}

       Write方法实现io.Writer接口,用于向日志文件中写入信息,如果写入将导致日志文件大于MaxSize,则将当前文件关闭,将其重命名为包括当前时间的时间戳,并使用原始日志文件名创建新的日志文件。如果一次写入的长度大于MaxSize,则返回错误。

     从Write方法中我们看到每次写入日志前都会检测本次的写入是否会导致当前日志文件的大小大于MaxSize,如果大于则调用

rotate方法进行处理。

    rotate方法

func (l *Logger) rotate() error {
	//关闭当前日志文件
	if err := l.close(); err != nil {
		return err
	}
	//把当前日志文件进行重命名,并创建一个新的日志文件用于写入日志
	if err := l.openNew(); err != nil {
		return err
	}
	l.mill()
	return nil
}

       rotate方法用于日志切换,关闭现有的日志文件,并调用openNew方法把当前关闭的日志文件重命名,并创建一个新的日志文件进行写入日志。调用mill方法根据配置进行日志删除或压缩操作。

 

openNew方法

func (l *Logger) openNew() error {
	//如果目录不存在,则进行创建
	err := os.MkdirAll(l.dir(), 0744)
	if err != nil {
		return fmt.Errorf("can't make directories for new logfile: %s", err)
	}

	name := l.filename()
	mode := os.FileMode(0644)
	info, err := os_Stat(name)  //获取当前文件的信息
	if err == nil {
		// Copy the mode off the old logfile.
		mode = info.Mode()
		// move the existing file
		newname := backupName(name, l.LocalTime)  //获取要转换的日志名称
		if err := os.Rename(name, newname); err != nil { //将当前文件重命名
			return fmt.Errorf("can't rename log file: %s", err)
		}

		// this is a no-op anywhere but linux
		if err := chown(name, info); err != nil {  //改变linux系统下文件的权限
			return err
		}
	}

	// we use truncate here because this should only get called when we've moved
	// the file ourselves. if someone else creates the file in the meantime,
	// just wipe out the contents.
	//创建新的日志文件
	f, err := os.OpenFile(name, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode)
	if err != nil {
		return fmt.Errorf("can't open new logfile: %s", err)
	}
	l.file = f
	l.size = 0
	return nil
}

openNew方法比较简单,主要是把当前关闭的日志文件重命名,并创建一个新的日志文件进行写入日志。

 

mill相关的函数

func (l *Logger) mill() {
	l.startMill.Do(func() {
		l.millCh = make(chan bool, 1)
		go l.millRun()
	})
	select {
	case l.millCh <- true:
	default:
	}
}

func (l *Logger) millRun() {
	for _ = range l.millCh {
		// what am I going to do, log this?
		_ = l.millRunOnce()
	}
}

func (l *Logger) millRunOnce() error {
	if l.MaxBackups == 0 && l.MaxAge == 0 && !l.Compress {
		return nil
	}

	//获取老的日志文件
	files, err := l.oldLogFiles()
	if err != nil {
		return err
	}

	var compress, remove []logInfo

	//MaxBackups大于0 并且 当前的文件数大于MaxBackups,说明有需要删除的日志文件
	if l.MaxBackups > 0 && l.MaxBackups < len(files) {
		preserved := make(map[string]bool)
		var remaining []logInfo
		for _, f := range files {  //遍历每一个文件
			// Only count the uncompressed log file or the
			// compressed log file, not both.
			fn := f.Name()  //获取文件名称
			//如果文件名以.gz结尾,则从文件名称删除.gz
			if strings.HasSuffix(fn, compressSuffix) {
				fn = fn[:len(fn)-len(compressSuffix)]
			}
			preserved[fn] = true

			if len(preserved) > l.MaxBackups {
				remove = append(remove, f)  //需要删除的文件列表
			} else {
				remaining = append(remaining, f)  //保留的文件列表
			}
		}
		files = remaining
	}
	if l.MaxAge > 0 {
		diff := time.Duration(int64(24*time.Hour) * int64(l.MaxAge))
		cutoff := currentTime().Add(-1 * diff)  //需要删除的时间节点

		var remaining []logInfo
		for _, f := range files { //遍历保留的日志文件
			if f.timestamp.Before(cutoff) {  //需要删除的日志文件(超过了保留时间)
				remove = append(remove, f)  //需要删除的文件列表
			} else {
				remaining = append(remaining, f)
			}
		}
		files = remaining
	}

	if l.Compress { //获取需要压缩的文件列表
		for _, f := range files {
			if !strings.HasSuffix(f.Name(), compressSuffix) {
				compress = append(compress, f)
			}
		}
	}

	for _, f := range remove {  //需要删除的文件列表
		errRemove := os.Remove(filepath.Join(l.dir(), f.Name()))
		if err == nil && errRemove != nil {
			err = errRemove
		}
	}
	for _, f := range compress {  //压缩每一个需要删除的日志文件
		fn := filepath.Join(l.dir(), f.Name())
		errCompress := compressLogFile(fn, fn+compressSuffix)
		if err == nil && errCompress != nil {
			err = errCompress
		}
	}

	return err
}

mill方法会开启一个goroutine进行处理,处理的核心方法是millRunOnce, millRunOnce方法会根据配置判断是否需要删除的历史日志文件,如果有则删除。如果配置的压缩,则会对未压缩的历史文件进行压缩。

 

Rotate方法
func (l *Logger) Rotate() error {
	l.mu.Lock()
	defer l.mu.Unlock()
	return l.rotate()
}

Rotate方法是对外提供手动切换日志文件的功能,同步调用rotate方法

 

Close方法

func (l *Logger) Close() error {
	l.mu.Lock()
	defer l.mu.Unlock()
	return l.close()
}

// close closes the file if it is open.
//关闭日志文件,将file置为nil
func (l *Logger) close() error {
	if l.file == nil {
		return nil
	}
	err := l.file.Close()
	l.file = nil
	return err
}

 

其他的一些方法就不在累述,有兴趣可自行阅读

 

 

lumberjack:伐木工人是Go的日志滚动包 伐木工人 Lumberjack是一个Go软件包,用于将日志写入滚动文件。 包伐木工人提供了一个滚动记录器。 请注意,这是伐木工人的v2.0,因此应使用gopkg.in导入: import "gopkg.in/natefinch/lumberjack.v2" 软件包名称仍然只是lumberjack,代码位于v2.0分支下的 。 Lumberjack旨在成为日志记录基础结构的一部分。 它不是一个一站式解决方案,而是一个位于日志记录堆栈底部的可插拔组件,该组件可简单地控制将日志写入其中的文件。 Lumberjack可以与任何可以写入io.Writer的日志记录包一起很好地工作,包括标准库的日志包。 Lumberjack假定只有一个进程正在写入输出文件。 在同一台计算机上的多个进程中使用相同的伐木工人配置将导致不正确的行为。 例子 要将伐木工人与标准库的日志包一起使用,只需在应用程序 立即下载

相关推荐

属性基加密(Attribute-based Encryption, ABE)

KP-ABE中,用户的密钥定义了访问策略,密文的属性与策略进行匹配。CP-ABE中,密文定义了访问策略,用户的属性与策略进行匹配。

2201_75392924的博客 1831

Go:日志滚动(rolling)记录器 lumberjack 简介

lumberjack是一个日志滚动记录器。写入lumberjack的日志达到一定的条件后会进行存档(普通文件的形式,或压缩文件的形式),然后新建另一个同名文件(原文件存档时会重命名)继续记录。但是lumberjack本身并不包含日志的基础功能,例如日志等级、日志格式化等。理论上可以向lumberjack写入任意文本,并实现滚动记录。一般情况下,lumberjack配合其他日志库,实现日志的滚动(rolling)记录。

GIS摆渡人 2961

PHM算法与智能分析技术

PHM概述

柳杰的博客 1万+

Golang高性能日志库zap + lumberjack 日志切割组件详解

Golang 高性能日志库 zap 的详细使用!!! Gin集成zap!!! lumberjack 日志切割组件的详细使用!!!

脑袋装水的博客 9514

深入解析GEM5 McPAT NoC功耗模型:从arbiter电容计算到微架构关联

本文深入解析GEM5 McPAT NoC功耗模型,重点探讨arbiter电容计算与微架构的关联。通过拆解动态功耗公式和四类电容的物理本质,揭示能量与功率的转换关系,并提供参数调试和模型精度优化的实战技巧,帮助开发者更准确地预测和优化NoC功耗。

weixin_29305313的博客 203

Halcon实战:5分钟搞定自定义直线卡尺工具(附完整代码下载)

本文详细介绍了如何使用Halcon快速开发自定义直线卡尺工具,通过交互式ROI绘制、边缘检测和直线拟合三个核心函数,实现高效工业视觉检测。文章包含完整代码下载,帮助开发者在5分钟内掌握这一实用工具的开发技巧,提升直线检测的精度和效率。

perl8的博客 252

go-lumberjack

lumberjack是一个用于日志文件滚动和归档的 Go 语言库。

wick_light的博客 1235

ABB机器人C#二次开发实战:从通信打通到数据读写的落地指南(附完整工程)

最近在做自动化产线升级项目时,遇到一个核心需求:用C#上位机实时读取ABB机器人的关节角度、IO信号,同时下发运动指令和参数配置。翻遍全网,要么是Robot Studio的基础操作教程,要么是C++/VC++的老旧案例,C#相关的实操内容要么只给片段代码,要么跳过通信配置、权限设置等关键步骤,导致刚上手时踩了无数坑——连接超时、数据读不到、写入无响应,甚至误触发机器人安全保护。

这里只有干货:从 Modbus 通信到 MES 对接,从 YOLO 训练到工控机部署,带你搞定工业软件开发全流程。 1398

go 日志库zap的使用

Goland中使用zap日志库 运行下面的命令安装zap go get -u go.uber.org/zap 一 简单的logger实例 通过调用zap.NewProduction()/zap.NewDevelopment()或者zap.Example()创建一个Logger 在性能很好但不是很关键的上下文中,使用SugaredLogger。它比其他结构化日志记录包快4-10倍,并且支持结构化和printf风格的日志记录。 在每一微秒和每一次内存分配都很重要的上下文中,使用Logger。它甚至比S

qq_39298872的博客 1214

MATLAB基础应用精讲-【图像处理】GNN图神经网络(附python代码实现)

图神经网络由于其在处理非欧空间数据和复杂特征方面的优势,受到广泛关注并应用于推荐系统、知识图谱、交通道路分析等场景。图结构可描述复杂关系,如基因结构、通信网络、交通路线、社交网络等。图计算挖掘结构信息,但无法学习节点特征。神经网络在欧氏空间数据上表现优越,但无法直接应用于非欧氏空间的图数据。图神经网络结合图计算和神经网络优势,处理非欧氏空间数据及其复杂特征,应用于网络链接预测、推荐系统和交通道路分析等场景。实际应用中,图数据规模庞大,如百亿级节点、千亿级边,存储开销超过10TB。

qq_36130719的博客 1141

把复杂计算推到数据库里:ABAP CDS View Table Function 从原理到落地实践

摘要:本文深入解析ABAP CDS Table Function技术,重点探讨其在复杂业务场景下的应用价值。当CDS View无法满足窗口函数、递归计算等高级需求时,Table Function通过结合CDS类型系统与AMDP的数据库原生计算能力,成为关键解决方案。文章详细介绍了Table Function的架构设计(CDS实体声明+AMDP实现)、客户端处理机制、开发传输流程中的注意事项,并通过航班数据示例展示最小实现闭环。这项技术特别适用于需要将复杂计算下推到数据库层的场景,能显著提升RAP和OData

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 2303

golang 日志库logrus和lumberjack 日志切割库实践

golang 日志库logrus和lumberjack 日志切割库实践

qq_32421489的博客 2601

《AI应用架构师:解锁AI智能体于经济学研究洞察经济趋势的关键密码》

当经济学家还在为“疫情后的消费复苏曲线”“ChatGPT对就业的长期影响”等非线性问题挠头时,AI应用架构师已经用AI智能体搭建了一个“数字经济沙盘”:里面有会砍价的消费者、会算成本的企业、会调政策的政府,每个智能体像真实角色一样思考决策,甚至能模拟出“政策出台3个月后失业率的微妙变化”。这篇文章将带你从架构师视角拆解:如何用AI智能体破解经济学研究的3大痛点(数据爆炸、非线性复杂、动态演化)?如何设计能“活起来”的经济智能体?如何用智能体模拟出能指导现实的经济趋势?

AI天才研究院 903

Go lumberjack 日志轮换和管理

在开发应用程序时,记录日志是一项关键的任务,以便在应用程序运行时追踪问题、监视性能和保留审计记录。Go 语言提供了灵活且强大的日志记录功能,可以通过多种方式配置和使用。其中一个常用的日志记录库是 github.com/natefinch/lumberjack,它提供了一个方便的方式来处理日志文件的轮换,以防止日志文件无限增长。本文将详细介绍 github.com/natefinch/lumberjack,包括其主要特点、如何使用它以及示例代码。让我们一起深入了解这个有用的 Go 语言日志记录库。

Lamb的博客 2197

蓝牙模块HM-10手册

蓝牙模块HM-10手册,该手册对蓝牙模块HM-10进行了详细的介绍和使用方法以及一些AT指令。

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

Go分布式爬虫学习笔记(十六)_go slog日志切割

这 3 个函数可以传入若干类型为 zap.Option 的选项,从而扩展 Logger 的行为。选项包括 zap.WithCaller 打印文件、行号、zap.AddStacktrace 打印堆栈信息。除此之外,我们还可以定制自己的 Logger,提供比预置的 Logger 更灵活的能力。

2401_86984673的博客 1248

IDEA的使用指南

IDEA基于java的使用手册,可以最快速的上手,在使用中不断的丰富自己的使用技巧.

golang xorm 自定义日志记录器,使用zap实现日志输出、切割日志

golang xorm 自定义日志记录器,使用zap实现日志输出、切割日志

Yuuuuuubs的博客 2985

Outlook Express 6 (OE6) for Win7/Win8/Win10

英文版 Outlook Express6.0, 兼容Win7/Win10/WinXP,解压就可用。

Lumberjack 开源项目教程

Lumberjack 开源项目教程 项目介绍 Lumberjack 是一个用于日志滚动的 Go 包。它可以帮助开发者将日志写入到滚动文件中,确保日志文件不会无限增长,从而有效地管理日志文件。 项目快速启动 安装 首先,确保你已经安装了 Go 环境。然后使用以下命令安装 Lumberjack 包: go get gopkg.in/natefinch/lumberjack.v2 示例代码 以下是一个...

gitblog_00242的博客 741

GO语言使用Zap日志库(小白文)

本文介绍了GO语言如何快速使用zap日志库进行日志打印

weixin_59801183的博客 2997

Go日志库zap和lumberjack库的使用

有一些日志库使用了基于反射的序列化和字符串格式化,这写都是CPU密集型的操作,会影响日志库的性能,这些都是。我们可以在初始化时,设置记录哪种级别的日志的调用栈信息。日志库在每个项目中都是必不可少的一部分,Go语言中有很多优秀的日志库,比如logrus、zap等,这里我们介绍zap日志库的使用。我们看到,logs目录下多了几个压缩文件(由于我们写入的是相同的字符串,所以压缩文件很小,在实际应用中,不会这么小)。可以看到,info级别的日志没有打印调用栈信息,而error级别的日志记录了调用栈信息。

dreambreeze110的博客 1186

go 标准日志库使用

是一个日志滚动记录器。可以把日志文件根据大小、日期等分割。一般情况下,lumberjack配合其他日志库,实现日志的滚动(rolling)记录。MaxSize:在进行切割之前,日志文件的最大大小(以MB为单位)MaxBackups:保留旧文件的最大个数。Compress:是否压缩/归档旧文件。MaxAges:保留旧文件的最大天数。Filename: 日志文件的位置。

专注于Python编程技术的分享与交流,致力于帮助开发者提升编程技能,解决实际问题,探索Python的无限可能。 697

Go语言日志滚动包:Lumberjack完全指南

Go语言日志滚动包:Lumberjack完全指南 项目介绍 Lumberjack是专为Go语言设计的一个高效且易于使用的日志滚动包。它允许开发人员将日志写入自动滚动的文件中,从而避免单个日志文件变得过大。此项目特别适用于那些需要长时间运行并持续记录大量数据的应用场景。通过管理日志大小和自动归档,Lumberjack简化了日志管理和维护的过程。 项目快速启动 要开始使用Lumberjack,首先确保...

gitblog_00524的博客 1070

go 设置滚动日志

通过log/slog实现结构化日志生成,通过lumberjack实现日志文件分割。

秋雨夕照的博客 1714

golang lumberjack 日志包

Lumberjackloglogruszap` 等)结合使用。它的核心功能是自动管理日志文件的切割、备份和清理,避免日志文件无限增长占用磁盘空间。当单个日志文件大小超过设定阈值(如100MB)时,自动创建新文件,旧文件按时间戳重命名备份。支持设置日志文件的最大保留时间(如保留最近7 天的日志),超时文件自动删除。可设置最大备份文件数(如最多保留5 个备份),超出数量时删除最旧的日志。支持并发写入,适用于高并发场景。

zfj321的博客 752

Golang zap和lumberjack实现日志存储和自动管理

期望实现功能 按不同的日志级别存储到不同的日志文件中(这里只实现了info和error级别) 控制最多保留的日志文件及保留最近多少天的日志文件(控制日志的最大磁盘使用量) 控制每个日志文件的大小 我们使用zap和lumberjack框架实现 go get -u go.uber.org/zap go get gopkg.in/natefinch/lumberjack.v2 lumberjack的Logger结构体如下 type Logger struct { //写日志的..

skh2015java的博客 4774

ZAP日志框架&lumberjack日志归档库的分析使用

本次我们从官方例程的角度出发,来分析学习如何让zap日志框架动起来1、加糖版 2、正常高性能版 官方例程中总共列举出了两种打log的方法,和,这两种的区别是logger的性能优于sugar,但是sugar提供了格式化输出,而logger不支持,所以根据自己的需要,来选择两种不同的log方式。 通过zap.newxxx的方法来生成一个logger对象,我们在编程时可以看到,zap总共提供了三种默认的模式和一种自定义模式 四种模式对比分析 1&2&3 因为1、2、3只有配置不同,其他都相同,所以合并分析 在源

qq_44011116的博客 1549

使用Gin框架快速构建Web项目

Gin是Golang的一个web框架, 封装比较优雅, API友好, 源码注释比较明确, 具有快速灵活, 容错方便等特点.对于想我这样的Go语言初学者, 选用Gin来构建Web服务是一个不错的选择.构建一个简单的Web项目,除了Gin框架还用到的库有创建Go的项目,目录如图所示:引入gin: 2 解析配置文件 创建配置文件初始化函数,使用viper解析配置文件, 将yaml文件的内容映射到结构体中,使用哪个文件可以在启动命令中指定。项目启动的时候在主函数调用此函数初始化配置文件。 2.3

weixin_43357992的博客 1304
上一篇: git提示error setting certificate verify locations错误解决
下一篇: Golang zap和lumberjack实现日志存储和自动管理
思维的深度
博客等级 码龄11年 2万+粉丝 391原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值