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