likes
comments
collection
share

「反叛Go」如何在 Go 中使用 macro!

作者站长头像
站长
· 阅读数 19

「这是我参与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)

你会注意到两件事:

  1. 很多分号。这是因为C宏不会扩展到新的一行(不过你可以使用\u000A来获得新的行,看你)。
  2. 我们使用的是函数宏,当然你可以使用其他选项,但在本文中坚持使用这种语法。

然后我们利用这个宏:

// #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 runmake 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
评论
请登录