浅谈元编程

本文更新于 {{ days }} 天前,内容可能已经过时。

本文将浅入浅出介绍一下元编程的概念,以及探索一下各语言的元编程技术。

什么是元编程?#

元编程(英语:Metaprogramming),是指某类计算机程序的编写,这类计算机程序编写或者操纵其它程序(或者自身)作为它们的资料,或者在运行时完成部分本应在编译时完成的工作。

多数情况下,与手工编写全部代码相比,程序员可以获得更高的工作效率,或者给与程序更大的灵活度去处理新的情形而无需重新编译。

—— 维基百科

我最初了解到 “元编程” 这个名词和概念是来自《Ruby 元编程 (2nd)》一书,该书通过剖析 Ruby 类库的实现细节来展示 Ruby 的元编程。

元编程,Metaprogramming,该概念常见于动态语言,比如 Ruby、Python 等。从英文的角度,meta 的概念为在 xx 之后、超出 xx、超越 xx,元编程可以被理解为超出编程、在编程之上的东西。

广义来说,元编程的概念为与编程有关的编程,比如根据 Protobuf 定义生成 Golang、Python 代码。

1
2
3
4
5
6
7
8
// 一个用 Golang 编写的生成器, 生成 Python 代码
func main() {
    g := &Generator{OutputFile: "index.py"} // 随便写的结构体
    g.Println("# -*- coding: utf-8 -*-\n")
    for i = 0; i < 10; i++ {
        g.Printf("print(%d)\n", i)
    }
}

狭义来说,元编程的概念为编写能改变语言语法特性或者运行时特性的程序,比如 C 的宏、C++ 的模板。

1
2
3
4
5
6
#define add(a, b) ((a) + (b))
#define sub(a, b) ((a) - (b))
#define mul(a, b) ((a) * (b))
#define div(a, b) ((a) / (b))

int a = mul(123, add(1, 2))

元编程可以理解是一种将代码当成数据处理的能力,代码可以像数据一样被新增、替换、删除。大部分的编程语言均支持不同形式的元编程能力,为编程提供更丰富的抽象能力和表达能力。

元编程:代码编写代码#

In metaprogramming, you write code that writes code.

——《Metaprogramming Ruby 2》

元编程本质上是一种使用代码编写代码的方式。元编程也是编程,是在编程的基础上更抽象的层面。

  1. 为什么需要元编程去使用代码编写代码?
  2. 元编程在何时使用代码编写代码?
  3. 元编程怎么使用代码编写代码?

使用代码去编写代码,根本原因是解决程序日益增长的重复、繁琐、冗余的代码编写和程序员有限劳动力之间的矛盾。元编程为代码生成自动化提供了更强的表达能力,避免了我们一遍又一遍写着重复的代码。

既然元编程也是编程,那么我们在何时进行元 “编程” 也是比较重要的问题。现代的编程语言基本都会为我们提供不同的元编程能力,可以根据编写代码的时机进行分类。代码自身的生命周期可以简单被分为编码、编译链接、运行,这些步骤都有元编程的身影。

编码
编码
编译链接
编译链接
运行
运行
Generator
生成器
Generator...
Generic
泛型
Generic...
Template
模板
Template...
Macro
Macro...
Decorator
装饰器
Decorator...
Runtime
运行时
Runtime...
Text is not SVG - cannot display

元编程大致可以分为编译时元编程和运行时元编程,这取决于编写代码的操作是在编译时或者编译前进行,还是在运行时进行。前者依靠编译器提供的能力进行代码生成,而后者要依赖于语言运行时或者语言的特殊特性进行代码生成。除此之外还有编码时的代码生成器。

接下来我们就了解一下不同编程语言中,元编程是怎样使用代码去编写代码的。

元编程在编写什么代码?#

在了解各不同编程语言的元编程之前,我们先了解一些比较常见的元编程场景。

示例一:模板代码#

在大部分的代码编写代码、代码生成代码的场景中,主要解决的是重复编程问题,也就是重复的代码可以被抽象成复刻或者模板。

假设我们有一个场景需要定义 10 个方法,你可能选择以下的其中一个方案。

  1. 按部就班编写 10 个方法
  2. 编写一个方法去生成这 10 个方法
  3. 实现一个方法来接管这 10 个方法的工作

