Go
golang

Golangの実行ファイルを複数まとめてトータルのファイルサイズを減らす工夫(busybox方式)

Golangの実行ファイルはサイズが大きい

Golangはスタティックリンクされた実行ファイルを生成します。そのため実行ファイルのサイズは大きくなります。
ストレージの容量が十分あれば、これはとるに足らないことなのですが組み込みLinuxではストレージにNANDフラッシュメモリを使用していて容量が少ないこともあります。
残り容量が30MBしかないところに、3MBくらいの実行ファイルがぽんぽん増えていったりするとかなりつらい状況になります。

まずはひとつのファイルサイズを減らす

go buildのときに不要なシンボルを削るオプションをつけます。

go build -ldflags '-s -w'

実行ファイルを複数まとめてトータルのファイルサイズを減らす工夫

組み込みLinuxを使っている人なら通じると思うのですが、いわゆる「busybox方式」です。
Golangで書かれた複数のプログラムをひとつの実行ファイルにリンクしてしまいます。これによって重複して使用されているライブラリの分のサイズが削減できます。
元々の名前でシンボリックリンクを作成しておき、実行ファイルの大元で自分がどのコマンド名で起動されたかをみて、それに応じて分岐するようにします。

このためにソースコード作成時にルールを導入します。

  • mainパッケージを使用しない。それぞれ独自パッケージを定義する。
  • 実行開始のメソッドはMain()とする。

単体デバッグのときと結合して動かすときではmainパッケージの内容が入れ替わります。

具体例

GOPATHのディレクトリを起点として以下のようなソースファイルとMakefileがあります。

$ find . -type f
./src/bar/bar.go
./src/bar/main/main.go
./src/bar/Makefile
./src/one_binary/main.go
./src/one_binary/Makefile
./src/foo/foo.go
./src/foo/main/main.go
./src/foo/Makefile
./src/baz/main/main.go
./src/baz/baz.go
./src/baz/Makefile

foo, bar, baz がそれぞれのプログラムで、one_binaryが結合するときのディレクトリです。

それぞれのプログラムのソースツリー

src/foo/foo.go
package foo

import "fmt"

func Main() {
    fmt.Printf("Hello, I am foo.\n")
}
src/foo/main/main.go
package main

import (
    "foo"
)

func main() {
    foo.Main()
}
src/foo/Makefile
TARGET = foo
GO_SRCS := $(shell find . -name "*.go")

all: $(TARGET)

$(TARGET): $(GO_SRCS)
    go build -ldflags '-s -w' -o $@ main/main.go

clean:
    rm -f $(TARGET)

makeして実行するとこうなります。

$ cd src/foo/
$ make
go build -ldflags '-s -w' -o foo main/main.go
$ ls -l foo
-rwxrwxr-x 1 koba koba 1032704 Jan 16 12:47 foo
$ ./foo
Hello, I am foo.

bar, bazも同様のプログラムです。
実行ファイルのサイズはひとつが約1MBあります。3つだと合計で約3MBになります。

結合用ディレクトリ

src/one_binary/main.go
package main

import (
    "log"
    "os"
    "path/filepath"

    "bar"
    "baz"
    "foo"
)

func main() {
    _, cmdname := filepath.Split(os.Args[0])
    switch cmdname {
    case "foo":
        foo.Main()
    case "bar":
        bar.Main()
    case "baz":
        baz.Main()
    default:
        log.Printf("cmdname=%s is not found.\n", cmdname)
    }
}

mainパッケージのmain()の中で、自分が起動されたときのコマンド名をみて、それに対応するパッケージのMain()を呼び出します。

src/one_binary/Makefile
TARGET = gobin

all: $(TARGET)

$(TARGET): force
    go build -ldflags '-s -w' -o $@
    ln -s $@ foo
    ln -s $@ bar
    ln -s $@ baz

clean:
    rm -f $(TARGET)
    rm -f foo bar baz

force:

$ cd src/one_binary/
$ make
go build -ldflags '-s -w' -o gobin
ln -s gobin foo
ln -s gobin bar
ln -s gobin baz
$ ls -l
total 1184
lrwxrwxrwx 1 koba koba       5 Jan 16 12:52 bar -> gobin
lrwxrwxrwx 1 koba koba       5 Jan 16 12:52 baz -> gobin
lrwxrwxrwx 1 koba koba       5 Jan 16 12:52 foo -> gobin
-rwxrwxr-x 1 koba koba 1201280 Jan 16 12:52 gobin
-rw-rw-r-- 1 koba koba     297 Jan 16 12:18 main.go
-rw-rw-r-- 1 koba koba     177 Jan 16 12:24 Makefile
$ ./foo
Hello, I am foo.
$ ./bar
Hello, I am bar.
$ ./baz
Hello, I am baz.

gobinはfoo, bar, bazがひとつに結合された実行ファイルです。サイズは約1.2MB。
3つを別々にビルドした場合の合計のファイルサイズは約3MBなので、それに比べて大幅にストレージ容量を節約できました。これは小さなサンプルプログラムなのでサイズが小さいですが、実際に使用するプログラムではその差はもっと顕著なものになります。
mainパッケージを使用しないなどのルールが面倒ですが、その面倒に見合うくらいの効果はあると思います。理論的にはカーネルのメモリの使用量の削減、プロセスの起動高速化にもわずかながらでもよいほうに働くはずです。