golang中進(jìn)行條件編譯的示例詳解
寫(xiě)c/c++或者rust的開(kāi)發(fā)者應(yīng)該對(duì)條件編譯不陌生,條件編譯顧名思義就是在編譯時(shí)讓代碼中的一部分生效或者失效,從而控制編譯時(shí)的代碼執(zhí)行路徑,進(jìn)而影響編譯出來(lái)的程序的行為。
這有啥用呢?通常在編寫(xiě)跨平臺(tái)代碼的時(shí)候有用。比如我想開(kāi)發(fā)一個(gè)文件操作庫(kù),這個(gè)庫(kù)有全平臺(tái)統(tǒng)一的接口,然而各大操作系統(tǒng)提供的文件和文件系統(tǒng)api百花齊放,我們沒(méi)法只用一套代碼就讓我們的庫(kù)能在所有的操作系統(tǒng)上正常運(yùn)行。
這時(shí)候就需要條件編譯出場(chǎng)了,在Linux上我們只讓適配了Linux的代碼生效,在Windows上則只讓W(xué)indows相關(guān)的代碼生效其他失效。比如:
#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
}
在這個(gè)例子里,Windows和Linux的api完全不同,為了隱藏這種不同我們用條件編譯在不同平臺(tái)上定義出了一組相同的接口,這樣我們就無(wú)需關(guān)心平臺(tái)差異了。
從上面的例子也可以看出,c/c++實(shí)現(xiàn)條件編譯最常用的是依靠宏。通過(guò)在編譯時(shí)指定特定平臺(tái)的標(biāo)識(shí),這些預(yù)編譯宏就能自動(dòng)把不需要的代碼剔除不進(jìn)行編譯。c和c++中另一種實(shí)現(xiàn)條件編譯的做法是依賴構(gòu)建系統(tǒng),我們不再使用預(yù)編譯宏,但會(huì)為每個(gè)平臺(tái)都編寫(xiě)一份代碼:
// 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);
}
然后指定構(gòu)建系統(tǒng)在編譯Linux程序時(shí)只使用open_file_linux.c,在Windows上則只使用open_file_windows.c。這樣同樣可以把和當(dāng)前平臺(tái)無(wú)關(guān)的不兼容的代碼排除掉?,F(xiàn)在的構(gòu)建系統(tǒng)如meson,cmake都可以輕松實(shí)現(xiàn)上述功能。
自稱系統(tǒng)級(jí)的golang,自然也是支持條件編譯的,而且它支持的方式是靠第二種——即依靠構(gòu)建系統(tǒng)。
想要在golang中使用條件編譯,也有兩種辦法。因?yàn)槲覀儾皇褂煤?,也沒(méi)法在編譯時(shí)給go build指定信息哪些代碼不需要,所以需要一些手段來(lái)讓go編譯工具鏈識(shí)別出應(yīng)該編譯和應(yīng)該忽略的代碼。
第一種就是依賴文件后綴名。go的源代碼文件的名字是有特殊規(guī)定的,符合下面格式的文件會(huì)被認(rèn)為是在特定平臺(tái)上需要被編譯的文件:
name_{system}_{arch}.go
name_{system}_{arch}_test.go
其中system的取值和環(huán)境變量GOOS一樣,常見(jiàn)的有windows、linux、darwin、unix,其中后綴是unix時(shí)文件會(huì)在Linux、bsd和darwin這些平臺(tái)上編譯。沒(méi)有明確指定那么該文件就會(huì)在全平臺(tái)有效,除非有額外指定我們后面會(huì)說(shuō)的build tag。
arch的取值和GOARCH環(huán)境變量一樣,都是常見(jiàn)的硬件平臺(tái)比如amd64、arm64、loong64等等。有這些后綴的文件只會(huì)在為特定的硬件平臺(tái)編譯程序時(shí)才會(huì)生效并加入編譯過(guò)程。如果沒(méi)明確指定arch,則默認(rèn)目標(biāo)操作系統(tǒng)的所有支持的硬件平臺(tái)上這個(gè)文件都會(huì)參與編譯。
第一種方法簡(jiǎn)單易懂,但缺點(diǎn)也很明顯,我們需要為每個(gè)平臺(tái)都維護(hù)一份源代碼文件,而且這些文件里必定會(huì)有很多重復(fù)的平臺(tái)無(wú)關(guān)的代碼,這對(duì)維護(hù)來(lái)說(shuō)是個(gè)很大的負(fù)擔(dān)。
所以第一種方案只適合那種平臺(tái)間差異巨大的代碼,一個(gè)典型的例子是go自己的runtime的代碼,因?yàn)閰f(xié)程調(diào)度需要很多操作系統(tǒng)甚至硬件平臺(tái)的功能做輔助,因此runtime在每個(gè)操作系統(tǒng)上出了自己的api之外差異很大,因此使用文件名后綴的形式分成多個(gè)文件維護(hù)是比較合適的。
第二種方法不再使用文件名后綴,而是依賴build tag這種東西來(lái)提示編譯器哪些代碼需要被編譯。
build tag是go的一種編譯指令,用于告訴編譯器該文件需要在什么條件下才需要被編譯:
//go:build 表達(dá)式
tag一般寫(xiě)在文件的開(kāi)頭(在版權(quán)聲明之后)。其中表達(dá)式是一些tag的名字和簡(jiǎn)單的布爾運(yùn)算符。比如:
1.go:build !windows
表示文件在Windows以外的系統(tǒng)上才編譯
2.go:build linux && (arm64 || amd64)
表示在arm64或者amd64的Linux系統(tǒng)上才編譯這個(gè)文件
3.go:build ignore
特殊tag,表示文件不管在什么平臺(tái)上都會(huì)被忽略,除非明確使用go run、go build或者go generate運(yùn)行這個(gè)文件
4.go:build 自定義tag名
表示只有在`go build -tags tag名`明確指定相同的tag名時(shí)才編譯這個(gè)文件
預(yù)定義的tag的值其實(shí)就是前面文件名后綴那里提到過(guò)的system和arch??梢钥吹竭壿嬤\(yùn)算符和括號(hào)都可以使用,語(yǔ)義也和邏輯運(yùn)算一樣。使用tag的優(yōu)點(diǎn)在于它可以讓linux和Windows通用的邏輯出現(xiàn)在同一個(gè)文件里而不需要復(fù)制兩份到_windows.go和_linux.go里。更重要的是它允許我們自定義編譯tag。
能自定義tag的話玩法就很多了,我們來(lái)看個(gè)例子,一個(gè)可以在編譯時(shí)指定日志輸出級(jí)別的玩具程序,它的特點(diǎn)在于低于指定級(jí)別的日志不僅不會(huì)輸出,而且連代碼都不會(huì)存在,真正的做到零開(kāi)銷。
通??刂迫罩据敵黾?jí)別都是這么做的:
func DebugLog(msg ...any) {
if level > DEBUG {
return
}
...
}
func InfoLog(msg ...any) {
if level > INFO {
return
}
...
}
然而這不可避免的需要一次if判斷,如果函數(shù)比較復(fù)雜的話還需要付出一次額外的函數(shù)調(diào)用開(kāi)銷。
使用條件編譯可以消除這些開(kāi)銷,首先是處理debug級(jí)別的日志函數(shù):
// file log_debug.go
//go:build debug || (!info && !warning)
package log
import "fmt"
func Debug(msg any) {
fmt.Println("DEBUG:", msg)
}
// file log_no_debug.go
//go:build info || warning
package log
func Debug(_ any) {}
作為最低的級(jí)別,只有在指定了debug這個(gè)tag以及默認(rèn)情況下才會(huì)生效,其他時(shí)間都是空函數(shù)。
info級(jí)別的處理是一樣的,只有指定級(jí)別為debug和info時(shí)才生效:
// file log_info.go
//go:build !debug && !warning
package log
import "fmt"
func Info(msg any) {
fmt.Println("INFO:", msg)
}
// file log_no_info.go
//go:build warning
package log
func Info(_ any) {}
最后是warning級(jí)別,這個(gè)級(jí)別的日志不管在什么時(shí)候都會(huì)輸出,因此它不需要條件編譯所以也不需要tag:
// file log_warning.go
package log
import "fmt"
func Warning(msg any) {
fmt.Println("WARN:", msg)
}
最后是main函數(shù):
package main
import "conditionalcompile/log"
func main() {
log.Debug("A debug level message")
log.Info("A info level message")
log.Warning("A warning level message")
}
因?yàn)槲覀儼巡簧У暮瘮?shù)都寫(xiě)成了空函數(shù),因此編譯器會(huì)在編譯時(shí)發(fā)現(xiàn)這些空函數(shù)的調(diào)用什么都沒(méi)做,因此直接忽略掉它們,所以運(yùn)行的時(shí)候不會(huì)產(chǎn)生任何額外的開(kāi)銷。
下面簡(jiǎn)單做個(gè)測(cè)試:
$ 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
和我們預(yù)期的一致。不過(guò)我并不推薦你使用這個(gè)方法,因?yàn)樗枰獮槊總€(gè)日志函數(shù)編寫(xiě)兩份代碼,而且需要對(duì)編譯tag做很復(fù)雜的邏輯運(yùn)算,非常容易出錯(cuò);而且運(yùn)行時(shí)一次if判斷一般也不會(huì)帶來(lái)太多的性能開(kāi)銷,除非明確定位到了判斷日志級(jí)別產(chǎn)生了不可接受的性能瓶頸,否則永遠(yuǎn)不要嘗試使用上面的玩具代碼。
不過(guò)生產(chǎn)實(shí)踐里真的有使用自定義tag的例子:wire。
依賴注入工具wire讓開(kāi)發(fā)者把需要注入的依賴寫(xiě)入有特殊編譯tag的源文件,這些源文件正常編譯的時(shí)候不會(huì)被編譯到程序里,使用wire工具生成注入代碼的時(shí)候這些文件才會(huì)被識(shí)別,這樣既可以正常實(shí)現(xiàn)依賴注入功能又不會(huì)對(duì)代碼產(chǎn)生太大的影響。更具體的做法可以去看wire的使用教程。
至于選擇在golang里選擇哪種方式實(shí)現(xiàn)條件編譯,這個(gè)得結(jié)合實(shí)際需求來(lái)看。至少像go自身的代碼以及k8s中兩種方法文件名后綴和build tag都有并行使用,最重要的選擇依據(jù)還是要以方便自己和他人維護(hù)代碼為準(zhǔn)。
到此這篇關(guān)于golang中進(jìn)行條件編譯的示例詳解的文章就介紹到這了,更多相關(guān)go條件編譯內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
golang?db事務(wù)的統(tǒng)一封裝的實(shí)現(xiàn)
這篇文章主要介紹了golang db事務(wù)的統(tǒng)一封裝的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-12-12
深入解析Go語(yǔ)言中for循環(huán)的寫(xiě)法
這篇文章主要介紹了Go語(yǔ)言中for循環(huán)的寫(xiě)法,是Golang入門(mén)學(xué)習(xí)中的基礎(chǔ)知識(shí),需要的朋友可以參考下2015-10-10
golang實(shí)現(xiàn)對(duì)JavaScript代碼混淆
在Go語(yǔ)言中,你可以使用一些工具來(lái)混淆JavaScript代碼,一個(gè)常用的工具是Terser,它可以用于壓縮和混淆JavaScript代碼,你可以通過(guò)Go語(yǔ)言的`os/exec`包來(lái)調(diào)用Terser工具,本文給通過(guò)一個(gè)簡(jiǎn)單的示例給大家介紹一下,感興趣的朋友可以參考下2024-01-01
使用client-go工具調(diào)用kubernetes API接口的教程詳解(v1.17版本)
這篇文章主要介紹了使用client-go工具調(diào)kubernetes API接口(v1.17版本),本文通過(guò)圖文實(shí)例相結(jié)合給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-08-08

