惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Security Archives - TechRepublic
Security Archives - TechRepublic
N
News and Events Feed by Topic
Last Week in AI
Last Week in AI
博客园 - 司徒正美
The GitHub Blog
The GitHub Blog
O
OpenAI News
The Last Watchdog
The Last Watchdog
T
The Blog of Author Tim Ferriss
M
MIT News - Artificial intelligence
P
Proofpoint News Feed
Forbes - Security
Forbes - Security
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
有赞技术团队
有赞技术团队
Jina AI
Jina AI
GbyAI
GbyAI
V
Vulnerabilities – Threatpost
L
LangChain Blog
Vercel News
Vercel News
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
AI
AI
博客园 - 聂微东
W
WeLiveSecurity
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Scott Helme
Scott Helme
罗磊的独立博客
Martin Fowler
Martin Fowler
S
Security Affairs
T
Tor Project blog
Recent Announcements
Recent Announcements
F
Fortinet All Blogs
美团技术团队
C
Cisco Blogs
PCI Perspectives
PCI Perspectives
Recent Commits to openclaw:main
Recent Commits to openclaw:main
S
Security @ Cisco Blogs
T
Threat Research - Cisco Blogs
A
About on SuperTechFans
Cisco Talos Blog
Cisco Talos Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
I
Intezer
B
Blog
WordPress大学
WordPress大学
I
InfoQ
G
Google Developers Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
V
V2EX
P
Privacy & Cybersecurity Law Blog
雷峰网
雷峰网

极客兔兔

