当前位置:首页 > 文章列表 > Golang > Go问答 > Go runtime/metrics.Read 怎样复用样本缓冲区

Go runtime/metrics.Read 怎样复用样本缓冲区

来源:17golang原创 2026-11-02 03:53:23 0浏览 收藏

如果程序每隔几秒读取一次 Go 运行时指标,不需要每轮都重新创建 []metrics.Sample。更合适的做法是:先创建一个长期持有的样本切片,为每个元素设置一次 Name,之后反复调用 runtime/metrics.Read,让它更新对应的 Value。

真正需要注意的是复用的边界:一次 Read 返回前不要读取或修改这批 Value;多个 Read 并发执行时,传入的切片不能共享底层数组;如果要把直方图等指针型结果交给其他 goroutine,还要先复制其底层数据。

结论:串行定时采样可以复用同一个 []metrics.Sample,但“复用”只适用于样本缓冲区,不等于可以在下一轮 Read 进行时继续使用上一轮的 Value。
Go runtime metrics Read 复用同一 metrics.Sample 切片并更新 Value 的结构示意图
图1:同一组 metrics.Sample 在多次定时采样中复用,Name 保持不变、Value 随 Read 更新;这是静态说明图,不是运行截图。

先把 Name 和 Value 的职责分开

metrics.Sample 只有两个关键字段:Name 指定要读取的指标名称,Value 保存运行时返回的值。Read 接收的是一整个样本切片,它会填充每个元素的 Value,并不会要求调用方为每一次采样重新构造样本对象。

因此,定时采样器应该把“指标选择”放在初始化阶段,把“读取结果”放在循环阶段。这样既减少了切片和样本对象的重复分配,也让采样代码更容易确认每个结果对应哪个指标。

package main

import (
	"fmt"
	"runtime/metrics"
)

// sampler 持有长期复用的样本缓冲区;Name 初始化后不再改变。
type sampler struct {
	samples []metrics.Sample
}

// newSampler 把本次采样需要的指标名写入切片,只执行一次。
func newSampler(names ...string) *sampler {
	samples := make([]metrics.Sample, len(names))
	for i, name := range names {
		// Name 决定 Read 这一项要填充哪一个运行时指标。
		samples[i].Name = name
	}
	return &sampler{samples: samples}
}

func main() {
	s := newSampler(
		"/sched/goroutines:goroutines",
		"/memory/classes/heap/free:bytes",
	)

	// 每次调用都复用 s.samples 的底层数组。
	metrics.Read(s.samples)
	for _, sample := range s.samples {
		// Kind 先决定读取方法,避免用错 Uint64 或 Float64。
		fmt.Printf("%s: %v\\n", sample.Name, sample.Value.Kind())
	}
}

这里的切片可以由采样器长期持有。后续再次调用 metrics.Read(s.samples) 时,Name 仍然是同一组指标名,运行时只需要更新 Value。如果指标集合发生变化,再显式创建新切片,不要在一轮读取过程中修改正在使用的切片结构。

定时采样时复用同一切片

实际程序通常不会只读一次,而是把读取动作放进 ticker 循环。关键顺序是“调用 Read,等待它返回,再读取 Value,最后进入下一轮”。不要把同一切片同时交给一个后台 goroutine 处理,又马上在主循环中发起下一次 Read。

runtime metrics Read 串行复用与并发使用不同底层内存的安全边界图
图2:串行复用同一 slice 与并发使用不同底层内存的安全边界说明图,不是运行截图或性能测试结果。
package main

import (
	"fmt"
	"runtime/metrics"
	"time"
)

// readUint64 读取一组已知为 uint64 的指标,并返回标量副本。
func readUint64(samples []metrics.Sample) map[string]uint64 {
	// Read 完成前不要读取 samples 中的 Value。
	metrics.Read(samples)
	result := make(map[string]uint64, len(samples))
	for _, sample := range samples {
		// 未支持的名称会得到 KindBad,不能继续调用 Uint64。
		if sample.Value.Kind() != metrics.KindUint64 {
			continue
		}
		// 写入 map 后,标量值与下一轮 Read 使用的 Value 脱离。
		result[sample.Name] = sample.Value.Uint64()
	}
	return result
}

func main() {
	samples := []metrics.Sample{
		{Name: "/sched/goroutines:goroutines"},
		{Name: "/memory/classes/heap/free:bytes"},
	}
	ticker := time.NewTicker(5 * time.Second)
	defer ticker.Stop() // 退出时释放 ticker,避免留下定时资源。

	for range ticker.C {
		// 本轮读取和消费完成后,下一轮仍复用同一个 samples 切片。
		fmt.Println(readUint64(samples))
	}
}

这个示例复用了样本切片,但每轮返回一个新的 map。原因是 map 中保存的是已经取出的 uint64 副本,后续 Read 更新样本的 Value 时,不会改写已经交给日志、聚合器或消息队列的数据。

