Skip to content

Go 常见优化点:把成本降下来,也把语义保住 ​

优化最常见的失误,是还不知道哪个成本占主导,就开始换容器、加对象池或替换 JSON 库。一次修改也许让微基准好看,却留下更大的常驻内存、更长的排队时间或不同的协议行为。

这篇按证据选择改法:分配多时检查容量与逃逸,字符串中间对象多时用 Builder,等待多时检查锁与批量边界,反射和 JSON 真在热路径上时再评估替代方案,最后用 PGO 把代表性 CPU profile 交回编译器。例子均在 Go 1.27.2、Linux/amd64 运行;没有对线上进程做采样或压测。

先选目标,再看哪个改动值得做 ​

看到的证据候选改动主要收益来源要同时检查的代价
allocs 中 slice 扩容占比高已知规模时预分配少扩容、少复制中间数组过度预留、输入长度攻击、nil 与空切片差异
相同临时对象反复分配sync.Pool复用可丢弃的缓冲常驻容量、所有权、脏数据、池中对象可随时消失
编译器报告不必要逃逸缩小生命周期、改变数据组织减少堆对象和 GC 工作大值复制、接口设计复杂化;不保证改成指针更快
字符串循环拼接制造大量对象strings.Builder、必要时 Grow减少中间字符串Builder 不能复制,过大 Grow 也占内存
block/mutex profile 指向共享热点缩短临界区、分片或本地聚合少等待、少竞争一致性与可见性改变;分片仍可能有热点
每条操作都有固定调用/锁/网络成本有界批处理把固定成本摊到多条记录等待成批、部分失败、重试幂等、内存上限
CPU profile 指向动态字段查找缓存元数据、直接访问、代码生成少反射查找和动态处理更复杂的维护与生成流程
JSON 编解码是实测热点调整模型、流式 API、兼容替代少中间表示或更合适实现数字、空值、重复键、大小写、错误等兼容性
主要成本是可优化的 CPU 热路径PGO给编译器真实热点信息profile 代表性、更新、构建可复现性

表格是选项,不是固定优化顺序。若主要时间在数据库锁等待,换 JSON 库可能几乎不改变请求延迟。先保存基线,修改一个主要变量,验证正确性,再用同等负载看目标指标和副作用。[1]

正在绘制图示…

可切换适合窗口或原始尺寸
查看图源代码
flowchart LR
    A[目标:例如 p99 或 CPU/请求] --> B[代表性工作量和基线]
    B --> C[CPU / allocs / heap / block / trace 证据]
    C --> D[一个可解释的修改]
    D --> E[行为测试与 race]
    E --> F[重复性能测量]
    F --> G{目标改善且副作用可接受?}
    G -->|是| H[受控发布并保留回退]
    G -->|否| B
放大图示

原始尺寸下可横向或纵向滚动;按 Escape 关闭。

正在绘制图示…

查看图源代码
flowchart LR
    A[目标:例如 p99 或 CPU/请求] --> B[代表性工作量和基线]
    B --> C[CPU / allocs / heap / block / trace 证据]
    C --> D[一个可解释的修改]
    D --> E[行为测试与 race]
    E --> F[重复性能测量]
    F --> G{目标改善且副作用可接受?}
    G -->|是| H[受控发布并保留回退]
    G -->|否| B

预分配:先知道规模,也知道返回值该是什么 ​

两个函数做同一件事:返回 0 到 n-1。区别只在于是否提前给出容量。

go
func GrowSlice(n int) []int {
    var out []int
    for i := 0; i < n; i++ { out = append(out, i) }
    return out
}
func ReserveSlice(n int) []int {
    out := make([]int, 0, n)
    for i := 0; i < n; i++ { out = append(out, i) }
    return out
}

追加超出容量时,需要获得新的底层存储并复制旧内容;具体扩容容量是实现细节,不应把“始终翻倍”写成语言保证。已知最终数量时,预留恰当容量可以消掉中间增长。[2]

本次每个变体各测 10 次、100 ms/次、GOMAXPROCS=1,slice 结果都写入相同全局 sink,明确模拟返回值逃到调用方保存的情形:

n=1024 的变体ns/op 中位数B/opallocs/op
逐步增长2873,±7%2520811
预留 1024 容量1277,±8%81921