Go sync.Cond | Go 语言高性能编程 Go 死码消除与调试(debug)模式 | Go 语言高性能编程 Go sync.Once | Go 语言高性能编程 Go 逃逸分析 | Go 语言高性能编程 2020 年终总结 | 极客兔兔 Go struct 内存对齐 | Go 语言高性能编程 Go 空结构体 struct{} 的使用 | Go 语言高性能编程 控制协程(goroutine)的并发数量 | Go 语言高性能编程 | 极客兔兔 如何退出协程 goroutine (其他场景) | Go 语言高性能编程 如何退出协程 goroutine (超时场景) | Go 语言高性能编程 Go 语言陷阱 - 数组和切片 | Go 语言高性能编程 减小 Go 代码编译后的二进制体积 | Go 语言高性能编程 读写锁和互斥锁的性能比较 | Go 语言高性能编程 | 极客兔兔 for 和 range 的性能比较 | Go 语言高性能编程 切片(slice)性能及陷阱 | Go 语言高性能编程 | 极客兔兔 字符串拼接性能及原理 | Go 语言高性能编程 | 极客兔兔 pprof 性能分析 | Go 语言高性能编程 benchmark 基准测试 | Go 语言高性能编程 Go 语言高性能编程 | 极客兔兔 Go 接口型函数的使用场景 | 极客兔兔 Python 简明教程 | 快速入门 | 极客兔兔 Go 语言笔试面试题(代码输出) | 极客面试 | 极客兔兔 动手写RPC框架 - GeeRPC第七天 服务发现与注册中心(registry) | 极客兔兔 动手写RPC框架 - GeeRPC第六天 负载均衡(load balance) 动手写RPC框架 - GeeRPC第五天 支持HTTP协议 | 极客兔兔 动手写RPC框架 - GeeRPC第四天 超时处理(timeout) | 极客兔兔 动手写RPC框架 - GeeRPC第三天 服务注册(service register) 动手写RPC框架 - GeeRPC第二天 支持并发与异步的客户端 | 极客兔兔 动手写RPC框架 - GeeRPC第一天 服务端与消息编码 | 极客兔兔 7天用Go从零实现RPC框架GeeRPC | 极客兔兔 Go 语言笔试面试题(并发编程) | 极客面试 | 极客兔兔 Go 语言笔试面试题(基础语法) | 极客面试 | 极客兔兔 Go 语言笔试面试题汇总 | 极客面试 | 极客兔兔 Go Context 并发编程简明教程 | 快速入门 Go Mmap 文件内存映射简明教程 | 快速入门 动手写ORM框架 - GeeORM第七天 数据库迁移(Migrate) | 极客兔兔 动手写ORM框架 - GeeORM第六天 支持事务(Transaction) | 极客兔兔 动手写ORM框架 - GeeORM第五天 实现钩子(Hooks) | 极客兔兔 动手写ORM框架 - GeeORM第四天 链式操作与更新删除 | 极客兔兔 动手写ORM框架 - GeeORM第三天 记录新增和查询 | 极客兔兔 动手写ORM框架 - GeeORM第二天 对象表结构映射 | 极客兔兔 动手写ORM框架 - GeeORM第一天 database/sql 基础 SQLite 常用命令 | 速查表(Cheat Sheet) 7天用Go从零实现ORM框架GeeORM | 极客兔兔 动手写分布式缓存 - GeeCache第七天 使用 Protobuf 通信 动手写分布式缓存 - GeeCache第六天 防止缓存击穿 | 极客兔兔 动手写分布式缓存 - GeeCache第五天 分布式节点 | 极客兔兔 动手写分布式缓存 - GeeCache第四天 一致性哈希(hash) | 极客兔兔 Go Mock (gomock)简明教程 | 快速入门 动手写分布式缓存 - GeeCache第三天 HTTP 服务端 动手写分布式缓存 - GeeCache第二天 单机并发缓存 | 极客兔兔 Go Test 单元测试简明教程 | 快速入门 7天用Go从零实现分布式缓存GeeCache | 极客兔兔 Go WebAssembly (Wasm) 简明教程 | 快速入门 Go RPC & TLS 鉴权简明教程 | 快速入门 Go Protobuf 简明教程 | 快速入门 Go语言动手写Web框架 - Gee第七天 错误恢复(Panic Recover) WSL, Git, Mircosoft Terminal 等常用工具配置 Rust 简明教程 | 快速入门 | 极客兔兔 Go语言动手写Web框架 - Gee第六天 模板(HTML Template) 百宝箱 - 值得收藏的工具网站 | 极客兔兔 Go语言动手写Web框架 - Gee第五天 中间件Middleware | 极客兔兔 Go语言动手写Web框架 - Gee第四天 分组控制Group | 极客兔兔 Go语言动手写Web框架 - Gee第三天 前缀树路由Router | 极客兔兔 博客折腾记(七) - Gitalk Plus | 极客兔兔 Go语言动手写Web框架 - Gee第二天 上下文Context | 极客兔兔 Go2 新特性简明教程 | 快速入门 | 极客兔兔 博客折腾记(六) - 不要为了流量忘记了初心 | 极客兔兔 Go语言动手写Web框架 - Gee第一天 http.Handler | 极客兔兔 7天用Go从零实现Web框架Gee教程 | 极客兔兔 Go Gin 简明教程 | 快速入门 Go 语言简明教程 | 快速入门 | 极客兔兔 机器学习笔试面试题 11-20 | 极客面试 | 极客兔兔 机器学习笔试面试题 1-10 | 极客面试 | 极客兔兔 机器学习笔试面试题汇总 | 极客面试 | 极客兔兔 TensorFlow 2 中文文档 - RNN LSTM 文本分类 TensorFlow 2 中文文档 - TFHub 迁移学习 TensorFlow 2 中文文档 - 卷积神经网络分类 CIFAR-10 TensorFlow 2 中文文档 - 保存与加载模型 TensorFlow 2 中文文档 - 过拟合与欠拟合 TensorFlow 2 中文文档 - 回归预测燃油效率 TensorFlow 2 中文文档 - 特征工程结构化数据分类 TensorFlow 2 中文文档 - IMDB 文本分类 TensorFlow 2 中文文档 - MNIST 图像分类 TensorFlow 2 / 2.0 中文文档 TensorFlow 2.0 (九) - 强化学习 70行代码实战 Policy Gradient 博客折腾记(五) - 友链这件事,没那么简单 | 极客兔兔 博客折腾记(四) - 原创资格是争取来的 | 极客兔兔 TensorFlow 2.0 (八) - 强化学习 DQN 玩转 gym Mountain Car TensorFlow 2.0 (七) - 强化学习 Q-Learning 玩转 OpenAI gym 博客折腾记(三) - 主题设计、彩蛋与阅读量翻倍 | 极客兔兔 TensorFlow 2.0 (六) - 监督学习玩转 OpenAI gym game 博客折腾记(二) - 对搜索引擎的理解 | 极客兔兔 博客折腾记(一) - 极致性能的尝试 | 极客兔兔 Pandas 数据处理(三) - Cheat Sheet 中文版 TensorFlow 2.0 (五) - mnist手写数字识别(CNN卷积神经网络) TensorFlow入门(四) - mnist手写数字识别(制作h5py训练集) | 极客兔兔 TensorFlow入门(三) - mnist手写数字识别(可视化训练) | 极客兔兔 Pandas 数据处理(二) - 筛选数据 | 极客兔兔 Pandas 数据处理(一) - DataFrame 与 Series
Go Reflect 提高反射性能 | Go 语言高性能编程
2020-12-06 · via 极客兔兔

