「反叛Go」如何在 Go 中使用 macro!
「这是我参与2022首次更文挑战的第 1 天,活动详情查看:2022首次更文挑战」。
今天让我们在构建过程中使用一些真正的宏!这会把我们带回到90年代的命令行工具,以及那些精通C语言预处理器的人可能会记得的一些事情。
首先,让我们创造一个问题背景:
我们将重温函数式编程的最爱:MAP, FILTERFOLDL
。我们将选择两个简单的操作: map/filter
。我们将使用C语言预处理器,所以我们的宏看起来会像C的宏,当然,欢迎你使用 M4或其他更通用的预处理器,也许你可以自己写。
开始
本篇文章的准备很直接:启动一个go项目,然后写一个 makefile
。下面将从这个目录结构开始。
.
├── app.cgo
├── go.mod
├── macros
│ └── functions.h
└─── makefile
看看 app.cgo
的内容是啥:
package main
import (
"fmt"
"math/rand"
)
type Demo struct {
A int
}
func main() {
as := []Demo{}
for i := 0; i < 1000; i++ {
as = append(as, demo.Demo{A: rand.Intn(100)})
}
}
需要说明的是:通常我们会使用传统的 for i := range array
,上述我们没有这样做,是因为可以更好地测试我们的宏:
bs := []Demo{}
for i := range as {
if as[i].A < 50 {
bs = append(bs, as[i])
}
}
fmt.Printf("bs=%d demos\n", len(bs))
在我运行的机器上,最终向bs中插入了506个元素。你的结果可能不同,但是在多次运行中应该是一致的结果(rand并不是真正的随机)。
不过在开始实验前,还有一件事要做:把我们的makefile补充一下
all:
cat app.cgo | perl -np -e 's{^\t*//(\#.+)$$}{$$1}' | gcc -P -traditional -E - 2>/dev/null | cat -s | goimports 1>app.go
注意:如果你是makefile的新手,all指令下的命令前面的空格必须是一个制表符(\t),而不是一串连续的空格。
makefile中描述的是:C预处理器运行我们的 app.cgo
文件,并将处理结果输入 app.go
中。
perl -np ...
→ 允许我们对cgo文件运行gofmt或goimports命令,而不会报出#include ""
是无效的go语法(但是在Go中确实是)。这将删除go注释(//),并将宏放在行首,以避免任何预处理器问题。gcc -P ...
→ 运行C语言预处理器cat -s
→ 去掉长长的多行代码goimports 1>app.go
→ 在写到app.go
之前运行了goimports
那结果是什么?先看看:
package main
import (
"fmt"
"math/rand"
)
type Demo struct {
A int
}
func main() {
as := []Demo{}
for i := 0; i < 1000; i++ {
as = append(as, Demo{A: rand.Intn(100)})
}
bs := []Demo{}
for i := range as {
if as[i].A < 50 {
bs = append(bs, as[i])
}
}
fmt.Printf("bs=%d demos\n", len(bs))
}
和我们上面预期的差不多。那就这样,下面开始这个看似魔法的操作。
MACROS!
上面也许你注意到了,我们有一个叫做 macros
文件夹,里面有一个 function.h
文件,把这个文件加到你的编辑器里。
Filter
然后让我们来写一个 FILTER
宏,我们将遵循C语言的惯例,使宏完全大写。
#define FILTER(arr, condition, type) \
func(arr []type) []type { \
zs := []type{}; \
for i := range arr {; \
if condition { \
zs = append(zs, arr[i]); \
} \
}; \
return zs; \
}(arr)
你会注意到两件事:
- 很多分号。这是因为C宏不会扩展到新的一行(不过你可以使用\u000A来获得新的行,看你)。
- 我们使用的是函数宏,当然你可以使用其他选项,但在本文中坚持使用这种语法。
然后我们利用这个宏:
// #include "macros/functions.h"
func main() {
as := []Demo{}
for i := 0; i < 1000; i++ {
as = append(as, Demo{A: rand.Intn(100)})
}
bs := []Demo{}
for i := range as {
if as[i].A < 50 {
bs = append(bs, as[i])
}
}
fmt.Printf("bs=%d demos\n", len(bs))
cs := FILTER(as, as[i].A < 50, Demo)
fmt.Printf("cs=%d demos\n", len(cs))
}
然后你运行 make
,就可以得到新的 app.go
:
func main() {
as := []Demo{}
for i := 0; i < 1000; i++ {
as = append(as, Demo{A: rand.Intn(100)})
}
bs := []Demo{}
for i := range as {
if as[i].A < 50 {
bs = append(bs, as[i])
}
}
fmt.Printf("bs=%d demos\n", len(bs))
cs := func(as []Demo) []Demo {
zs := []Demo{}
for i := range as {
if as[i].A < 50 {
zs = append(zs, as[i])
}
}
return zs
}(as)
fmt.Printf("cs=%d demos\n", len(cs))
}
当然,这也可以不使用闭包,但这可以保护我们不与作用域中的其他变量混淆。当你运行这个应用程序时,可以得到这个结果(机器不同可能结果不同):
bs=506 demos
cs=506 demos
但是我们不确定这个结果是不是真正正确的。需要做一个校验:
if len(cs) != len(bs) {
panic("cs and bs differ")
}
for i := range bs {
if bs[i] != cs[i] {
fmt.Printf("index %d differs, b=%d,c=%d\n", i, bs[i], cs[i])
errors++
}
}
Map
然后我们看看:map,照葫芦画瓢。
#define MAP(arr, condition, member, type, returnType) \
func(arr []type) []returnType { \
zs := []returnType{}; \
for i := range arr {; \
if condition { \
zs = append(zs, member); \
} \
}; \
return zs; \
}(arr)
这个宏中你会看到更多的参数。这是为了让我们生成的代码能够正确地描述类型。然后将使用 MAP
宏的代码插入 app.cgo
中:
js := MAP(as, as[i].A < 50, as[i].A, Demo, int)
fmt.Println("js=%d ints\n", len(js))
运行 make
会生成额外的代码:
js := func(as []Demo) []int {
zs := []int{}
for i := range as {
if as[i].A < 50 {
zs = append(zs, as[i].A)
}
}
return zs
}(as)
fmt.Printf("js=%d ints\n", len(js))
makefile选项
目前我们的 makefile 还很基本的,网上有很多教你如何在 makefile 中迭代repo中的所有*.cgo文件的文章,所以你不必列出每个Cgo文件在你的makefile中,也不必头疼保持更新。
在写这篇文章的时候,有这两条规则很有帮助,make run
和 make debug
分别构建和运行应用程序,并查看预处理器输出:
debug:
cat app.cgo | perl -np -e 's{^\t*//(\#.+)$$}{$$1}' | gcc -P -CC -traditional -E - 2>/dev/null | cat -s | tee /dev/tty | goimports
run: all
if [[ -f app.go ]]; then go run app.go; rm app.go; else echo "app.go failed to generate"; fi
这比使用这些类型的代码生成要干净得多,而且如果发现了bug或需要一些稍微不同的东西,它也更容易修复,少了很多模板文件,只需要关注功能是如何完成的。
好了,这就是 Go 中使用 macro!
的全部内容。你不妨试试,把目前集合数据结构的操作改造成上述宏操作。
转载自:https://juejin.cn/post/7056336548886740999