这台机器的 int 是 8 字节,最终 1024 个 int 需要 8192 字节。逐步增长的累计分配大于最终数组,是中间数组也算进来了。测试里的 AllocsPerRun 调用点还测到 9 对 1 次分配;调用点与编译优化上下文不同,次数不是 API 承诺。这也说明不能从一处的分配数字机械推到所有调用方。

预分配还有一个很小、却真实的语义变化:

text
zero length nil: grow=true reserve=false

n=0 时,前者返回 nil,后者返回非 nil 的空切片。业务若区分二者,或 JSON 协议对 null 与 [] 有约定,就需要保留原语义。n 来源于不可信输入时,还必须检查上限和溢出,不能为了省分配就允许一次预留任意大容量。

减少逃逸:检查原因,不追求“全都上栈” ​

局部变量取地址,并不自动意味着分配在堆上;关键在于编译器是否能证明地址不会在栈帧失效后继续被使用。下面两个函数都取了 n 的地址,但生命周期不同。为使演示边界稳定,代码显式禁止这两个小函数内联。

go
//go:noinline
func Local(n int) int { p := &n; return *p }

//go:noinline
func Escaped(n int) *int { return &n }

测试把第二个返回的指针保存起来,真实输出是:

text
int allocations: local=0 escaped=1

go test -gcflags='-m=2' -run='^$' . 同时报告 Escaped 中的 n escapes to heap、moved to heap: n。这是当前实现的编译决策,代码形状、内联、调用者用途和工具链都会影响它。生产代码不要为了复现这个数字随意加 //go:noinline。[3]

实用动作是检查热路径上是否把短命值装入长期容器,是否为很小的数据建立不必要的指针图,是否有无意的闭包捕获。减少堆对象通常也减少 GC 需要处理的工作,但强行按值复制大对象可能增加 CPU 和栈成本。把 -m 当解释器,和 -benchmem、profile 一起用,别把“出现 escapes”本身当成 bug。[1][3]

sync.Pool:借用期间只有一个所有者 ​

sync.Pool 用于复用临时对象,不是可靠缓存、固定容量资源池或连接池。Get 可以忽略池中已有对象;任何池中对象都可能被移除,所以程序必须在一次复用也没发生时仍然正确。New 常返回指针,以避免把非指针值装进接口时引入额外分配。[4]

本文的 EncodePooled 借一个 *bytes.Buffer,Reset 后交给 JSON Encoder,返回前复制结果,只有容量不超过 64 KiB 的缓冲才归还。

go
b := buffers.Get().(*bytes.Buffer)
b.Reset()
defer func() {
    if b.Cap() <= 64<<10 { b.Reset(); buffers.Put(b) }
}()
if err := json.NewEncoder(b).Encode(v); err != nil { return nil, err }
return bytes.Clone(b.Bytes()), nil

复制看似又多了一次分配,却解决所有权:调用方拿到的字节不能随着下一次借用被覆盖。测试保存第一次结果,再编码十次不同数据,断言第一次结果没有变化。对象放回 Pool 之后,旧持有者就不能继续访问它。[4][5]

正在绘制图示…

可切换适合窗口或原始尺寸
查看图源代码
flowchart LR
    P[Pool:可随时丢弃临时对象] -->|Get| O[当前借用者独占 Buffer]
    O --> W[Reset 并写入]
    W --> C[复制结果交给调用方]
    W --> Q{容量是否超过本例 64 KiB 上限?}
    Q -->|否| R[Reset 后 Put]
    R --> P
    Q -->|是| D[不归还,让对象可被回收]
放大图示

原始尺寸下可横向或纵向滚动;按 Escape 关闭。

正在绘制图示…

查看图源代码
flowchart LR
    P[Pool:可随时丢弃临时对象] -->|Get| O[当前借用者独占 Buffer]
    O --> W[Reset 并写入]
    W --> C[复制结果交给调用方]
    W --> Q{容量是否超过本例 64 KiB 上限?}
    Q -->|否| R[Reset 后 Put]
    R --> P
    Q -->|是| D[不归还,让对象可被回收]

64 KiB 是本例的选择,不是通用最佳值。应按大小分布与复用命中情况决定;把偶发的 20 MiB 缓冲长期留在池里,可能降低分配次数,却提高存活内存。Reset 也不是安全擦除,敏感内容是否允许复用、是否需要清零,要按数据处理要求判断。本篇只验证 Pool 用法的正确性,没有声称这一版编码一定比 json.Marshal 更快。