按 ValueKind 读取,不要假设所有指标都是整数

Value 通过 Kind() 表示值的类型。常见的三种有效类型是 KindUint64、KindFloat64 和 KindFloat64Histogram。对应的读取方法分别是 Uint64()、Float64() 和 Float64Histogram();调用与实际类型不匹配的方法会 panic。

如果程序只关心固定指标,可以根据官方描述确认它的 kind 后直接读取;如果程序要适配不同 Go 版本或不同实现,更稳妥的做法是从 metrics.All() 获取支持的指标集合,并对未知类型保留一个降级分支。

// readValue 展示三种有效 ValueKind 的分派方式。
func readValue(sample metrics.Sample) (any, bool) {
	value := sample.Value
	switch value.Kind() {
	case metrics.KindUint64:
		// 整数指标复制出一个独立的标量值。
		return value.Uint64(), true
	case metrics.KindFloat64:
		// 浮点指标同样在当前 Read 完成后立即取出。
		return value.Float64(), true
	case metrics.KindFloat64Histogram:
		// 直方图先交给专门的复制函数,避免把运行时数据指针直接传出去。
		return cloneHistogram(value.Float64Histogram()), true
	case metrics.KindBad:
		// 名称不存在或不再支持时,调用方应记录并跳过这一项。
		return nil, false
	default:
		// 新增 kind 时先安全降级,避免错误调用读取方法。
		return nil, false
	}
}

什么时候必须停止复用,改用独立内存

如果所有工作都在同一个 goroutine 中,并且上一轮 Read 已经返回,复用同一切片通常就是最简单的方案。问题出现在并发边界:一边执行 Read,另一边读取或修改同一个 Value,即使业务上只是“读监控数据”,也可能形成数据竞争。

多个 Read 可以并发执行,但它们的参数必须不共享底层内存。也就是说,两个 goroutine 不能同时使用同一个 samples 切片;应该分别创建两份切片,或者为每个 worker 使用独立的缓冲区。

尤其要注意 Float64Histogram:它返回的是指针型值,底层存储在条件允许时也可能被后续 Read 复用。需要异步处理时,先复制 Counts 和 Buckets,再把副本交给其他 goroutine。

// cloneHistogram 深拷贝直方图的两个底层切片,切断与下一轮 Read 的共享。
func cloneHistogram(src *metrics.Float64Histogram) *metrics.Float64Histogram {
	if src == nil {
		return nil
	}
	return &metrics.Float64Histogram{
		// Counts 是每个桶的计数,必须复制而不是只复制切片头。
		Counts: append([]uint64(nil), src.Counts...),
		// Buckets 是桶边界,同样需要独立的底层数组。
		Buckets: append([]float64(nil), src.Buckets...),
	}
}

如果不想处理深拷贝,也可以在调用方约定“只在当前采样函数返回前消费结果”,不要把 Value 或其中的指针继续向外传。对于两个独立的并发采样任务,直接各自创建 []metrics.Sample 是更直观的隔离方式,代价只是增加少量分配。

把复用策略落成一张检查表

场景推荐做法原因
单 goroutine 定时采样复用同一 []metrics.Sample串行调用,上一轮完成后再读取下一轮
多个 goroutine 同时 Read每个任务使用独立切片避免参数共享底层内存
把整数或浮点值交给异步任务先取出标量副本脱离当前 Sample 的 Value
把直方图交给异步任务深拷贝 Counts 和 Buckets避免底层存储被后续 Read 复用
指标名可能不存在检查 KindBad 并跳过未知名称不能按有效值读取

常见追问

每次调用 Read 都要重新设置 Name 吗?

不需要。只要指标名称没有变化,初始化一次后可以反复复用同一组 Sample。如果要换指标集合,再创建或重建对应的样本切片。

复用切片会不会让上一轮数据丢失?

会覆盖样本中的 Value,所以要保留历史数据时必须在下一轮读取前复制需要的字段。整数和浮点值直接取成标量,直方图则复制其底层切片。

可以把同一个 samples 传给两个 goroutine 吗?

不能让两个并发的 Read 或一个 Read 与另一个消费者共享同一底层内存。并发采样使用独立切片;异步消费使用完成后的深拷贝。

官方资料在哪里?

官方文档:https://pkg.go.dev/runtime/metrics

Read 源码:https://go.dev/src/runtime/metrics/sample.go

记住“样本切片可复用,正在使用的 Value 不可跨边界共享”这一句,基本就能覆盖 runtime/metrics.Read 的性能与并发取舍。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go unsafe.Pointer 转换为什么可能让 GC 失去追踪Go unsafe.Pointer 转换为什么可能让 GC 失去追踪
上一篇
Go unsafe.Pointer 转换为什么可能让 GC 失去追踪
Go comparable 约束为什么拒绝 interface 类型
下一篇
Go comparable 约束为什么拒绝 interface 类型
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    838次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    835次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    919次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    894次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    718次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码