golang中進(jìn)行條件編譯的示例詳解
寫c/c++或者rust的開發(fā)者應(yīng)該對(duì)條件編譯不陌生,條件編譯顧名思義就是在編譯時(shí)讓代碼中的一部分生效或者失效,從而控制編譯時(shí)的代碼執(zhí)行路徑,進(jìn)而影響編譯出來的程序的行為。
這有啥用呢?通常在編寫跨平臺(tái)代碼的時(shí)候有用。比如我想開發(fā)一個(gè)文件操作庫,這個(gè)庫有全平臺(tái)統(tǒng)一的接口,然而各大操作系統(tǒng)提供的文件和文件系統(tǒng)api百花齊放,我們沒法只用一套代碼就讓我們的庫能在所有的操作系統(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)上定義出了一組相同的接口,這樣我們就無需關(guān)心平臺(tái)差異了。
從上面的例子也可以看出,c/c++實(shí)現(xiàn)條件編譯最常用的是依靠宏。通過在編譯時(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)都編寫一份代碼:
// 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)無關(guān)的不兼容的代碼排除掉。現(xiàn)在的構(gòu)建系統(tǒng)如meson,cmake都可以輕松實(shí)現(xiàn)上述功能。
自稱系統(tǒng)級(jí)的golang,自然也是支持條件編譯的,而且它支持的方式是靠第二種——即依靠構(gòu)建系統(tǒng)。
想要在golang中使用條件編譯,也有兩種辦法。因?yàn)槲覀儾皇褂煤?,也沒法在編譯時(shí)給go build指定信息哪些代碼不需要,所以需要一些手段來讓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一樣,常見的有windows、linux、darwin、unix,其中后綴是unix時(shí)文件會(huì)在Linux、bsd和darwin這些平臺(tái)上編譯。沒有明確指定那么該文件就會(huì)在全平臺(tái)有效,除非有額外指定我們后面會(huì)說的build tag。
arch的取值和GOARCH環(huán)境變量一樣,都是常見的硬件平臺(tái)比如amd64、arm64、loong64等等。有這些后綴的文件只會(huì)在為特定的硬件平臺(tái)編譯程序時(shí)才會(huì)生效并加入編譯過程。如果沒明確指定arch,則默認(rèn)目標(biāo)操作系統(tǒng)的所有支持的硬件平臺(tái)上這個(gè)文件都會(huì)參與編譯。
第一種方法簡單易懂,但缺點(diǎn)也很明顯,我們需要為每個(gè)平臺(tái)都維護(hù)一份源代碼文件,而且這些文件里必定會(huì)有很多重復(fù)的平臺(tái)無關(guān)的代碼,這對(duì)維護(hù)來說是個(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這種東西來提示編譯器哪些代碼需要被編譯。
build tag是go的一種編譯指令,用于告訴編譯器該文件需要在什么條件下才需要被編譯:
//go:build 表達(dá)式
tag一般寫在文件的開頭(在版權(quán)聲明之后)。其中表達(dá)式是一些tag的名字和簡單的布爾運(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í)就是前面文件名后綴那里提到過的system和arch。可以看到邏輯運(yùn)算符和括號(hào)都可以使用,語義也和邏輯運(yùn)算一樣。使用tag的優(yōu)點(diǎn)在于它可以讓linux和Windows通用的邏輯出現(xiàn)在同一個(gè)文件里而不需要復(fù)制兩份到_windows.go和_linux.go里。更重要的是它允許我們自定義編譯tag。
能自定義tag的話玩法就很多了,我們來看個(gè)例子,一個(gè)可以在編譯時(shí)指定日志輸出級(jí)別的玩具程序,它的特點(diǎn)在于低于指定級(jí)別的日志不僅不會(huì)輸出,而且連代碼都不會(huì)存在,真正的做到零開銷。
通??刂迫罩据敵黾?jí)別都是這么做的:
func DebugLog(msg ...any) { if level > DEBUG { return } ... } func InfoLog(msg ...any) { if level > INFO { return } ... }
然而這不可避免的需要一次if判斷,如果函數(shù)比較復(fù)雜的話還需要付出一次額外的函數(shù)調(diào)用開銷。
使用條件編譯可以消除這些開銷,首先是處理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ù)都寫成了空函數(shù),因此編譯器會(huì)在編譯時(shí)發(fā)現(xiàn)這些空函數(shù)的調(diào)用什么都沒做,因此直接忽略掉它們,所以運(yùn)行的時(shí)候不會(huì)產(chǎ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ù)期的一致。不過我并不推薦你使用這個(gè)方法,因?yàn)樗枰獮槊總€(gè)日志函數(shù)編寫兩份代碼,而且需要對(duì)編譯tag做很復(fù)雜的邏輯運(yùn)算,非常容易出錯(cuò);而且運(yùn)行時(shí)一次if判斷一般也不會(huì)帶來太多的性能開銷,除非明確定位到了判斷日志級(jí)別產(chǎn)生了不可接受的性能瓶頸,否則永遠(yuǎn)不要嘗試使用上面的玩具代碼。
不過生產(chǎn)實(shí)踐里真的有使用自定義tag的例子:wire。
依賴注入工具wire讓開發(fā)者把需要注入的依賴寫入有特殊編譯tag的源文件,這些源文件正常編譯的時(shí)候不會(huì)被編譯到程序里,使用wire工具生成注入代碼的時(shí)候這些文件才會(huì)被識(shí)別,這樣既可以正常實(shí)現(xiàn)依賴注入功能又不會(huì)對(duì)代碼產(chǎn)生太大的影響。更具體的做法可以去看wire的使用教程。
至于選擇在golang里選擇哪種方式實(shí)現(xiàn)條件編譯,這個(gè)得結(jié)合實(shí)際需求來看。至少像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),文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-12-12golang實(shí)現(xiàn)對(duì)JavaScript代碼混淆
在Go語言中,你可以使用一些工具來混淆JavaScript代碼,一個(gè)常用的工具是Terser,它可以用于壓縮和混淆JavaScript代碼,你可以通過Go語言的`os/exec`包來調(diào)用Terser工具,本文給通過一個(gè)簡單的示例給大家介紹一下,感興趣的朋友可以參考下2024-01-01使用client-go工具調(diào)用kubernetes API接口的教程詳解(v1.17版本)
這篇文章主要介紹了使用client-go工具調(diào)kubernetes API接口(v1.17版本),本文通過圖文實(shí)例相結(jié)合給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-08-08