Docker與Golang的巧妙結合
【編者的話】這是一個展示在使用 Go 語言時如何讓 Docker 更有用的提示與技巧的簡輯。例如,如何使用不同版本的 Go 工具鏈來編譯 Go 程式碼,如何交叉編譯到不同的平臺(並且測試結果!),或者如何製作真正小的容器映象。
下面的文章假定你已經安裝了 Docker。不必是最新版本(這篇文章不會使用 Docker 任何花哨的功能)。
沒有 go 的 Go
...意思是:“不用安裝go
就能使用 Go”
如果你寫 Go 程式碼,或者你對 Go 語言有一點點興趣,你肯定要安裝了 Go 編譯器和 Go 工具鏈,所以你可能想知道:“重點是什麼?”;但有些情況下,你想不安裝Go
就來編譯 Go。
- 機器上依舊有老版本 Go 1.2(你不能或不想更新),不得不使用這個程式碼庫,需要一個高版本的工具鏈。
- 想使用 Go1.5 的交叉編譯功能(例如,確保能從一個 Linux 系統建立作業系統 X 的二進位制檔案)。
- 想擁有多版本的 Go,但不想完全弄亂系統。
- 想 100% 確定專案和它所有的依賴,下載,建立和執行在一個純淨的系統上。
如果遇到上述情況,找 Docker 來解決!
在容器裡編譯一個程式
當你安裝了 Go,你可以執行go get -v github.com/user/repo
來下載,建立和安裝一個庫。(-v
只是資訊顯示,如果你喜歡工具鏈快速和靜默地執行,可以將它移除!)
你也可以執行go get github.com/user/repo/...
來下載,建立和安裝那個 repo(包括庫和二進位制檔案)裡面所有的東西。
我們可以在一個容器裡面這樣做!
試試這個:
docker run golang go get -v github.com/golang/example/hello/...
這將拉取 golang 映象(除非你已經有了,那它會馬上啟動),並且建立一個基於它的容器。在那個容器裡,go 會下載一個 “hello world” 的例子,建立它,安裝它。但它會把它安裝到這個容器裡……我們現在怎麼執行那個程式呢?
### 在容器裡執行程式 一個辦法是提交我們剛剛建立的容器,即,打包它到一個新的映象:
docker commit $(docker ps -lq) awesomeness
注意:docker ps –lq
輸出最後一個執行的容器的 ID(只有 ID!)。如果你是機器的唯一使用者,並且你從上一個命令開始沒有建立另一個容器,那這個容器就是你剛剛建立的 “hello world” 的例子。
現在,可以用剛剛構建的映象建立容器來執行程式:
docker run awesomeness hello
輸出會是Hello, Go examples!
。
閃光點
當用docker commit
構建映象時,可以用--change
標識指定任意Dockerfile命令。例如,可以使用一個CMD
或者ENTRYPOINT
命令以便docker run awesomeness
自動執行 hello。
### 在一次性容器上執行 如果不想建立額外的映象只想執行這個 Go 程式呢?
使用:
docker run --rm golang sh -c \
"go get github.com/golang/example/hello/... && exec hello"
等等,那些花哨的東西是什麼?
-
--rm
告訴 Docker CLI 一旦容器退出,就自動發起一個docker rm
命令。那樣,不會留下任何東西。 - 使用 shell 邏輯運算子
&&
把建立步驟(go get
)和執行步驟(exec hello
)聯接在一起。如果不喜歡 shell,&&
意思是 “與”。它允許第一部分go get...
,並且如果(而且僅僅是如果!)那部分執行成功,它將執行第二部分(exec hello
)。如果你想知道為什麼這樣:它像一個懶惰的and
計算器,只有當左邊的值是true
才計算右邊的。 - 傳遞命令到
sh –c
,因為如果是簡單的做docker run golang "go get ... && hello"
,Docker 將試著執行名為go SPACE get SPACE etc
的程式。並且那不會起作用。因此,我們啟動一個 shell,並讓 shell 執行命令序列。 - 使用
exec hello
而不是hello
:這將使用 hello 程式替代當前的程式(我們剛才啟動的 shell)。這確保hello
在容器裡是 PID 1。而不是 shell 的是 PID 1 而hello
作為一個子程式。這對這個微小的例子毫無用處,但是當執行更有用的程式,這將允許它們正確地接收外部訊號,因為外部訊號是傳送給容器裡的 PID 1。你可能會想,什麼訊號啊?好的例子是docker stop
,傳送SIGTERM
給容器的 PID 1。
### 使用不同版本的 Go
當使用golang
映象,Docker 擴充套件為golang:latest,
將(像你所猜的)對映到 Docker Hub 上的最新可用版本。
如果想用一個特定的 Go 版本,很容易:在映象名字後面用那個版本做標籤指定它。
例如,想用 Go 1.5,修改上面的例子,用golang:1.5
替換golang
:
docker run --rm golang:1.5 sh -c \
"go get github.com/golang/example/hello/... && exec hello"
你能在 Docker Hub 的Golang 映象頁面上看到所有可用的版本(和變數)。
### 在系統上安裝 好了,如果想在系統上執行編譯好的程式,而不是一個容器呢?我們將複製這個編譯了的二進位制檔案到容器外面。注意,僅當容器架構和主機架構匹配的時候,才會起作用;換言之,如果在 Linux 上執行 Docker。(我排除的可能是執行 Windows 容器的人!)
最容易在容器外獲得二進位制檔案的方法是對映$GOPATH/bin
目錄到一個本地目錄,在golang
容器裡,$GOPATH
是/go.
所以我們可以如下操作:
docker run -v /tmp/bin:/go/bin \
golang go get github.com/golang/example/hello/...
/tmp/bin/hello
如果在 Linux 上,將看到Hello, Go examples!
訊息。但如果是,例如在 Mac 上,可能會看到:
-bash:
/tmp/test/hello: cannot execute binary file
我們又能做什麼呢?
### 交叉編譯 Go 1.5 具備優秀的開箱即用交叉編譯能力,所以如果你的容器作業系統和/或架構和你的系統不匹配,根本不是問題!
開啟交叉編譯,需要設定GOOS
和/或GOARCH
。
例如,假設在 64 位的 Mac 上:
docker run -e GOOS=darwin -e GOARCH=amd64 -v /tmp/crosstest:/go/bin \
golang go get github.com/golang/example/hello/...
交叉編譯的輸出不是直接在$GOPATH/bin
,而是在$GOPATH/bin/$GOOS_$GOARCH.
。換言之,想執行程式,得執行/tmp/crosstest/darwin_amd64/hello.
。
### 直接安裝到 $PATH 如果在 Linux 上,甚至可以直接安裝到系統 bin 目錄:
docker run -v /usr/local/bin:/go/bin \
golang get github.com/golang/example/hello/...
然而,在 Mac 上,嘗試用/usr
作為一個卷將不能掛載 Mac 的檔案系統到容器。會掛載 Moby VM(小 Linux VM 藏在工具欄 Docker 圖示的後面)的/usr
目錄。(譯註:目前 Docker for Mac 版本可以自定義設定掛載路徑)
但可以使用/tmp
或者在你的 home 目錄下的什麼其它目錄,然後從這裡複製。
建立依賴映象
我們用這種技術產生的 Go 二進位制檔案是靜態連結的。這意味著所有需要執行的程式碼包括所有依賴都被嵌入了。動態連結的程式與之相反,不包含一些基本的庫(像 “libc”)並且使用系統範圍的複製,是在執行時確定的。
這意味著可以在容器裡放棄 Go 編譯好的程式,沒有別的,並且它會執行。
我們試試!
###scratch 映象
Docker 生態系統有一個特殊的映象:scratch.
這是一個空映象。它不需要被建立或者下載,因為定義的就是空的。
給新的 Go 依賴映象建立一個新的空目錄。
在這個新目錄,建立下面的 Dockerfile:
FROM scratch
COPY ./hello /hello
ENTRYPOINT ["/hello"]
這意味著:從 scratch 開始(一個空映象),增加hello
檔案到映象的根目錄,*定義hello
程式為啟動這個容器後預設執行的程式。
然後,產生hello
二進位制檔案如下:
docker run -v $(pwd):/go/bin --rm \
golang go get github.com/golang/example/hello/...
注意:不需要設定GOOS
和GOARCH
,正因為,想要一個執行在 Docker 容器裡的二進位制檔案,不是在主機上。所以不用設定這些變數!
然後,建立映象:
docker build -t hello .
測試它:
docker run hello
(將顯示 “Hello, Go examples!”)
最後但不重要,檢查映象的大小:
docker images hello
如果一切做得正確,這個映象大約 2M。相當好!
### 構建東西而不推送到 Github 當然,如果不得不推送到 GitHub,每次編譯都會浪費很多時間。
想在一個程式碼段上工作並在容器中建立它時,可以在golang
容器裡掛載一個本地目錄到/go
。所以$GOPATH
是持久呼叫:docker run -v $HOME/go:/go golang ....
但也可以掛載本地目錄到特定的路徑上,來 “過載” 一些包(那些在本地編輯的)。這是一個完整的例子:
# Adapt the two following environment variables if you are not running on a Mac
export GOOS=darwin GOARCH=amd64
mkdir go-and-docker-is-love
cd go-and-docker-is-love
git clone git://github.com/golang/example
cat example/hello/hello.go
sed -i .bak s/olleH/eyB/ example/hello/hello.go
docker run --rm \
-v $(pwd)/example:/go/src/github.com/golang/example \
-v $(pwd):/go/bin/${GOOS}_${GOARCH} \
-e GOOS -e GOARCH \
golang go get github.com/golang/example/hello/...
./hello
# Should display "Bye, Go examples!"
網路包和 CGo 的特殊情況
進入真實的 Go 程式碼世界前,必須承認的是:在二進位制檔案上有一點點偏差。如果在使用 CGo,或如果在使用net
包,Go 連結器將生成一個動態庫。這種情況下,net
包(裡面確實有許多有用的 Go 程式!),罪魁禍首是 DNS 解析。大多數系統都有一個花哨的,模組化的名稱解析系統(像名稱服務切換),它依賴於外掛,技術上,是動態庫。預設地,Go 將嘗試使用它;這樣,它將產生動態庫。
我們怎麼解決?
### 重用另一個版本的 libc
一個解決方法是用一個基礎映象,有那些程式功能所必需的庫。幾乎任何 “正規” 基於 GNU libc 的 Linux 發行版都能做到。所以,例如,使用FROM debian
或FROM fedora
,替代FROM scratch
。現在結果映象會比原來大一些;但至少,大出來的這一點將和系統裡其它映象共享。
注意:這種情況不能使用 Alpine,因為 Alpine 是使用 musl 庫而不是 GNU libc。
### 使用自己的 libc
另一個解決方案是像做手術般地提取需要的檔案,用COPY
替換容器裡的。結果容器會小。然而,這個提取過程困難又繁瑣,太多更深的細節要處理。
如果想自己看,看看前面提到的ldd
和名稱服務切換外掛。
### 用 netgo 生成靜態二進位制檔案
我們也可以指示 Go 不用系統的 libc,用本地 DNS 解析代替 Go 的netgo
。
要使用它,只需在go get
選項加入-tags netgo -installsuffix netgo
。
-
-tags netgo
指示工具鏈使用netgo
。 -
-installsuffix netgo
確保結果庫(任何)被一個不同的,非預設的目錄所替代。如果做多重go get
(或go build
)呼叫,這將避免程式碼建立和用不用 netgo 之間的衝突。如果像目前我們講到的這樣,在容器裡建立,是完全沒有必要的。因為這個容器裡面永遠沒有其他 Go 程式碼要編譯。但它是個好主意,習慣它,或至少知道這個標識存在。
SSL 證書的特殊情況
還有一件事,你會擔心,你的程式碼必須驗證 SSL 證書;例如,通過 HTTPS 聯接外部 API。這種情況,需要將根證書也放入容器裡,因為 Go 不會捆綁它們到二進位制檔案裡。
### 安裝 SSL 證書 再次,有很多可用的選擇,但最簡單的是使用一個已經存在的釋出裡面的包。
Alpine 是一個好的選擇,因為它非常小。下面的Dockerfile
將給你一個小的基礎映象,但捆綁了一個過期的跟證書:
FROM alpine:3.4
RUN apk add --no-cache ca-certificates apache2-utils
來看看吧,結果映象只有 6MB!
注意:--no-cache
選項告訴apk
(Alpine 包管理器)從 Alpine 的映象釋出上獲取可用包的列表,不儲存在磁碟上。你可能會看到 Dockerfiles 做這樣的事apt-get update && apt-get install ... && rm -rf /var/cache/apt/*
;這實現了(即在最終映象中不保留包快取)與一個單一標誌相當的東西。
一個附加的回報:把你的應用程式放入基於 Alpine 映象的容器,讓你獲得了一堆有用的工具。如果需要,現在你可以吧 shell 放入容器並在它執行時做點什麼。
打包
我們看到 Docker 如何幫助我們在乾淨獨立的環境裡編譯 Go 程式碼;如何使用不同版本的 Go 工具鏈;以及如何在不同的作業系統和平臺之間交叉編譯。
我們還看到 Go 如何幫我們給 Docker 建立小的,容器依賴映象,並且描述了一些靜態庫和網路依賴相關的微妙聯絡(沒別的意思)。
除了 Go 是真的適合 Docker 專案這個事實,我們希望展示給你的是,Go 和 Docker 如何相互借鑑並且一起工作得很好!
### 致謝 這最初是在 2016 年 GopherCon 駭客日提出的。
我要感謝所有的校對材料、提出建議和意見讓它更好的人,包括但不侷限於:
所有的錯誤和拼寫錯誤都是我自己的;所有的好東西都是他們的!
原文連結:Docker + Golang = <3(翻譯:陳晏娥 審校:田浩浩)
- 加微信實戰群請加微信(註明:實戰群):gocnio
相關文章
- Java 繼承與多型:程式碼重用與靈活性的巧妙結合Java繼承多型
- SpringBoot基礎教程(十六)——與docker的結合Spring BootDocker
- View Binding 與Kotlin委託屬性的巧妙結合,告別垃圾程式碼!ViewKotlin
- 將即時戰略玩法與戰棋巧妙結合!呵呵魚與它的《戰律》系列
- golang 與 docker 初體驗GolangDocker
- 【潛力佳作】對沖自走棋與差異化美術的巧妙結合 - 《Master Piece》產品分析AST
- Docker結合.Net Core初步使用Docker
- SpringBoot與mongodb的結合Spring BootMongoDB
- WinAMS 與 Jenkins 的結合Jenkins
- golang 效能優化分析:benchmark 結合 pprofGolang優化
- 如何結合phpstorm配置在docker中的xdebugPHPORMDocker
- Golang從合併連結串列聊遞迴Golang遞迴
- Docker--docker ps 命令與結果解析Docker
- Docker:技術和商業的結合點在哪裡?Docker
- Retrofit與LiveData結合LiveData
- spark 與 yarn 結合SparkYarn
- 加密與水印結合加密
- 區塊鏈與金融的結合區塊鏈
- chatgpt與其他行業的結合ChatGPT行業
- async 與 Thread 的錯誤結合thread
- dockerfile中ENTRYPOINT與CMD的結合Docker
- 巧妙的CSSCSS
- Redis 結合 Docker 搭建叢集,並整合SpringBootRedisDockerSpring Boot
- 結合Docker執行Spring Cloud微服務的多種方式DockerSpringCloud微服務
- 區塊鏈學習-Golang 與智慧合約的互動(一)區塊鏈Golang
- pm2與go的完美結合Go
- 自定義梯形view與XRecyclerView的結合View
- 速度與精度的結合 - EfficientNet 詳解
- 大模型API與前端的結合使用大模型API前端
- ML與BI結合的產品:Tellius
- 休閒與競技的結合:我與《公主連結 Re:Dive》的邂逅
- 巧妙的煎餅
- rem與em的區別||結合使用rem與emREM
- Redis 結合 Docker 搭建哨兵+主從,並整合SpringBootRedisDockerSpring Boot
- 基於gin的golang web開發:dockerGolangWebDocker
- 預測區塊鏈與司法的結合區塊鏈
- SpringBoot與單元測試JUnit的結合Spring Boot
- SpringBoot:結合 SpringBoot 與 Grails 3Spring BootAI
- 理解Golang的Time結構Golang