golang的条件编译

news/2025/4/2 4:57:40/文章来源:https://www.cnblogs.com/apocelipes/p/18801276

写c/c++或者rust的开发者应该对条件编译不陌生,条件编译顾名思义就是在编译时让代码中的一部分生效或者失效,从而控制编译时的代码执行路径,进而影响编译出来的程序的行为。

这有啥用呢?通常在编写跨平台代码的时候有用。比如我想开发一个文件操作库,这个库有全平台统一的接口,然而各大操作系统提供的文件和文件系统api百花齐放,我们没法只用一套代码就让我们的库能在所有的操作系统上正常运行。

这时候就需要条件编译出场了,在Linux上我们只让适配了Linux的代码生效,在Windows上则只让Windows相关的代码生效其他失效。比如:

#ifdef _Windows
typedef HFILE file_handle
#else
typedef int file_handle
#endiffile_handle open_file(const char *path)
{if (!path) {
#ifdef _Windowsreturn invalid_handle;
#elsereturn -1;
#endif}#ifdef _WindowsOFSTRUCT buffer;return OpenFile(path, &buffer, OF_READ);
#elsereturn open(path, O_RDONLY|O_CLOEXEC);
#endif
}

在这个例子里,Windows和Linux的api完全不同,为了隐藏这种不同我们用条件编译在不同平台上定义出了一组相同的接口,这样我们就无需关心平台差异了。

从上面的例子也可以看出,c/c++实现条件编译最常用的是依靠宏。通过在编译时指定特定平台的标识,这些预编译宏就能自动把不需要的代码剔除不进行编译。c和c++中另一种实现条件编译的做法是依赖构建系统,我们不再使用预编译宏,但会为每个平台都编写一份代码:

// open_file_windows.c
typedef HFILE file_handlefile_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_handlefile_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一样,常见的有windowslinuxdarwinunix,其中后缀是unix时文件会在Linux、bsd和darwin这些平台上编译。没有明确指定那么该文件就会在全平台有效,除非有额外指定我们后面会说的build tag

arch的取值和GOARCH环境变量一样,都是常见的硬件平台比如amd64arm64loong64等等。有这些后缀的文件只会在为特定的硬件平台编译程序时才会生效并加入编译过程。如果没明确指定arch,则默认目标操作系统的所有支持的硬件平台上这个文件都会参与编译。

第一种方法简单易懂,但缺点也很明显,我们需要为每个平台都维护一份源代码文件,而且这些文件里必定会有很多重复的平台无关的代码,这对维护来说是个很大的负担。

所以第一种方案只适合那种平台间差异巨大的代码,一个典型的例子是go自己的runtime的代码,因为协程调度需要很多操作系统甚至硬件平台的功能做辅助,因此runtime在每个操作系统上出了自己的api之外差异很大,因此使用文件名后缀的形式分成多个文件维护是比较合适的。

第二种方法不再使用文件名后缀,而是依赖build tag这种东西来提示编译器哪些代码需要被编译。

build tag是go的一种编译指令,用于告诉编译器该文件需要在什么条件下才需要被编译:

//go:build 表达式

tag一般写在文件的开头(在版权声明之后)。其中表达式是一些tag的名字和简单的布尔运算符。比如:

//go:build !windows
表示文件在Windows以外的系统上才编译
//go:build linux && (arm64 || amd64)
表示在arm64或者amd64的Linux系统上才编译这个文件
//go:build ignore
特殊tag,表示文件不管在什么平台上都会被忽略,除非明确使用go run、go build或者go generate运行这个文件
//go:build 自定义tag名
表示只有在`go build -tags tag名`明确指定相同的tag名时才编译这个文件

预定义的tag的值其实就是前面文件名后缀那里提到过的systemarch。可以看到逻辑运算符和括号都可以使用,语义也和逻辑运算一样。使用tag的优点在于它可以让linux和Windows通用的逻辑出现在同一个文件里而不需要复制两份到_windows.go_linux.go里。更重要的是它允许我们自定义编译tag。

能自定义tag的话玩法就很多了,我们来看个例子,一个可以在编译时指定日志输出级别的玩具程序,它的特点在于低于指定级别的日志不仅不会输出,而且连代码都不会存在,真正的做到零开销。

通常控制日志输出级别都是这么做的:

func DebugLog(msg ...any) {if level > DEBUG {return}...
}
func InfoLog(msg ...any) {if level > INFO {return}...
}

然而这不可避免的需要一次if判断,如果函数比较复杂的话还需要付出一次额外的函数调用开销。

使用条件编译可以消除这些开销,首先是处理debug级别的日志函数:

// file log_debug.go
//go:build debug || (!info && !warning)
package logimport "fmt"func Debug(msg any) {fmt.Println("DEBUG:", msg)
}// file log_no_debug.go
//go:build info || warning
package logfunc Debug(_ any) {}

作为最低的级别,只有在指定了debug这个tag以及默认情况下才会生效,其他时间都是空函数。

info级别的处理是一样的,只有指定级别为debug和info时才生效:

// file log_info.go
//go:build !debug && !warning
package logimport "fmt"func Info(msg any) {fmt.Println("INFO:", msg)
}// file log_no_info.go
//go:build warningpackage logfunc Info(_ any) {}

最后是warning级别,这个级别的日志不管在什么时候都会输出,因此它不需要条件编译所以也不需要tag:

// file log_warning.go
package logimport "fmt"func Warning(msg any) {fmt.Println("WARN:", msg)
}

最后是main函数:

package mainimport "conditionalcompile/log"func main() {log.Debug("A debug level message")log.Info("A info level message")log.Warning("A warning level message")
}

因为我们把不生效的函数都写成了空函数,因此编译器会在编译时发现这些空函数的调用什么都没做,因此直接忽略掉它们,所以运行的时候不会产生任何额外的开销。

下面简单做个测试:

$ go run# 输出
DEBUG: A debug level message
INFO: A info level message
WARN: A warning level message$ go run -tags info .# 输出
INFO: A info level message
WARN: A warning level message$ go run -tags warning .# 输出
WARN: A warning level message

和我们预期的一致。不过我并不推荐你使用这个方法,因为它需要为每个日志函数编写两份代码,而且需要对编译tag做很复杂的逻辑运算,非常容易出错;而且运行时一次if判断一般也不会带来太多的性能开销,除非明确定位到了判断日志级别产生了不可接受的性能瓶颈,否则永远不要尝试使用上面的玩具代码。

不过生产实践里真的有使用自定义tag的例子:wire。

依赖注入工具wire让开发者把需要注入的依赖写入有特殊编译tag的源文件,这些源文件正常编译的时候不会被编译到程序里,使用wire工具生成注入代码的时候这些文件才会被识别,这样既可以正常实现依赖注入功能又不会对代码产生太大的影响。更具体的做法可以去看wire的使用教程。

至于选择在golang里选择哪种方式实现条件编译,这个得结合实际需求来看。至少像go自身的代码以及k8s中两种方法文件名后缀和build tag都有并行使用,最重要的选择依据还是要以方便自己和他人维护代码为准。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/908514.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

vscode同时编辑多行添加自定义递增数字

vscode有时会遇到需要添加大量递增的数字的工作,这样的劳动相对重复又机械,使用以下办法来解决这样重复机械的劳动 如果对于批量操作需要高度自定义话,可直接看最后一种方法 1.多行光标,批量操作 MAC:option+shift+鼠标点击进行选择可以实现多行批量操作,批量删除 2.可自…

从菜鸟到高手:Linux C/C++程序性能分析实战指南!

"你这程序怎么这么卡啊?能不能优化一下?" —— 你的leader,大概率大家好,我是小康。 你有没有这样的经历:辛辛苦苦写完的 C++ 程序,功能测试一切正常,但一到生产环境就被吐槽"太慢了"?作为开发者,我们经常被要求解决性能问题,但如何找出程序的性…

US104N-ASEMI家用电器专用US104N

US104N-ASEMI家用电器专用US104N编辑:LL US104N-ASEMI家用电器专用US104N 型号:US104N 品牌:ASEMI 封装:TO-220F 栅极触发电压(Vgt):1.3V。 栅极触发电流(Igt):0.2mA。 保持电流最大值(Ih):30mA。 正向重复电压(Vdrm):800V。 通态电流最大值(It(RMS)):4A。 …

【Nmap】扫描结果美化后的服务器端口安全管理

所有的服务器使用Nmap扫描一下服务器列表的txt就可以,Nmap扫描结果通常是个XML文件,不方便浏览,推荐几个脚本,做一个扫描结果优化 https://github.com/ssjt21/parser_nmap_xml_2_Excel https://github.com/sp4rkw/NmapToExcel https://github.com/chuanwei/nmaptocsvNMAP扫…

plink软件中计算距离矩阵

001、--distance选项,计算具有不同位点的数目plink --file test --distance --out result ## 结果文件为每一个个体具有不同的SNP的数目,下三角矩阵形式展示,省略了第一行和最后一列 。002、

STM32 + keil5 HelloWrold

硬件清单 1. STM32F407VET6 2. STLINK V2下载器(带线)环境配置 1. 安装ST-LINK2. 安装并配置 keil5 https://blog.csdn.net/qq_36535414/article/details/108947292 https://blog.csdn.net/weixin_43732386/article/details/1173752663. 接线并下载烧入完成后拔插ST-LINK V2,就…

MySQL 锁机制:数据库的交通管制系统

MySQL 锁机制:数据库的"交通管制系统" 🚦 在数据的高速公路上,没有红绿灯会怎样?一片混乱!MySQL 的锁机制就是数据库世界的交通规则... 什么是锁机制?🤔 锁机制是数据库用来控制并发访问的一种方式,确保在多人同时操作数据库时不会出现数据不一致或损坏。简…

揭秘AI自动化框架Browser-use(四):Browser-use记忆模块技术解析

在 AI 自动化任务中,记忆模块是实现复杂任务处理的关键组件。Browser-use 项目通过引入记忆模块,解决了 LLM 在连续性任务中的无状态性问题,使代理能够维持上下文连贯性,执行复杂多步骤任务,并从错误中学习和恢复。一、从一次失败的景点采集说起 在 AI 自动化任务中,记忆…

微软Dynamics 365Power Platform技术找工作|宝藏岗位+金饭碗+高薪潜力

【简单介绍】‌ (长沙爱码士IT培训是一家专门培训微软Dynamics 365 CRM和Power Platform技术的一家公司www.aimashi365.com)本人是长沙爱码士IT的金牌讲师,已经帮助上百位学员找到IT工作,对训微软Dynamics 365 CRM和Power Platform技术有兴趣的朋友,我们可以随时沟通交流。…

差分约束学习笔记

一.差分约束系统 如果一个系统有 \(n\) 个变量 \(x_1,x_2,,x_n\) 和 \(m\) 个约束条件(也是不等式)和\(m\) 个常量 \(w_1,w_2,,w_m\)。每一个不等式形如以下格式 \(x_i - x_j \le w_k\)(\(1 \le i,j \le n\),\(1 \le k \le m\))。则称之为差分约束系统。 这个名字的由来是…

9.6K+ Star!一个基于 SpringBoot + Vue3 的工作流引擎快速开发平台!

mldong —— 一个基于 SpringBoot + Vue3 实现的工作流引擎快速开发平台,采用前后端分离的模式,内置完整的权限架构。大家好,我是 Java陈序员。 今天,给大家介绍一个基于 SpringBoot + Vue3 的工作流引擎快速开发平台!关注微信公众号:【Java陈序员】,获取开源项目分享、…

搭建开源笔记平台:outline

折腾的意义 为什么要自己搭建一个笔记平台?没理由,就是突然想试试。有时候突然有个想法,搜了一下正好有合适的方案,就顺手试一下。其实已经有很多成熟的笔记软件,例如Notion/OneNote,但谁不想要一个数据完全在自己服务器的笔记呢。 开始搭建 这个搭建是真的麻烦,需要一堆…