郑文峰的博客 郑文峰的博客
首页
  • Go语言高性能编程
分类
标签
归档
关于
  • 导航 (opens new window)
  • 代码片段 (opens new window)
  • 收藏
  • 友链
  • 外部页面

    • 开往 (opens new window)
GitHub (opens new window)

zhengwenfeng

穷则变,变则通,通则久
首页
  • Go语言高性能编程
分类
标签
归档
关于
  • 导航 (opens new window)
  • 代码片段 (opens new window)
  • 收藏
  • 友链
  • 外部页面

    • 开往 (opens new window)
GitHub (opens new window)
  • python

  • go语言

    • go简单使用grpc
    • gin中validator模块的源码分析
    • 优化gin表单的错误提示信息
    • go中如何处理error
    • tcp缓存引起的日志丢失
    • 使用etcd分布式锁导致的协程泄露与死锁问题
    • go语言高性能编程

      • Go协程池深度解析:原理、实现与最佳实践
      • Go语言Interface Boxing原理与性能优化指南
        • 前言
        • Interface Boxing原理
        • 基础示例分析
          • 值类型赋值
          • 对比具体类型
        • 结构体示例
          • 值类型结构体
          • 指针类型结构体
        • 性能基准测试
          • 切片操作性能
          • 函数调用性能
        • 什么时候允许Interface Boxing?
        • 最佳实践
      • Go语言遍历性能深度解析:从原理到优化实践
      • Go语言零拷贝技术完全指南
      • Go语言不可变数据共享:无锁并发编程实践
      • Go语言内存预分配完全指南
      • Go语言原子操作完全指南
      • Go语言堆栈分配与逃逸分析深度解析
      • Go语言空结构体:零内存消耗的高效编程
      • Go语言结构体内存对齐完全指南
      • Go语言字符串拼接性能对比与优化指南
      • Go语言延迟初始化(Lazy Initialization)最佳实践
      • Go语言高效IO缓冲技术详解
  • linux

  • 其他

  • 编程
  • go语言
  • go语言高性能编程
zhengwenfeng
2025-06-14
目录

Go语言Interface Boxing原理与性能优化指南

# 前言

在Go语言中,interface{} 是一种强大的抽象机制,但将具体类型赋值给 interface{} (称为Boxing)会带来一定的性能开销。本文将深入分析 interface boxing 的原理、性能影响及优化实践。

# Interface Boxing原理

将具体类型的值赋值给interface{}的过程称为Boxing。在这个过程中:

  1. 值会在堆上分配新内存并拷贝
  2. 将指针及对应类型赋值给interface{}变量
  3. 这会带来额外性能开销并增加GC压力

# 基础示例分析

# 值类型赋值

将整型 1 赋值给 interface{} 变量 demo 中

func main() {
	var demo interface{}
	demo = 1   // 发生boxing,整型1逃逸到堆
	fmt.Println(demo)
}
1
2
3
4
5

运行时添加 -gcflags="-m" 参数,查看逃逸分析的结果。可以看到 1 逃逸到堆中了。

$ go run -gcflags="-m" main.go 
# command-line-arguments
./main.go:12:13: inlining call to fmt.Println
./main.go:11:9: 1 escapes to heap
./main.go:12:13: ... argument does not escape
1
1
2
3
4
5
6

# 对比具体类型

我们在对比用具体的类型来赋值 1,可以看到是没有逃逸的,但是发现 demo 发生了逃逸到堆,这是为什么呢?

func main() {
	var demo int
	demo = 1
	fmt.Println(demo)
}

1
2
3
4
5
6
% go run -gcflags="-m" main.go 
# command-line-arguments
./main.go:12:13: inlining call to fmt.Println
./main.go:12:13: ... argument does not escape
./main.go:12:14: demo escapes to heap
1
1
2
3
4
5
6

查看 fmt.Println 的源码实现可以发现,其参数是 interface{} 类型,将 demo 传参给 Println 时,也会发生 Boxing 过程,也会发生在堆中申请新内存以及复制的过程,所以会发生逃逸。

type any = interface{}

func Println(a ...any) (n int, err error) {
	return Fprintln(os.Stdout, a...)
}
1
2
3
4
5

# 结构体示例

# 值类型结构体

将一个结构体赋值给 inteface{}

type Person struct {
	Name string
}

func main() {
	var demo interface{}
	demo = Person{}
	fmt.Println(demo)
}
1
2
3
4
5
6
7
8
9

可以看到 Person{} 是有发生逃逸的,这里就是发生了 Boxing

$ go run -gcflags="-m" main.go 
# command-line-arguments
./main.go:12:13: inlining call to fmt.Println
./main.go:11:15: Person{} escapes to heap
./main.go:12:13: ... argument does not escape
{}
1
2
3
4
5
6