10 个简单方法使用方案1或许可以接受,但是如果方法实现极其复杂,或者需要定义 100 个或者更多的方法,那实现起来即无聊又费人力。在这种代码相似且有规律的场景下,我们可以编写一个方法,根据这 10 个、100 个方法的规律得出的模板来生成这 10 个、100 个方法。

比如一个结构体的 String,成员变量的 Getter、Setter 等。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
type Person struct {
    name string
    age  int16
}

func (p *Persion) Name() string { return p.name }
func (p *Persion) Age() int16   { return p.age }
func (p *Persion) SetName(name string) { p.name = name }
func (p *Persion) SetAge(age int16)    { p.age = age }

func (p *Person) String() string {
    return "Person(name:"+p.name+",age:"+strconv.FormatInt(int64(p.age), 10)+")"
}

比如相似的函数或结构定义等。

1
2
3
4
func MaxInt16(a, b int16) int16 { ... }
func MaxInt32(a, b int32) int32 { ... }
func MaxInt64(a, b int64) int64 { ... }
func MaxInt(a, b int) int { ... }

比如定义了一个服务的 Protobuf 协议,去生成服务的代码。(没有示例代码)

在以上几种场景中,都具有一些比较重复的代码,如果能自动化去编程这些内容,能节省我们很多人力。

示例二:动态代码#

示例一中展示了一些重复性代码的例子,通常需要编写的方法和代码都是固定的。除了这种固定的场景,我们可能还会遇到一些动态代码的场景,即代码是根据实时的配置、数据来构造的。在这种场景,基本都是依赖于不同语言所提供的语言特性和能力进行实时生成代码。

假设我们有一个场景需要定义x个方法,那我们有如下的实现方案。

  1. 编写一个方法去生成这x个方法
  2. 实现一个方法来接管这x个方法的工作

相比示例一,我们就不能按部就班的实现x个方法了,因为x是未知的,取决于运行时。

比如,根据本地配置的 Scheme 去生成 ActiveRecord。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
=begin 代码瞎写的,举个例子,不一定是这种场景
"person": {
  "name": string,
  "age": number,
}
=end

class Person < Entity # 假设 Entity 是 ActiveRecord 库的基础类
    attr_reader :name
    attr_reader :age

    def initialize(name, age)
        @name = name
        @age = age
    end
end

p = Person.new("XiaoMing", 12)

在我们有 ActiveRecord 的类后,我们甚至可以根据类去生成一些额外的处理方法和逻辑。 比如,根据 ActiveRecord 去创建数据库表。

1
2
3
4
5
6
7
class Person < Entity
    attr_reader :name
    attr_reader :age
end

# Gen
db.query('CREATE TABLE person ...')

甚至,可以通过方案二实现一个方法来接管这x个方法的工作。比如 Ruby ORM 的幽灵方法。

1
2
3
4
Person.find_by_name('XiaoHong')
Person.find_one_by_name('XiaoHong')
Person.find_by_age(12)
Person.find_by_name_and_age('XiaoMing', 12)

示例三:猴子补丁#

猴子补丁 (Monkey Patch) 是一种在不修改源代码的情况下对现有功能进行追加和变更的解决方案,常用于功能追加变更和BUG修复。

示例一、示例二举了一些元编程生成代码的例子,而猴子补丁是元编程处理、变更代码的例子。

比如,在开发的应用依赖了 log 函数,而应用的依赖库也依赖于 log 函数。当我想修改 log 函数的定义并且在应用全局生效,就需要通过补丁的方式修改 log 函数的定义,而不是封装。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# log
def log
    puts 'log version'
end

# lib
def run_lib
    log
end

# my_app
def log
    puts 'my app version'
end
run_lib # => my app version

除了功能追加变更外,补丁的方案也用于 BUG 修复,并作用于应用全局。当然,还有 gomonkey 被用于测试 mock 函数和方法的场景。

示例四:DSL#

部分动态语言的代码处理能力可以用于重新构建语法,为 DSL 特定领域语言的实现提供了丰富的表达能力。

比如 Rake,一个用 Ruby 开发的代码构建工具,类似于 Make。