strings.Builder:减少中间字符串,别复制已使用的 Builder ​

循环 s += part 不断产生新字符串,Builder 可以集中累积。若能低成本知道总长度,先 Grow(total),还能避免其自身多次扩容。计算总长度也要工作,因此短字符串、两三个片段不一定值得额外一遍扫描。[6]

本次拼接 128 个 "go中",每种 10 次的摘要:

Builder 写法ns/op 中位数B/opallocs/op
不提前 Grow797.4,±2%19128
按总字节数 Grow571.4,±12%6401

这里收益来自特定输入规模。一个已经使用过的 Builder 不能按值复制;负向测试实际触发并 recover 了:

text
copy: strings: illegal use of non-zero Builder copied by value

需要在函数间传递它时用指针,并保持清楚的生命周期;不要把带 Builder 的结构体悄悄复制。返回字符串后,长期保留一个很短的字符串是否连带留住过大的底层存储,也值得用 heap profile 检查。[6]

锁竞争与批处理:少拿几次锁,改变的可能还有可见性 ​

如果 mutex profile 表明一把共享锁让大量 goroutine 等待,先缩短临界区:移走 I/O、日志格式化和可在锁外完成的计算。不是看见锁就改成 RWMutex;读写比、持锁时长、缓存一致性和热点分布都影响结果。[4]

本例累计一组整数,先在本地求和,最后只拿一次锁:

go
func (c *Counter) AddBatch(xs []int) {
    sum := 0
    for _, x := range xs { sum += x }
    c.mu.Lock()
    c.n += sum
    c.mu.Unlock()
}

与每个元素都 Lock/Unlock 的版本比较,4 个 goroutine 分别添加 [1,2,3],两者最终值都被断言为 24,race 检查通过。这里验证的是最终总和;逐条更新原本可能被其他读者观察到,批量更新只在批次完成时可见,不能把它当作任何业务语义下都完全等价。

真实批处理还需要“最多多少条、最多等多久”两个上限:只设条数,低流量请求可能一直等;只设时间,高流量会堆很大一批。批量数据库/网络调用要定义部分失败和幂等重试,停机时要 flush 或明确舍弃策略。分片计数器、单 goroutine 拥有状态、atomic、RWMutex 都各有适用条件;用 profile 与代表性并发测试决定,别让一个热点锁变成一个热点队列。

反射与 JSON:先看热点,再看兼容性 ​

反射常见成本来自动态查找、类型判断、接口装箱和无法利用的静态信息。例如按名字读字段:

go
reflect.ValueOf(v).FieldByName("ID").Int()

如果类型已经固定,直接 v.ID 更简单;类型动态但模式有限,可以缓存按 reflect.Type 建立的字段元数据,避免每条记录重新查找。需要大量已知 schema 时再考虑代码生成,同时付出生成器版本、产物更新和调试成本。本文测试了直接读取与反射读取结果一致,没有给未测的反射场景编造加速倍数。[7]

JSON 更不能仅靠把 import 换掉结束评估。Go 1.27.2 的 encoding/json 与 encoding/json/v2 是当前就能复现语义差异的一对例子:

text
v1: {"id":0,"tags":null}
v2: {"id":0,"tags":[]}
uppercase key: v1=7 v2=0
duplicate rejected: v1=false v2=true

结构体标签是 json:"id"。输入 {"ID":7} 在 v1 被大小写不敏感匹配,在 v2 的默认行为下没有匹配;重复 id 键在 v1 默认接受、后值覆盖,在 v2 默认拒绝。[8]

替换或升级前的回归集为什么重要
nil/空切片与 map,omitempty/omitzero影响 null、[]、{}、字段是否出现
大整数、浮点边界、解码到 any避免 ID 精度与类型变化
大小写、重复键、未知字段影响兼容性、安全检查与容错
无效 UTF-8、HTML 转义、map 顺序影响外部协议、签名/缓存键和文本比较
自定义 Marshaler/Unmarshaler外部库是否遵守项目现有约定
流式输入、错误时部分修改、最大输入长度影响内存上限、恢复与攻击面

