- 投稿日:2020-04-14T23:20:15+09:00
いつも使っているGoのBuildオプションがバイナリサイズにどの程度影響しているか調べた
はじめに
GoのBuildオプションでサイズに影響しそうなオプションで、どの程度サイズに差がでるか試しました。
以下の環境です。自分のよくあるGoのユースケースとしてWebAPI開発あるので、go-swaggerやAWS SDK Goに依存したプロジェクトで試してみました。
$cat /etc/os-release | head -n2 NAME="Ubuntu" VERSION="18.04.4 LTS (Bionic Beaver)" $ go version go version go1.14.2 linux/amd64比較
# 1. ノーオプション go build -o nooption main.go # 2. ldflagsでdebug情報を削除 go build -ldflags="-s -w" -o ldflags main.go # 3. trimpathでパス情報を削除 go build -ldflags="-s -w" -trimpath -o trimpath main.go
-ldflags
- Go Documentによるとリンカーへの引数。
-s
,-w
はそれぞれシンボルテーブルとデバック情報とのこと。-trimpath
- Go1.13から追加されたオプション。バイナリから全ファイルシステムパスを削除する
- panic で表示されるパスからBuild時のディレクトリ構造を消すためにも指定
サイズ
No Name Size [KB] 1と比べたときのサイズ 1 nooption 22,501 100% 2 ldflags 16,996 75.5% 3 trimpath 16,968 75.4% 2,3はほぼ変わらないですが、ファイルシステムパスを削除した分、多少サイズが減っています。(30KBなので小さいシステムだとインパクト大きいかもしれません)
まとめ
- ライブラリ提供でなく実行バイナリをビルドするのであれば、
-ldflags="-s -w"
や-trimpath
をつけるのがベターで、そこそこ依存関係があるPJでも25%ほどバイナリサイズを小さくすることができた
- 投稿日:2020-04-14T23:20:15+09:00
よく使うGoのBuildオプションがバイナリサイズにどの程度影響しているか調べた
はじめに
GoのBuildオプションでサイズに影響しそうなオプションで、どの程度サイズに差がでるか試しました。
以下の環境です。自分のよくあるGoのユースケースとしてWebAPI開発あるので、go-swaggerやAWS SDK Goに依存したプロジェクトで試してみました。
$cat /etc/os-release | head -n2 NAME="Ubuntu" VERSION="18.04.4 LTS (Bionic Beaver)" $ go version go version go1.14.2 linux/amd64比較
# 1. ノーオプション go build -o nooption main.go # 2. ldflagsでdebug情報を削除 go build -ldflags="-s -w" -o ldflags main.go # 3. trimpathでパス情報を削除 go build -ldflags="-s -w" -trimpath -o trimpath main.go
-ldflags
- Go Documentによるとリンカーへの引数。
-s
,-w
はそれぞれシンボルテーブルとデバック情報とのこと。-trimpath
- Go1.13から追加されたオプション。バイナリから全ファイルシステムパスを削除する
- panic で表示されるパスからBuild時のディレクトリ構造を消すためにも指定
UPX
Goのバイナリサイズを削減する で初めて知ったのですが、UPXは実行形式を保ったまま、バイナリを圧縮するツールとのことです。(ライセンスは記事にかかれている通り、GPLに例外事項をつけたライセンスですので、本番環境への利用時は念のため確認してください)
UbuntuにUPXをインストールします。
インストール$sudo apt-get update -y $sudo apt-get install -y upx # Versionの確認 $upx --version | head -4 upx 3.94 UCL data compression library 1.03 zlib data compression library 1.2.11 LZMA SDK version 4.43UPXを実行してみます。
https://linux.die.net/man/1/upx によると、-1から-9まで圧縮レベルを指定できるそうです。
--best
で最善を尽くしてくれるそうです。ちなみにデフォルトは -8 だそうです。upx -1 -o upx1 trimpath upx -8 -o upx8 trimpath upx -9 -o upx9 trimpath upx --best -o upx_best trimpath
-8
より上のレベルはかなりパックに時間がかかったので、リモートサーバならともかく、ローカル環境で毎Build時にやることはオススメしないなって思いました。特に--best
は数分はかかった気がします。
--lzma
で LZMA(Lempel-Ziv-Markov chain-Algorithm) を有効にできるとのこと。LZMA利用upx --lzma -o upx_lzma trimpath
--brute
,--ultra-brute
のオプションもあるようなので試してみます。brute
はman
で確認すると..Compression tuning options:
--brute try all available compression methods & filters [slow]
--ultra-brute try even more compression variants [very slow]...とのことで、良い圧縮率が期待できそうです。
brute利用upx --brute -o brute trimpath upx --ultra-brute -o ultra_brute trimpathこれも
--best
と同様にかなりパック時間がかかりました。サイズ
No Name Size [KB] 1と比べたときのサイズ 1 nooption 22,501 100% 2 ldflags 16,996 75.5% 3 trimpath 16,968 75.4% 4 3 + UPX 1 6,622 29.4% 5 3 + UPX 8 5,635 25.0% 6 3 + UPX 9 5,564 24.7% 7 3 + UPX BEST 5,522 24.5% 8 3 + UPX LZMA 4232 18.9% 9 3 + UPX BRUTE 4218 18.7% 10 3 + UPX ULTRA BRUTE 4198 18.7% 2,3はほぼ変わらないですが、ファイルシステムパスを削除した分、多少サイズが減っています。(30KBなので小さいシステムだとインパクト大きいかもしれません)
4以降のUPXが劇的過ぎてビビるですが、試しにパックしたバイナリを実行してみるともちろん普通に起動しました。Stacktraceなどの情報は消えるんですかね?試してない無いですが。今回は処理時間を記載していないですが、UPXを使うのであれば
--lzma
が1番バランスが良さそうだなと思いました。 BRUTEは時間がかかるので。まとめ
- ライブラリ提供でなく実行バイナリをビルドするのであれば、
-ldflags="-s -w"
や-trimpath
をつけるのがベターで、そこそこ依存関係があるPJでも25%ほどバイナリサイズを小さくすることができた- UPXの効果が素晴らしい
- 投稿日:2020-04-14T23:20:15+09:00
よく使うGoのBuildオプションと、UPXの圧縮がバイナリサイズにどの程度影響しているか調べた
はじめに
GoのBuildオプションでサイズに影響しそうなオプションで、どの程度サイズに差がでるか試しました。
以下の環境です。自分のよくあるGoのユースケースとしてWebAPI開発あるので、go-swaggerやAWS SDK Goに依存したプロジェクトで試してみました。
$cat /etc/os-release | head -n2 NAME="Ubuntu" VERSION="18.04.4 LTS (Bionic Beaver)" $ go version go version go1.14.2 linux/amd64比較
# 1. ノーオプション go build -o nooption main.go # 2. ldflagsでdebug情報を削除 go build -ldflags="-s -w" -o ldflags main.go # 3. trimpathでパス情報を削除 go build -ldflags="-s -w" -trimpath -o trimpath main.go
-ldflags
- Go Documentによるとリンカーへの引数。
-s
,-w
はそれぞれシンボルテーブルとデバック情報とのこと。-trimpath
- Go1.13から追加されたオプション。バイナリから全ファイルシステムパスを削除する
- panic で表示されるパスからBuild時のディレクトリ構造を消すためにも指定
UPX
Goのバイナリサイズを削減する で初めて知ったのですが、UPXは実行形式を保ったまま、バイナリを圧縮するツールとのことです。(ライセンスは記事にかかれている通り、GPLに例外事項をつけたライセンスですので、本番環境への利用時は念のため確認してください)
基本的には、UPXはバイナリを圧縮するツールですが、実行ファイル圧縮 形式で行ってくれるらしく、元のバイナリの圧縮+自己展開のバイナリも付与されるモデルだそうです。そのため、バイナリサイズが小さくなるトレードオフとして、(どの程度かはさておき)起動時に圧縮されたバイナリを展開するため起動速度は遅くなるようです。一度起動してしまえば後は性能差は無いと思います。
UbuntuにUPXをインストールします。
インストール$sudo apt-get update -y $sudo apt-get install -y upx # Versionの確認 $upx --version | head -4 upx 3.94 UCL data compression library 1.03 zlib data compression library 1.2.11 LZMA SDK version 4.43UPXを実行してみます。
https://linux.die.net/man/1/upx によると、-1から-9まで圧縮レベルを指定できるそうです。
--best
で最善を尽くしてくれるそうです。ちなみにデフォルトは -8 だそうです。upx -1 -o upx1 trimpath upx -8 -o upx8 trimpath upx -9 -o upx9 trimpath upx --best -o upx_best trimpath
-8
より上のレベルはかなりパックに時間がかかったので、リモートサーバならともかく、ローカル環境で毎Build時にやることはオススメしないなって思いました。特に--best
は数分はかかった気がします。
--lzma
で LZMA(Lempel-Ziv-Markov chain-Algorithm) を有効にできるとのこと。LZMA利用upx --lzma -o upx_lzma trimpath
--brute
,--ultra-brute
のオプションもあるようなので試してみます。brute
はman
で確認すると..Compression tuning options:
--brute try all available compression methods & filters [slow]
--ultra-brute try even more compression variants [very slow]...とのことで、良い圧縮率が期待できそうです。
brute利用upx --brute -o brute trimpath upx --ultra-brute -o ultra_brute trimpathこれも
--best
と同様にかなりパック時間がかかりました。サイズ
No Name Size [KB] 1と比べたときのサイズ 1 nooption 22,501 100% 2 ldflags 16,996 75.5% 3 trimpath 16,968 75.4% 4 3 + UPX 1 6,622 29.4% 5 3 + UPX 8 5,635 25.0% 6 3 + UPX 9 5,564 24.7% 7 3 + UPX BEST 5,522 24.5% 8 3 + UPX LZMA 4232 18.9% 9 3 + UPX BRUTE 4218 18.7% 10 3 + UPX ULTRA BRUTE 4198 18.7% 2,3はほぼ変わらないですが、ファイルシステムパスを削除した分、多少サイズが減っています。(30KBなので小さいシステムだとインパクト大きいかもしれません)
4以降のUPXが劇的過ぎてビビるですが、試しにパックしたバイナリを実行してみるともちろん普通に起動しました。パックするにはオプションによってそこそこ処理時間がかかります。圧縮率と処理時間のバランスで、UPXを使うのであれば
--lzma
が1番バランスが良さそうだなと思いました。 BRUTEは時間がかかるので。まとめ
- ライブラリ提供でなく実行バイナリをビルドするのであれば、
-ldflags="-s -w"
や-trimpath
をつけるのがベターで、そこそこ依存関係があるPJでも25%ほどバイナリサイズを小さくすることができた- UPXの効果が素晴らしい
- 投稿日:2020-04-14T13:45:25+09:00
Go言語のdeferを正しく理解する | How defer in Golang works
deferってなんかかっこいい!
くらいの認識の人向け。環境
go version go1.13.7 darwin/amd64A Tour of Goより
A defer statement defers the execution of a function until the surrounding function returns.
The deferred call's arguments are evaluated immediately, but the function call is not executed until the surrounding function returns.
defer文は、上位ブロックの関数がreturnするまで関数の実行を遅延させる。
遅延実行される関数の引数は即時評価されるが、関数の実行は上位ブロックの関数がreturnするまで実行されない。出典: A Tour of Go
試してみる
なにはともあれコードを書く。
遅延関数の引数の即時評価について
まずは上記Tour of Goのリンク先にあるplaygroundで試してみましょう。
デフォルトだと下記のコード。
package main import "fmt" func main() { defer fmt.Println("world") fmt.Println("hello") }実行すると
hello worldお、遅延してますね。
試しに"world"
部分を変数にして、その変数を書き換えてみましょうか。package main import "fmt" func main() { world := "world" world = "world?" defer fmt.Println(world) fmt.Println("hello") }実行すると
hello world?書き換わっていますね。
では
world
の中身をdefer宣言の後に書き換えてみましょう。package main import "fmt" func main() { world := "world" defer fmt.Println(world) world = "world?" fmt.Println("hello") }実行すると
hello worldこれが
遅延実行される関数の引数は即時評価される
ということですね。
遅延実行を定義した後にその引数をいくら書き換えても意味がないようです。このおかげで意図せぬdeferの挙動を避けられそうですね。
deferの積み上げ
ではdeferを連続して宣言するとどうなるでしょうか。
こちらのplaygroundで試してみましょう。
package main import "fmt" func main() { fmt.Println("counting") for i := 0; i < 10; i++ { defer fmt.Println(i) } defer fmt.Println("last defer") // ここ追記しました fmt.Println("done") }元々はfor文内のdeferのみだったので、一応外側にもdefer追記してみました。
実行してみます。counting done last defer 9 8 7 6 5 4 3 2 1 0
まあ特にfor文など関係なく、deferが処理されたのと逆順に実行されていくようです。
後入先出法で実行されるんですね。返り値の書き換え
deferは
returnするまで実行されない
というのはちょっと曖昧です。ちょっとテストしてみましょう。
これに合致するTourは無いので、こちらででも実行してみてください。package main import ( "fmt" ) func test1() (myInt int) { myInt = 1 defer func() { myInt++ }() return myInt } func main() { fmt.Printf("test1: %d\n", test1()) }これを実行すると
test1: 2!?
キモいですね!?まあ、遅延実行される関数は、上位関数の名前付き返り値を参照したり変更したりできるということが分かりました。
こうなると
return myInt
のmyInt
はどのタイミングでインクリメントされているのか、気になりますね。上記に追記して試してみましょう。
package main import ( "fmt" ) func test1() (myInt int) { myInt = 1 defer func() { myInt++ }() return myInt } // 追記 func test2() (myInt int) { myInt = 1 defer func() { myInt++ }() return func() int { fmt.Println(myInt) return myInt }() } func main() { fmt.Printf("test1: %d\n", test1()) fmt.Printf("test2: %d\n", test2()) // 追記 }実行すると。。。
test1: 2 1 # test2関数のreturn文で定義している無名関数内のPrintで表示したmyInt test2: 2 # 実際に返ってきたmyIntうーむ、これを見る限り、
- return文の式を評価する
- 遅延関数を実行する (返り値の書き換えがあれば書き換える)
- 返り値を呼び出し元にreturnする
というふうになっているようですね。
いずれにしてもこの特徴があることにより、エラー値の書き換えなどが便利になるようです。
panic, defer, recover
別にdeferの用途がこれだけというわけでは無いのですが、panicに対処する方法としてdeferを使う、というテンプレみたいなものがあります。
これはもはやGo公式のブログそのままなのですが、
package main import "fmt" func main() { f() fmt.Println("Returned normally from f.") } func f() { defer func() { if r := recover(); r != nil { fmt.Println("Recovered in f", r) } }() fmt.Println("Calling g.") g(0) fmt.Println("Returned normally from g.") } func g(i int) { if i > 3 { fmt.Println("Panicking!") panic(fmt.Sprintf("%v", i)) } defer fmt.Println("Defer in g", i) fmt.Println("Printing in g", i) g(i + 1) }出典: The Go Blog | Defer, Panic, and Recover
main関数はfを呼び出し、fはgを呼び出します。
gは再帰的な関数になっており、受け取った引数をPrintして、引数をインクリメントした値でまた自身を呼び出します。ただしgは引数が4以上の場合panicします。
panic
ここでpanicについても理解しておかないと理解できません。
ある関数Fの中でpanicが呼び出されると、
- panicが起きた時点でそれ以降に定義されている処理は行われず
- 呼び出し元(親)に
return
する- 親では
F()
部分がpanicの呼び出しのように振る舞う- 親の処理が止まる
- さらにその上の呼び出し元(親の親)に
return
する- 親の親では親関数がpanicの呼び出しのように振る舞う
- ...以下、goroutine内の全ての関数が
return
されるまでコールスタックをさかのぼり続け、最終的にプログラムがクラッシュするという挙動になります。
さて、元のコードの話に戻ると、gがpanicを起こすと呼び出し元のfに
return
します。で、fでもpanicが引き起こされ、特に何も対策をしていなければ、そのままプログラムがクラッシュするのが分かりますね。
そこで、「panicしたら呼び出し元にreturnする」というのがミソです。
deferはreturnする前にその処理が挟まることになるので、panicで他の処理が殺されても、deferで宣言した処理だけは生き残ります。その中で
recover
組み込み関数を使ってやると、panicの流れを食い止めることができます。recover関数は、平常時に使用しても
nil
を返すのみですが、panic中に使用すると、panic()
に渡された引数が返ってきます。
なのでr != nil
の判定が入っているんですね。recoverした後の処理についてはプログラマ次第です。
死ぬ前にやりたいことをやって再度panicさせるもよし、errを返り値として呼び出し元に返すもよし、エラーをどこかに記録して何事も無かったように処理を進めるもよし。このケースではPrintしてpanicを握りつぶしてますね笑
実行結果はこのようになります。Calling g. Printing in g 0 Printing in g 1 Printing in g 2 Printing in g 3 Panicking! Defer in g 3 Defer in g 2 Defer in g 1 Defer in g 0 Recovered in f 4 Returned normally from f. # fがgで起きたpanicをrecoverしたので、呼び出し元のmain()は正常に処理を進められているTL;DR
deferの機構は分かったけど、結局実用コードを書かないとね、感。
- 投稿日:2020-04-14T11:05:09+09:00
Golang - MySQL Scan Error
Problem
sql: Scan error on column index 5, name \"time\": unsupported Scan, storing driver.Value type []uint8 into type *time.Time]"}Solution
You need added string "parseTime=true" in mysql connection setting.
db, err := sql.Open("mysql", "root:@/?parseTime=true")https://stackoverflow.com/questions/29341590/how-to-parse-time-from-database/29343013#29343013
https://stackoverflow.com/questions/45040319/unsupported-scan-storing-driver-value-type-uint8-into-type-time-time