1
2
3
4
5
6
7
8
9
desc "rake1"
task :rake1 do
    puts "rake1"
end

desc "rake2"
task :rake2 => [:rake1] do
    puts "rake2"
end

比如 Ruby 的单元测试。

1
2
3
4
5
6
7
describe 'add' do
    context 'when 1+1' do
        it 'return 2' do
            expect(add(1, 1)).to be 2
        end
    end
end

比如 Gradle,一个 JVM 系的项目自动化构建工具。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
repositories {
    mavenCentral()
}

dependencies {
    compile 'info.picocli:picocli:3.8.0'
    testCompile group: 'junit', name: 'junit', version: '4.12'
}

jar {
    from {
        configurations.runtime.collect{zipTree(it)}
    }
    manifest {
        attributes 'Main-Class': mainClassName
    }
}

运行时元编程#

由于代码生成器概念太过于广泛,包括编译器、解释器、各种脚手架都算在里面,我们不再讨论。下面主要从运行时和编译时两个角度讨论。

运行时元编程依靠语言层面的特性,在运行期间改变对象行为和属性。

内省与反射#

内省和反射常见于具备 Runtime 的编程语言,尤其是动态语言。

  • 内省 (Introspection):自我检查,获取自己内部信息,包括名字、类继承关系、成员等。
  • 反射 (Reflection):运行时获取类的信息、对象的数据,并可以改变对象的行为。

Java 的反射能力尤为著名,基本上被使用的最多的 Java 框架都有使用到反射去实现一些能力,比如服务端常用的 Spring 全家桶、Hibernate、Mybatis 等。

下面介绍下 Java 的一些反射能力,运行时获取类的信息,是反射的基本能力。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Person p = new Person();

Class klass = Person.class; // 直接获取类
Class klass = p.getClass(); // 根据对象获取类
Class klass = Class.forName("com.icytown.example.Person"); // 根据类名获取类

klass.getConstructor(String.class, Integer.class); // 获取构造函数
klass.getMethod("getName"); // 获取方法
klass.getField("name"); // 获取公有属性
klass.getDeclaredField("name"); // 获取所有属性

klass.getSuperClass(); // 获取父类
klass.getInterfaces(); // 获取实现的接口
klass.isAssignableFrom(klass2); // 判断是否可被赋值

获取类、方法、成员等信息后,可以根据这些信息改变对象的行为。

1
2
klass.invoke(method, "XiaoMing"); // 调用方法
nameField.set(p, "XiaoMing") // 修改成员信息

反射应用的场景也比较多,比如:

  1. JSON 字符串转换成一个特定类的对象
  2. ORM 创建数据库表
  3. ORM 动态生成接口实例
  4. 动态依赖注入
 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
// JSON => JavaBean
Person p = new Gson().fromJson("{\"name\":\"XiaoMing\",\"age\":12}", Person.class)
// JavaBean => JSON
String json = new Gson().toJson(p)

// ORM创建数据库表
@Entity
public class Person {
    @Id
    @GeneratedValue
    private Long id;

    private String name;
    private Integer age;
}

// ORM动态生成接口实例
@Repository
public interface PersonRepository extends CrudRepository<Person, Long> {
    List<Person> findByName(String name);
    List<Person> findByAge(Integer age);
    List<Person> findByNameAndAge(String name, Integer age);

    List<Person> findByAge(Integer age, Pageable pageable);
    List<Person> findByAge(Integer age, Sort sort);
}

反射在依赖注入自动化中使用的也比较多,比如 Spring IoC 容器、Android 常用的 Dagger、Butterknife 等。主要是通过获取需要注入的对象对应的类,并找到匹配注入规则的实例进行注入。Golang 也可以通过运行时识别参数的类型进行参数依赖注入。

1
2
3
4
5
6
7
8
9
// 动态依赖注入
class AppActivity : AppCompatActivity {
    @Inject
    private lateinit viewModel: AppViewModel
}

val component: AppComponent = DaggerAppComponent.builder().app(app).build()
component.provide(AppViewModel())
component.inject(activtiy)