先锁定这些行为,再基准相同语义和输入。解码到具体结构体、避免先 map[string]any 再转一遍,可能比换库更容易保持语义;流式 API 有助于控制中间缓冲,但不是自动的总输入上限。任何第三方替代还要检查维护、漏洞、平台支持和依赖成本。本篇没有用语义不同的两个 JSON 默认实现做速度排名。

PGO:把代表性 profile 变成构建输入 ​

PGO 使用 CPU pprof profile 帮助编译器判断热点,例如决定哪些调用值得更积极内联。它不使用 heap profile 来“学习内存”,也不会修复锁竞争、数据库慢查询或算法错误。[9]

常用入口有三个:主包目录的 default.pgo 由默认 -pgo=auto 发现;-pgo=/absolute/path/cpu.pprof 显式选文件;-pgo=off 明确关闭,便于基线比较。显式路径会作用到该构建命令涉及的主包,多个不同程序通常应分别选 profile。[9]

本次真实执行了以下流程:短 CPU 基准生成 train.pprof → 当前 pprof 成功解析 → go test -pgo=... 通过全部测试 → 生成 PGO 测试二进制。训练样本只覆盖本文的字符串小例子,所以这里只验证“采样到构建”的链路,没有声称得到生产收益。

生产流程应选有代表性的请求比例、输入大小和运行阶段,保存采样版本、时长与权重。把不同时长的 profile 简单相加会让长窗口贡献更多样本,合并前要确认这正是想要的权重。代码大幅重构、业务流量变化后要更新 profile;源码、工具链和 profile 一起固定,构建才有可复现的依据。再用独立于训练输入的工作量比较 PGO 开关,避免只在训练小例子上验证。[9]

本次测量怎样使用 ​

上述时间摘要来自 AMD EPYC 9V74、共享 Linux 云机器,标准优化构建、每变体 10 次、100 ms、-cpu=1;benchstat 固定到 v0.0.0-20261009192801-be2c69fb417e。这里展示各变体的中位数和 95% 置信区间,没有做成对 A/B 显著性断言。完整原始行见文末。race 仅用于独立正确性检查,不与普通构建计时混在一起。

测试能证明这些输入的结果、所有权和反例行为,不能证明对象池命中率、生产 p99 或新 JSON 实现的全面兼容性。下一次优化最值得留下的记录,是“哪份证据让我们选了这条改法”,而不只是最终少了几个纳秒。

依据 ​

  1. Go diagnostics、Go GC guide:按成本选诊断方式与分配/GC 权衡。
  2. Go 规范:append;Go 1.27.2 slice 实现:容量增长与实现边界。
  3. Go FAQ:堆或栈;本机 Go 1.27.2 -gcflags=-m=2 输出。
  4. Go 1.27.2 sync:Pool、Mutex、RWMutex 约定。
  5. Go 1.27.2 bytes.Buffer:Bytes 的共享存储与有效期。
  6. Go 1.27.2 strings.Builder,固定源码:Grow、String、copyCheck。
  7. Go 1.27.2 reflect:动态字段访问 API,速度不作未测推断。
  8. Go 1.27.2 encoding/json 与 encoding/json/v2:迁移差异与默认语义。
  9. Go 官方 PGO 文档:CPU profile、代表性、构建入口、合并权重与版本偏差。

完整源码、测试与脚本 ​

使用 Go 1.27.2,无第三方运行依赖。按文件名保存后执行 bash run.sh out。测试故意触发的 Builder panic 已局部 recover,只验证禁止复制的行为。PGO profile 是脚本生成的输入文件,不需要从别处下载。

go.mod ​

go.mod
module example.com/optimizelab

go 1.27.2

optimize.go ​

代码块 · 93 行 · 点击收起 / 展开
go
package optimizelab

import (
	"bytes"
	"encoding/json"
	"reflect"
	"strings"
	"sync"
)

func GrowSlice(n int) []int {
	var out []int
	for i := 0; i < n; i++ {
		out = append(out, i)
	}
	return out
}
func ReserveSlice(n int) []int {
	out := make([]int, 0, n)
	for i := 0; i < n; i++ {
		out = append(out, i)
	}
	return out
}
func Build(parts []string, reserve bool) string {
	var b strings.Builder
	if reserve {
		n := 0
		for _, p := range parts {
			n += len(p)
		}
		b.Grow(n)
	}
	for _, p := range parts {
		b.WriteString(p)
	}
	return b.String()
}