源代码/数据集已上传到 Github - high-performance-go

high performance go - data structure

1 反射的用途

标准库 reflect 为 Go 语言提供了运行时动态获取对象的类型和值以及动态创建对象的能力。反射可以帮助抽象和简化代码,提高开发效率。

Go 语言标准库以及很多开源软件中都使用了 Go 语言的反射能力,例如用于序列化和反序列化的 json、ORM 框架 gorm/xorm 等。

7days-golang 这个项目中,也有好几处用到了反射。在 七天用Go从零实现RPC框架 中,我们使用反射在服务端,利用接收到的二进制报文动态创建对象,例如利用反射实现函数的动态调用。在 7天用Go从零实现ORM框架GeeORM 中,我们使用反射,实现了结构体(struct)类型和数据库表名的映射,结构体字段和数据库字段的映射。同样利用反射动态创建对象的能力,将数据库中查询到的记录转换为 Go 语言中的对象。

2 反射如何简化代码

接下来呢,我们利用反射实现一个简单的功能,来看看反射如何帮助我们简化代码的。

假设有一个配置类 Config,每个字段是一个配置项。为了简化实现,假设字段均为 string 类型:

1
2
3
4
5
6
type Config struct {
Name string `json:"server-name"`
IP string `json:"server-ip"`
URL string `json:"server-url"`
Timeout string `json:"timeout"`
}

配置默认从 json 文件中读取,如果环境变量中设置了某个配置项,则以环境变量中的配置为准。配置项和环境变量对应的规则非常简单:将 json 字段的字母转为大写,将 - 转为下划线,并添加 CONFIG_ 前缀。

最终的对应结果如下:

1
2
3
4
5
6
type Config struct {
Name string `json:"server-name"`
IP string `json:"server-ip"`
URL string `json:"server-url"`
Timeout string `json:"timeout"`
}

实现这个功能非常简单,使用 switch case 或者 if else 硬编码很快就搞定了。但是,如果使用硬编码,Config 结构发生改变,例如修改 json 对应的字段,删除或新增了一个配置项,这块逻辑也需要发生改变。而更大的问题在于:容易出错,不好测试!!!

这个时候,就有了 reflect 的用武之地了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func readConfig() *Config {

config := Config{}
typ := reflect.TypeOf(config)
value := reflect.Indirect(reflect.ValueOf(&config))
for i := 0; i < typ.NumField(); i++ {
f := typ.Field(i)
if v, ok := f.Tag.Lookup("json"); ok {
key := fmt.Sprintf("CONFIG_%s", strings.ReplaceAll(strings.ToUpper(v), "-", "_"))
if env, exist := os.LookupEnv(key); exist {
value.FieldByName(f.Name).Set(reflect.ValueOf(env))
}
}
}
return &config
}