Java 的这套反射机制在 Ruby 中通常被叫做内省,取决于 Ruby 动态语言的特性,它相比 Java 提供了更加自由的能力,但是写出来的代码也更加难以维护。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
'MegaShow'.class # => String
'MegaShow'.methods            # 获取String对象的方法
'MegaShow'.singleton_methods  # 获取String对象的单例方法
'MegaShow'.private_methods    # 获取String对象的私有方法

String.methods                   # 获取String类的方法
String.instance_methods          # 获取String类的实例方法
String.singleton_methods         # 获取String类的单例方法
String.private_methods           # 获取String类的私有方法
String.private_instance_methods  # 获取String类的私有实例方法

String.superclass # => Object
String.ancestors # => [String, Comparable, Object, Kernel, BasicObject]

'MegaShow'.to_s               # 调用方法
'MegaShow'.send(:to_s)        # 可通过该方法调用私有方法
'MegaShow'.respond_to?(:to_s) # 是否可以响应to_s方法

与 Java 不一样,在 Ruby 中我们可以更改方法的实现逻辑。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# 覆盖实现
class Integer
    def to_s
        'good integer'
    end
end

puts 1.to_s # => good integer

# 追加实现
class Integer
    alias to_s_old to_s

    def to_s
        'good integer ' + to_s_old
    end
end

puts 1.to_s # => good integer 1

Ruby 的对象模型也特别特殊,一切皆为对象和方法。

String
String
Class
Class
'MegaShow'
'MegaShow'
class
class
class
class
Module
Module
Comparable
Comparable
Object
Object
Object
Object
Kernel
Kernel
Kernel
Kernel
BasicObject
BasicObject
BasicObject
BasicObject
Text is not SVG - cannot display

在 Ruby 中甚至可以修改对象实例化的方法和定义类和方法的方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
str = String.new('MegaShow')
puts String.respond_to? :new # => true

# 定义类
Person = Class.new do
    define_method :to_s do
        'Person'
    end
end
# 等价于
class Person
    def to_s; 'Person'; end
end

除此之外,Ruby 还提供了打开类、动态派发等提高元编程能力的工具。

注解与装饰器#

注解 (Annotation) 又叫标注,是一种注释机制。在 Java 中,可以对包、类、方法、成员进行标注,在编译时和运行时进行一些操作。

1
2
3
4
5
6
7
8
9
@Entity
public class Person {
    @Id
    @GeneratedValue
    private Long id;

    private String name;
    private Integer age;
}

Java 的注解具有三类生命周期:

  • SOURCE:源码级别的注解,提供给编译器使用,会被编译器丢弃。
  • CLASS:class 字节码级别的注解,提供给 JVM 使用,会被 JVM 丢弃。
  • RUNTIME:运行时级别的注解,始终保留。

通过反射机制可以在运行时获得 RUNTIME 级别的注解信息,然后编写相应的注解处理器,即可实现额外的逻辑。比如以上的源码中,对 Entity 处理注解,然后进行数据库表创建和别的数据库相关的操作。

1
2
3
4
Class.getAnnotation(Class)
Field.getAnnotation(Class)
Method.getAnnotation(Class)
Constructor.getAnnotation(Class)

注解仅仅是为类与方法附加了元数据,而装饰器 (Decorator) 还为类、方法等提供了行为劫持。支持装饰器的语言有 Python、TypeScript、ECMAScript 等,在 TS/ES 中装饰器均处于 unstable 状态。

在 TypeScript 中,装饰器是以函数的形式存在,通过对类、方法进行装饰,将需要的代码逻辑追加到类和方法运行之前。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class Person {
    @log()
    goToSchool() {
        console.log('go to school')
    }
}

function log() {
    return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
        console.log(`call ${propertyKey}`);
    }
}

(new Person()).goToSchool() // => call goToSchool \n go to school

编译时元编程#

编译时元编程主要通过编译器层面上的支持,实现代码编写代码。

宏展开#

宏 (Macro) 是很多编程语言都具有的特性,它是一种规则或模式,将输入的字符串映射成其他字符串。

宏在编码的过程就预先定义好,并且在编译器编译期间进行宏展开。利用好宏展开的模式特性,可以在程序中减少大量的重复代码,比如 C、C++、Rust 里面的宏。

1
2
3
4
5
6
#define MAX(a,b) (((a) > (b)) ? (a) : (b))
#define N(s) NORMANDY_##s