var buffers = sync.Pool{New: func() any { return new(bytes.Buffer) }}

func EncodePooled(v any) ([]byte, error) {
	b := buffers.Get().(*bytes.Buffer)
	b.Reset()
	defer func() {
		if b.Cap() <= 64<<10 {
			b.Reset()
			buffers.Put(b)
		}
	}()
	if err := json.NewEncoder(b).Encode(v); err != nil {
		return nil, err
	}
	// The caller owns this copy; the buffer is reusable after return.
	return bytes.Clone(b.Bytes()), nil
}

type Counter struct {
	mu sync.Mutex
	n  int
}

func (c *Counter) AddOneByOne(xs []int) {
	for _, x := range xs {
		c.mu.Lock()
		c.n += x
		c.mu.Unlock()
	}
}
func (c *Counter) AddBatch(xs []int) {
	sum := 0
	for _, x := range xs {
		sum += x
	}
	c.mu.Lock()
	c.n += sum
	c.mu.Unlock()
}
func (c *Counter) Value() int { c.mu.Lock(); defer c.mu.Unlock(); return c.n }

type Record struct {
	ID   int      `json:"id"`
	Tags []string `json:"tags"`
}

func DirectID(v Record) int  { return v.ID }
func ReflectID(v Record) int { return int(reflect.ValueOf(v).FieldByName("ID").Int()) }

//go:noinline
func Local(n int) int { p := &n; return *p }

//go:noinline
func Escaped(n int) *int { return &n }

optimize_test.go ​

代码块 · 151 行 · 点击收起 / 展开
go
package optimizelab

import (
	"bytes"
	"encoding/json"
	jsonv2 "encoding/json/v2"
	"fmt"
	"reflect"
	"strings"
	"sync"
	"testing"
)

var sliceSink []int
var intSink *int

func TestBehavior(t *testing.T) {
	for _, n := range []int{1, 16, 1024} {
		if !reflect.DeepEqual(GrowSlice(n), ReserveSlice(n)) {
			t.Fatal(n)
		}
	}
	parts := []string{"a", "中", "b"}
	if Build(parts, false) != Build(parts, true) {
		t.Fatal("builder")
	}
	for _, add := range []func(*Counter, []int){(*Counter).AddOneByOne, (*Counter).AddBatch} {
		c := new(Counter)
		var wg sync.WaitGroup
		for range 4 {
			wg.Go(func() { add(c, []int{1, 2, 3}) })
		}
		wg.Wait()
		if c.Value() != 24 {
			t.Fatal(c.Value())
		}
	}
	first, err := EncodePooled(Record{ID: 1})
	if err != nil {
		t.Fatal(err)
	}
	saved := bytes.Clone(first)
	for range 10 {
		if _, err := EncodePooled(Record{ID: 9}); err != nil {
			t.Fatal(err)
		}
	}
	if !bytes.Equal(first, saved) {
		t.Fatal("returned bytes were reused")
	}
	if DirectID(Record{ID: 7}) != ReflectID(Record{ID: 7}) {
		t.Fatal("reflect")
	}
}
func TestAllocationEvidence(t *testing.T) {
	a := testing.AllocsPerRun(20, func() { sliceSink = GrowSlice(1024) })
	b := testing.AllocsPerRun(20, func() { sliceSink = ReserveSlice(1024) })
	if b >= a {
		t.Fatalf("expected fewer allocations: %v %v", a, b)
	}
	fmt.Printf("slice allocations: grow=%.0f reserve=%.0f\n", a, b)
	a = testing.AllocsPerRun(20, func() { _ = Local(7) })
	b = testing.AllocsPerRun(20, func() { intSink = Escaped(7) })
	fmt.Printf("int allocations: local=%.0f escaped=%.0f\n", a, b)
	if a != 0 || b != 1 {
		t.Fatal(a, b)
	}
}
func TestBuilderCopyPanics(t *testing.T) {
	var b strings.Builder
	b.WriteString("a")
	c := b
	defer func() {
		if r := recover(); r == nil {
			t.Fatal("expected copied-builder panic")
		} else {
			fmt.Println("copy:", r)
		}
	}()
	c.WriteString("b")
}
func TestJSONDifferences(t *testing.T) {
	a, err := json.Marshal(Record{})
	if err != nil {
		t.Fatal(err)
	}
	b, err := jsonv2.Marshal(Record{})
	if err != nil {
		t.Fatal(err)
	}
	fmt.Println("v1:", string(a))
	fmt.Println("v2:", string(b))
	var x, y Record
	if err := json.Unmarshal([]byte(`{"ID":7}`), &x); err != nil {
		t.Fatal(err)
	}
	if err := jsonv2.Unmarshal([]byte(`{"ID":7}`), &y); err != nil {
		t.Fatal(err)
	}
	fmt.Printf("uppercase key: v1=%d v2=%d\n", x.ID, y.ID)
	if x.ID != 7 || y.ID != 0 {
		t.Fatal(x, y)
	}
	err1 := json.Unmarshal([]byte(`{"id":1,"id":2}`), &x)
	err2 := jsonv2.Unmarshal([]byte(`{"id":1,"id":2}`), &y)
	fmt.Printf("duplicate rejected: v1=%v v2=%v\n", err1 != nil, err2 != nil)
	if err1 != nil || err2 == nil {
		t.Fatal(err1, err2)
	}
}
func BenchmarkSlice(b *testing.B) {
	for _, name := range []string{"grow", "reserve"} {
		b.Run(name, func(b *testing.B) {
			b.ReportAllocs()
			for b.Loop() {
				if name == "grow" {
					sliceSink = GrowSlice(1024)
				} else {
					sliceSink = ReserveSlice(1024)
				}
			}
		})
	}
}
func BenchmarkBuilder(b *testing.B) {
	parts := make([]string, 128)
	for i := range parts {
		parts[i] = "go中"
	}
	for _, reserve := range []bool{false, true} {
		b.Run(fmt.Sprint(reserve), func(b *testing.B) {
			b.ReportAllocs()
			for b.Loop() {
				_ = Build(parts, reserve)
			}
		})
	}
}
func BenchmarkHot(b *testing.B) {
	parts := []string{"alpha", "中", "omega"}
	for b.Loop() {
		_ = Build(parts, true)
	}
}

