如何将Golang的条件编译转化为?

摘要:写cc++或者rust的开发者应该对条件编译不陌生,条件编译顾名思义就是在编译时让代码中的一部分生效或者失效,从而控制编译时的代码执行路径,进而影响编译出来的程序的行为。 这有啥用呢?通常在编写跨平台代
写c/c++或者rust的开发者应该对条件编译不陌生,条件编译顾名思义就是在编译时让代码中的一部分生效或者失效,从而控制编译时的代码执行路径,进而影响编译出来的程序的行为。 这有啥用呢?通常在编写跨平台代码的时候有用。比如我想开发一个文件操作库,这个库有全平台统一的接口,然而各大操作系统提供的文件和文件系统api百花齐放,我们没法只用一套代码就让我们的库能在所有的操作系统上正常运行。 这时候就需要条件编译出场了,在Linux上我们只让适配了Linux的代码生效,在Windows上则只让Windows相关的代码生效其他失效。比如: #ifdef _Windows typedef HFILE file_handle #else typedef int file_handle #endif file_handle open_file(const char *path) { if (!path) { #ifdef _Windows return invalid_handle; #else return -1; #endif } #ifdef _Windows OFSTRUCT buffer; return OpenFile(path, &buffer, OF_READ); #else return open(path, O_RDONLY|O_CLOEXEC); #endif } 在这个例子里,Windows和Linux的api完全不同,为了隐藏这种不同我们用条件编译在不同平台上定义出了一组相同的接口,这样我们就无需关心平台差异了。 从上面的例子也可以看出,c/c++实现条件编译最常用的是依靠宏。通过在编译时指定特定平台的标识,这些预编译宏就能自动把不需要的代码剔除不进行编译。c和c++中另一种实现条件编译的做法是依赖构建系统,我们不再使用预编译宏,但会为每个平台都编写一份代码: // open_file_windows.c typedef HFILE file_handle file_handle open_file(const char *path) { if (!path) { return invalid_handle; } OFSTRUCT buffer; return OpenFile(path, &buffer, OF_READ); } // open_file_linux.c typedef int file_handle file_handle open_file(const char *path) { if (!path) { return -1; } return open(path, O_RDONLY|O_CLOEXEC); } 然后指定构建系统在编译Linux程序时只使用open_file_linux.c,在Windows上则只使用open_file_windows.c。这样同样可以把和当前平台无关的不兼容的代码排除掉。现在的构建系统如meson,cmake都可以轻松实现上述功能。 自称系统级的golang,自然也是支持条件编译的,而且它支持的方式是靠第二种——即依靠构建系统。 想要在golang中使用条件编译,也有两种办法。因为我们不使用宏,也没法在编译时给go build指定信息哪些代码不需要,所以需要一些手段来让go编译工具链识别出应该编译和应该忽略的代码。 第一种就是依赖文件后缀名。go的源代码文件的名字是有特殊规定的,符合下面格式的文件会被认为是在特定平台上需要被编译的文件: name_{system}_{arch}.go name_{system}_{arch}_test.go 其中system的取值和环境变量GOOS一样,常见的有windows、linux、darwin、unix,其中后缀是unix时文件会在Linux、bsd和darwin这些平台上编译。没有明确指定那么该文件就会在全平台有效,除非有额外指定我们后面会说的build tag。 arch的取值和GOARCH环境变量一样,都是常见的硬件平台比如amd64、arm64、loong64等等。有这些后缀的文件只会在为特定的硬件平台编译程序时才会生效并加入编译过程。如果没明确指定arch,则默认目标操作系统的所有支持的硬件平台上这个文件都会参与编译。 第一种方法简单易懂,但缺点也很明显,我们需要为每个平台都维护一份源代码文件,而且这些文件里必定会有很多重复的平台无关的代码,这对维护来说是个很大的负担。
阅读全文