# 指针类型结构体

再来看直接将指针赋值给 interface{}

type Person struct {
	Name string
}

func main() {
	var demo interface{}
	demo = &Person{}
	fmt.Println(demo)
}
1
2
3
4
5
6
7
8
9

可以看到这里依然发生了堆逃逸,这是因为 &Person{} 取地址操作本身就是在堆上申请内存的,然后将地址赋值给 interface{} 的变量,这里是没有发生 boxing 的。

go run -gcflags="-m" main.go 
# command-line-arguments
./main.go:12:13: inlining call to fmt.Println
./main.go:11:9: &Person{} escapes to heap
./main.go:12:13: ... argument does not escape
&{}
1
2
3
4
5
6

那么问题来了,这两种方式都会在堆上申请内存,那么两种方式是不是没有区别呢?

# 性能基准测试

# 切片操作性能

我们来对上面两种方式来进行 Benchmark 看看两者性能对比。


type Worker interface {
	Work()
}

type LargeJob struct {
	payload [4096]byte
}

func (LargeJob) Work() {}

func BenchmarkBoxedLargeSlice(b *testing.B) {
	jobs := make([]Worker, 0, 1000)
	for range b.N {
		jobs = jobs[:0]
		for j := 0; j < 1000; j++ {
			var job LargeJob
			jobs = append(jobs, job)
		}
	}
}

func BenchmarkPointerLargeSlice(b *testing.B) {
	jobs := make([]Worker, 0, 1000)
	for range b.N {
		jobs := jobs[:0]
		for j := 0; j < 1000; j++ {
			job := &LargeJob{}
			jobs = append(jobs, job)
		}
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

运行结果如下,可以看到两者内存申请是差不多的,但是效率上使用指针的要快上 15%,

$ go test -bench=. -benchmem . 
goos: darwin
goarch: arm64
pkg: main/demo
cpu: Apple M4 Pro
BenchmarkBoxedLargeSlice-12                 2935            406307 ns/op         4096014 B/op       1000 allocs/op
BenchmarkPointerLargeSlice-12               3434            342263 ns/op         4096010 B/op       1000 allocs/op
PASS
ok      main/demo       3.589s
1
2
3
4
5
6
7
8
9

# 函数调用性能


var sink Worker

func call(w Worker) {
	sink = w
}

func BenchmarkCallWithValue(b *testing.B) {
	for range b.N {
		var j LargeJob
		call(j)
	}
}

func BenchmarkCallWithPointer(b *testing.B) {
	for range b.N {
		j := &LargeJob{}
		call(j)
	}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

运行结果如下,可以看到两者内存申请差不多,但指针传递效率要更高。

% go test -bench=. -benchmem . 
goos: darwin
goarch: arm64
pkg: main/demo
cpu: Apple M4 Pro
BenchmarkCallWithValue-12        2959195               388.3 ns/op          4096 B/op          1 allocs/op
BenchmarkCallWithPointer-12      3513249               339.7 ns/op          4096 B/op          1 allocs/op
PASS
ok      main/demo       3.419s
1
2
3
4
5
6
7
8
9

# 什么时候允许Interface Boxing?

  • 接口支持解耦和模块化,所以合理的使用接口来设计 API,即使有 Interface Boxing 的成本也是值得花费的
type Storage interface {
    Save([]byte) error
}
func Process(s Storage) { /* ... */ }
1
2
3
4
  • 如果值很小,成本也是忽略不计的
var i interface{}
i = 123 // safe and cheap
1
2
  • 如果只是短暂的使用,开销也是小的

可以看到fmt.Println 中的实现是用接口作为接收参数

func Println(a ...any) (n int, err error) {
	return Fprintln(os.Stdout, a...)
}
1
2
3

这里即使有Interface Boxing,但只是短暂的一次,成本也低。

fmt.Println("value:", someStruct) // implicit boxing is fine
1

# 最佳实践

  • 传递给接口时使用指针。可以避免内存的重复复制与申请。
  • 如果设计 API 时,类型已经确定并且是稳定的,尽可能避免使用 interface 。
  • 尽可能使用特定类型的容器。
#go语言#go语言高性能编程
上次更新: 2025/06/14, 16:16:07
Go协程池深度解析:原理、实现与最佳实践
Go语言遍历性能深度解析:从原理到优化实践

← Go协程池深度解析:原理、实现与最佳实践 Go语言遍历性能深度解析:从原理到优化实践→

最近更新
01
Go语言高效IO缓冲技术详解
06-14
02
Go语言延迟初始化(Lazy Initialization)最佳实践
06-14
03
Go语言字符串拼接性能对比与优化指南
06-14
更多文章>
Theme by Vdoing | Copyright © 2022-2025 zhengwenfeng | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式