func TestZeroLengthSemantics(t *testing.T) {
	fmt.Printf("zero length nil: grow=%v reserve=%v\n", GrowSlice(0) == nil, ReserveSlice(0) == nil)
	if GrowSlice(0) != nil || ReserveSlice(0) == nil {
		t.Fatal("unexpected zero length behavior")
	}
}

run.sh ​

bash
#!/usr/bin/env bash
set -euo pipefail
export TZ=UTC
out=${1:-out}
mkdir -p "$out"
{ go version; go env GOOS GOARCH CGO_ENABLED; grep -m1 'model name' /proc/cpuinfo; } > "$out/environment.txt"
go test -v . > "$out/test.txt"
go test -race . > "$out/race.txt"
go vet . > "$out/vet.txt"
go test -run='^$' -bench='Benchmark(Slice|Builder)$' -benchmem -benchtime=100ms -count=10 -cpu=1 > "$out/bench.txt"
go test -run='^$' -bench='^BenchmarkHot$' -pgo=off -benchtime=500ms -cpu=1 -cpuprofile="$out/train.pprof" -o "$out/train.test" > "$out/train.txt"
go tool pprof -top "$out/train.test" "$out/train.pprof" > "$out/train-top.txt"
go test -pgo="$PWD/$out/train.pprof" . > "$out/pgo-test.txt"
go test -c -pgo="$PWD/$out/train.pprof" -o "$out/pgo.test" .
go test -gcflags='-m=2' -run='^$' . > "$out/escape-stdout.txt" 2> "$out/escape.txt"

统计摘要命令 ​

bash
mkdir -p bin
GOBIN="$PWD/bin" GOPROXY=https://proxy.golang.org GOSUMDB=sum.golang.org \
  go install golang.org/x/perf/cmd/benchstat@v0.0.0-20261009192801-be2c69fb417e
./bin/benchstat out/bench.txt

本次全部性能测量行 ​