func main() {
os.Setenv("CONFIG_SERVER_NAME", "global_server")
os.Setenv("CONFIG_SERVER_IP", "10.0.0.1")
os.Setenv("CONFIG_SERVER_URL", "geektutu.com")
c := readConfig()
fmt.Printf("%+v", c)
}

实现逻辑其实是非常简单的:

  • 在运行时,利用反射获取到 Config 的每个字段的 Tag 属性,拼接出对应的环境变量的名称。
  • 查看该环境变量是否存在,如果存在,则将环境变量的值赋值给该字段。

运行该程序,输出为:

1
&{Name:global_server IP:10.0.0.1 URL:geektutu.com Timeout:}

可以看到,环境变量中设置的三个配置项已经生效。之后无论结构体 Config 内部的字段发生任何改变,这部分代码无需任何修改即可完美的适配,出错概率也极大地降低。

3 反射的性能

毫无疑问的是,反射会增加额外的代码指令,对性能肯定会产生影响的。具体影响有多大,我们可以使用 Benchmark 来测试一番。

3.1 创建对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func BenchmarkNew(b *testing.B) {
var config *Config
for i := 0; i < b.N; i++ {
config = new(Config)
}
_ = config
}

func BenchmarkReflectNew(b *testing.B) {
var config *Config
typ := reflect.TypeOf(Config{})
b.ResetTimer()
for i := 0; i < b.N; i++ {
config, _ = reflect.New(typ).Interface().(*Config)
}
_ = config
}

测试结果如下:

1
2
3
4
5
6
7
8
$ go test -bench .          
goos: darwin
goarch: amd64
pkg: example/hpg-reflect
BenchmarkNew-8 26478909 40.9 ns/op
BenchmarkReflectNew-8 18983700 62.1 ns/op
PASS
ok example/hpg-reflect 2.382s

通过反射创建对象的耗时约为 new 的 1.5 倍,相差不是特别大。

3.2 修改字段的值

通过反射获取结构体的字段有两种方式,一种是 FieldByName,另一种是 Field(按照下标)。前面的例子中,我们使用的是 FieldByName

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
33
34
func BenchmarkSet(b *testing.B) {
config := new(Config)
b.ResetTimer()
for i := 0; i < b.N; i++ {
config.Name = "name"
config.IP = "ip"
config.URL = "url"
config.Timeout = "timeout"
}
}

func BenchmarkReflect_FieldSet(b *testing.B) {
typ := reflect.TypeOf(Config{})
ins := reflect.New(typ).Elem()
b.ResetTimer()
for i := 0; i < b.N; i++ {
ins.Field(0).SetString("name")
ins.Field(1).SetString("ip")
ins.Field(2).SetString("url")
ins.Field(3).SetString("timeout")
}
}

func BenchmarkReflect_FieldByNameSet(b *testing.B) {
typ := reflect.TypeOf(Config{})
ins := reflect.New(typ).Elem()
b.ResetTimer()
for i := 0; i < b.N; i++ {
ins.FieldByName("Name").SetString("name")
ins.FieldByName("IP").SetString("ip")
ins.FieldByName("URL").SetString("url")
ins.FieldByName("Timeout").SetString("timeout")
}
}

测试结果如下:

1
2
3
4
5
6
7
8
9
$ go test -bench="Set$" .          
goos: darwin
goarch: amd64
pkg: example/hpg-reflect
BenchmarkSet-8 1000000000 0.302 ns/op
BenchmarkReflect_FieldSet-8 33913672 34.5 ns/op
BenchmarkReflect_FieldByNameSet-8 3775234 316 ns/op
PASS
ok example/hpg-reflect 3.066s
  • 三种场景下,对象已经提前创建好,测试的均为给字段赋值所消耗的时间。
  • 普通的赋值操作,每次耗时约为 0.3 ns,通过下标找到对应的字段再赋值,每次耗时约为 30 ns,通过名称找到对应字段再赋值,每次耗时约为 300 ns。