#ifndef __MATH_H__
    #define __MATH_H__
#endif

相比 C 的宏,Rust 的宏提供了强大的模式匹配能力。

1
2
3
4
5
6
7
8
macro_rules! foo {
    (x => $e:expr) => (println!("mode X: {}", $e));
    (y => $e:expr) => (println!("mode Y: {}", $e));
}

fn main() {
    foo!(y => 3); // => mode Y: 3
}

泛型与模板#

泛型 (Generic) 编程是大多数编程语言提供的参数化类型的能力,可以减少大部分不同类型但代码相似的重复代码。通常泛型提供编译时的类型安全检查机制,可以在编译期间检测出非法的类型使用。

在 Java 泛型中,泛型类型是编译擦写的,仅提供给编译器使用,在 JVM 运行期间不存在泛型的信息。

1
2
3
4
5
6
class Entity<T extends Person> { // 有界泛型
    private T obj;

    public void set(T t) { obj = t; }
    public T get() { return obj; }
}

值得关注的,C# 的泛型并不是编译擦写的,其泛型原始类型信息一直保存在运行时,且运行时会创建专门的泛型类型。

C++ 使用模板来实现编译时泛型,模板相当于泛型类或函数的蓝图。

1
2
3
4
5
6
7
8
template <typename T>
class Person {
private:
    T obj;
public:
    void set(T t) { this.obj = t; }
    T get() { return this.obj; }
}

相比 Java,C++ 图灵完备的模板还能完成很多特殊的工作,比如编译时循环展开、控制结构、类型推导。

比如我们需要多次重复打印相同的内容,并且需要打印的次数是固定的,通常我们是使用循环来迭代,但是 C++ 模板其实也可以完成这个工作。通过 C++ 的模板可以把循环展开,但是会导致二进制文件大小成比增大。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 循环迭代
int n = 100;
for (int i = 0; i < 100; i++) {
    std::cout << "xxx is no.1" << std::endl;
}

// 模板迭代
template <unsigned N>
void Print() {
    Print<0>();
    Print<N - 1>();
}

template <>
void Print<0>() {
    std::cout << "xxx is no.1" << std::endl;
}

Print<100>();

除了定长迭代外,C++ 模板也支持变长迭代。

1
2
3
4
5
6
7
// C++17
template <typename... Ts>
constexpr auto Sum (Ts... args) {
    return (0 + ... + args);
}

Sum(1, 2, 3, 4) // => 10

因为上述 C++ 的两种特性,甚至可以通过 C++ 编译器来运行代码,在编译时即可得到答案,无需运行程序。比如计算斐波那契数列,计算质数等。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// C++17
template <unsigned N>
constexpr unsigned long long _Fibonacci() {
    return _Fibonacci<N-1>() + _Fibonacci<N-2>();
}

template <>
constexpr unsigned long long _Fibonacci<1>() {
    return 1;
}

template <>
constexpr unsigned long long _Fibonacci<0>() {
    return 0;
}

template <unsigned N>
constexpr unsigned long long Fibonacci = _Fibonacci<N>();

Fibonacci<75>; // => 2111485077978050

C++ 的 constexpr if 可以用来控制编译分支,比 C 的 ifdef 和 ifndef 功能更加强,即在编译期间就得知需要运行哪个分支,并将其他分支的代码丢弃。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// C++17
template <typename T>
constexpr bool isNum = std::is_arithmetic<T>::value;

template <typename T>
constexpr bool isStr = std::is_same<T, const char *>::value;

template <typename T>
std::string ToString(T val) {
    if constexpr (isNum<T>) return std::to_string (val);
    else if constexpr (isStr<T>) return std::string (val);
}

ToString(12)
ToString("XiaoMing")

甚至可以使用 C++ 模板进行编译时类型推导和判断。

1
2
3
4
5
6
7
8
9
template<typename T1, typename T2>
class SameType { public: enum { ret = false }; };

template<typename T>
class SameType<T, T> { public: enum { ret = true }; };

auto t1 = xxxx;
auto t2 = xxxx;
std::cout << SameType<decltype(t1), decltype(t2)>::ret << std::endl; // 类型一样返回true,不一样返回false