代码块 · 40 行 · 点击收起 / 展开
text
BenchmarkSlice/grow         	   36056	      3062 ns/op	   25208 B/op	      11 allocs/op
BenchmarkSlice/grow         	   37762	      3043 ns/op	   25208 B/op	      11 allocs/op
BenchmarkSlice/grow         	   41712	      3073 ns/op	   25208 B/op	      11 allocs/op
BenchmarkSlice/grow         	   39757	      2887 ns/op	   25208 B/op	      11 allocs/op
BenchmarkSlice/grow         	   44893	      2808 ns/op	   25208 B/op	      11 allocs/op
BenchmarkSlice/grow         	   39680	      2858 ns/op	   25208 B/op	      11 allocs/op
BenchmarkSlice/grow         	   42763	      2809 ns/op	   25208 B/op	      11 allocs/op
BenchmarkSlice/grow         	   40262	      2804 ns/op	   25208 B/op	      11 allocs/op
BenchmarkSlice/grow         	   41823	      2816 ns/op	   25208 B/op	      11 allocs/op
BenchmarkSlice/grow         	   41467	      2958 ns/op	   25208 B/op	      11 allocs/op
BenchmarkSlice/reserve      	   83647	      1444 ns/op	    8192 B/op	       1 allocs/op
BenchmarkSlice/reserve      	   88416	      1381 ns/op	    8192 B/op	       1 allocs/op
BenchmarkSlice/reserve      	   94906	      1253 ns/op	    8192 B/op	       1 allocs/op
BenchmarkSlice/reserve      	   95439	      1277 ns/op	    8192 B/op	       1 allocs/op
BenchmarkSlice/reserve      	   96876	      1304 ns/op	    8192 B/op	       1 allocs/op
BenchmarkSlice/reserve      	   97070	      1257 ns/op	    8192 B/op	       1 allocs/op
BenchmarkSlice/reserve      	   92954	      1285 ns/op	    8192 B/op	       1 allocs/op
BenchmarkSlice/reserve      	   98426	      1277 ns/op	    8192 B/op	       1 allocs/op
BenchmarkSlice/reserve      	   96687	      1240 ns/op	    8192 B/op	       1 allocs/op
BenchmarkSlice/reserve      	   96697	      1274 ns/op	    8192 B/op	       1 allocs/op
BenchmarkBuilder/false      	  148002	       806.6 ns/op	    1912 B/op	       8 allocs/op
BenchmarkBuilder/false      	  154075	       794.8 ns/op	    1912 B/op	       8 allocs/op
BenchmarkBuilder/false      	  150734	       800.6 ns/op	    1912 B/op	       8 allocs/op
BenchmarkBuilder/false      	  145501	       775.4 ns/op	    1912 B/op	       8 allocs/op
BenchmarkBuilder/false      	  154795	       789.2 ns/op	    1912 B/op	       8 allocs/op
BenchmarkBuilder/false      	  150596	       797.2 ns/op	    1912 B/op	       8 allocs/op
BenchmarkBuilder/false      	  159316	       797.6 ns/op	    1912 B/op	       8 allocs/op
BenchmarkBuilder/false      	  166291	       785.2 ns/op	    1912 B/op	       8 allocs/op
BenchmarkBuilder/false      	  148338	       802.0 ns/op	    1912 B/op	       8 allocs/op
BenchmarkBuilder/false      	  157102	       821.4 ns/op	    1912 B/op	       8 allocs/op
BenchmarkBuilder/true       	  241076	       530.6 ns/op	     640 B/op	       1 allocs/op
BenchmarkBuilder/true       	  207992	       612.3 ns/op	     640 B/op	       1 allocs/op
BenchmarkBuilder/true       	  234565	       565.5 ns/op	     640 B/op	       1 allocs/op
BenchmarkBuilder/true       	  222883	       560.8 ns/op	     640 B/op	       1 allocs/op
BenchmarkBuilder/true       	  199386	       646.8 ns/op	     640 B/op	       1 allocs/op
BenchmarkBuilder/true       	  176230	       623.7 ns/op	     640 B/op	       1 allocs/op
BenchmarkBuilder/true       	  222014	       566.2 ns/op	     640 B/op	       1 allocs/op
BenchmarkBuilder/true       	  224803	       576.6 ns/op	     640 B/op	       1 allocs/op
BenchmarkBuilder/true       	  208192	       565.3 ns/op	     640 B/op	       1 allocs/op
BenchmarkBuilder/true       	  230055	       640.7 ns/op	     640 B/op	       1 allocs/op

保留各书来源与许可 · 示例运行条件以正文为准