总结一下,对于一个普通的拥有 4 个字段的结构体 Config 来说,使用反射给每个字段赋值,相比直接赋值,性能劣化约 100 - 1000 倍。其中,FieldByName 的性能相比 Field 劣化 10 倍。

3.3 FieldByName 和 Field 性能差距

FieldByNameField 十倍的性能差距让我对 FieldByName 的内部实现比较好奇,打开源代码一探究竟:

  • reflect/value.go
1
2
3
4
5
6
7
8
9
10



func (v Value) FieldByName(name string) Value {
v.mustBe(Struct)
if f, ok := v.typ.FieldByName(name); ok {
return v.FieldByIndex(f.Index)
}
return Value{}
}
  • reflect/type.go
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
func (t *rtype) FieldByName(name string) (StructField, bool) {
if t.Kind() != Struct {
panic("reflect: FieldByName of non-struct type")
}
tt := (*structType)(unsafe.Pointer(t))
return tt.FieldByName(name)
}



func (t *structType) FieldByName(name string) (f StructField, present bool) {

hasEmbeds := false
if name != "" {
for i := range t.fields {
tf := &t.fields[i]
if tf.name.name() == name {
return t.Field(i), true
}
if tf.embedded() {
hasEmbeds = true
}
}
}
if !hasEmbeds {
return
}
return t.FieldByNameFunc(func(s string) bool { return s == name })
}

整个调用链条是比较简单的:

1
(v Value) FieldByName -> (t *rtype) FieldByName -> (t *structType) FieldByName

(t *structType) FieldByName 中使用 for 循环,逐个字段查找,字段名匹配时返回。也就是说,在反射的内部,字段是按顺序存储的,因此按照下标访问查询效率为 O(1),而按照 Name 访问,则需要遍历所有字段,查询效率为 O(N)。结构体所包含的字段(包括方法)越多,那么两者之间的效率差距则越大。

4 如何提高性能

4.1 避免使用反射

使用反射赋值,效率非常低下,如果有替代方案,尽可能避免使用反射,特别是会被反复调用的热点代码。例如 RPC 协议中,需要对结构体进行序列化和反序列化,这个时候避免使用 Go 语言自带的 jsonMarshalUnmarshal 方法,因为标准库中的 json 序列化和反序列化是利用反射实现的。可选的替代方案有 easyjson,在大部分场景下,相比标准库,有 5 倍左右的性能提升。

4.2 缓存

在上面的例子中可以看到,FieldByName 相比于 Field 有一个数量级的性能劣化。那在实际的应用中,就要避免直接调用 FieldByName。我们可以利用字典将 NameIndex 的映射缓存起来。避免每次反复查找,耗费大量的时间。

我们利用缓存,优化下刚才的测试用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func BenchmarkReflect_FieldByNameCacheSet(b *testing.B) {
typ := reflect.TypeOf(Config{})
cache := make(map[string]int)
for i := 0; i < typ.NumField(); i++ {
cache[typ.Field(i).Name] = i
}
ins := reflect.New(typ).Elem()
b.ResetTimer()
for i := 0; i < b.N; i++ {
ins.Field(cache["Name"]).SetString("name")
ins.Field(cache["IP"]).SetString("ip")
ins.Field(cache["URL"]).SetString("url")
ins.Field(cache["Timeout"]).SetString("timeout")
}
}

测试结果如下:

1
2
3
4
5
6
7
8
9
10
$ go test -bench="Set$" . -v
goos: darwin
goarch: amd64
pkg: example/hpg-reflect
BenchmarkSet-8 1000000000 0.303 ns/op
BenchmarkReflect_FieldSet-8 33429990 34.1 ns/op
BenchmarkReflect_FieldByNameSet-8 3612130 331 ns/op
BenchmarkReflect_FieldByNameCacheSet-8 14575906 78.2 ns/op
PASS
ok example/hpg-reflect 4.280s

消耗时间从原来的 10 倍,缩小到了 2 倍。

附 推荐与参考


edit this page last updated at 2026-02-23