提到类型推导,不得不说其 TypeScript 的类型系统。由于 ECMAScript 动态语言的特性,为了在编码期间和编译期间提前发现更多潜在的问题,TypeScript 设计了一套较为复杂的泛型和类型推导特性。

在 TypeScript 中,类型本身是编译擦写的,因为 TypeScript 源码最终会生成成为 ECMAScript 源码。

1
2
3
4
5
6
interface Person {
    name: string
    age: number
    gender: 'male' | 'female'
    phone?: number
}

为了限制各个地方使用、传参 Person 的能力,我们可以定义以下的泛型。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>

type RequireAtLeastOne<T, Keys extends keyof T = keyof T> = Pick<T, Exclude<keyof T, Keys>> & { [K in Keys]-?: Required<Pick<T, K>> }[Keys]

type RequireOnlyOne<T, Keys extends keyof T = keyof T> = Pick<T, Exclude<keyof T, Keys>> &
  { [K in Keys]-?: Required<Pick<T, K>> & Partial<Record<Exclude<Keys, K>, undefined>> }[Keys]

// 使用
const a: Omit<Person, 'phone'> = {'phone': '12345678901'} // 编译错误
const b: RequireAtLeastOne<Person, 'name'|'age'> = {'gender': 'male'} // 编译错误
const c: RequireAtLeastOne<Person, 'balance'> = {'balance': '123'} // 编译错误
const d: RequireOnlyOne<Person, 'name'|'age'> = {'name': 'XiaoMing', 'age': 12, 'gender': 'male'} // 编译错误

除了以上的例子,泛型和模板还有别的元编程技术体现,这里就不一一展开了。

Golang元编程#

作为具备 Runtime 的静态编程语言,Golang 也拥有反射的语言特性,其标准库 reflect 提供了一系列反射和内省的函数和方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
a := 1

v := reflect.ValueOf(a) // 获取反射对象

fmt.Println("v Type:", v.Type()) // int
fmt.Println("v CanSet:", v.CanSet()) // false

v = reflect.ValueOf(&a) // 通过指针构建反射对象
fmt.Println("v Type:", v.Type()) // *int
fmt.Println("v CanSet:", v.CanSet()) // false

v = v.Elem() // element value
fmt.Println("v Type:", v.Type()) // int
fmt.Println("v CanSet:", v.CanSet()) // true

由于性能问题,Golang 的反射通常情况不被推荐使用。在 Golang 圈子里面,似乎特别热衷于代码生成,比如 gomock,gowire 等,都是通过代码生成器的方式实现 mock 和依赖注入。

Golang 语言提供在编译期、编码期进行代码生成的机制,通过 go generate 命令执行源代码去生成源代码。比如官方的 x/tools 库,提供了 stringer 用于生成带 String 绑定方法的代码。

1
2
3
4
5
6
7
//go:generate stringer -type=UserType
type UserType int32

const (
    UserTypeAdmin  UserType = 99
    UserTypeNormal UserType = 1
)

stringer 生成的代码中硬编码写死了各个常量的名字,避免运行时通过反射的方式去获取。(不过实际上 Golang 也获取不到这个名字)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func _() {
    var x [1]struct{}
    _ = x[UserTypeAdmin-99]
    _ = x[UserTypeNormal-1]
}

const (
    _UserType_name_0 = "UserTypeAdmin"
    _UserType_name_1 = "UserTypeNormal"
)

func (i UserType) String() string {
    switch {
    case i == 99:
        return _UserType_name_0
    case i == 1:
        return _UserType_name_1
    default:
        return "UserType(" + strconv.FormatInt(int64(i), 10) + ")"
    }
}

在编码过程中,我们也可以编写类似于 stringer 的工具,去提高编码的效率。标准库 go/ast 和 go/fmt 分别提供了语言语法树解析生成和代码格式化的能力,通过这两个库可以类似编写 HTML 模板一样简单实现代码生成。

结语#

There is no such thing as metaprogramming. It’s just programming all the way down.

——《Metaprogramming Ruby 2》

本文了解了各种语言、各种场景下的元编程技术,元编程实际上在我们平时开发中都会遇到,但很少会思考到这实际上是一种元编程技术。正如《Ruby 元编程》里面说的一样,从来没有什么元编程,有的只是编程。