20200414のGoに関する記事は5件です。

いつも使っている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
  • -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%ほどバイナリサイズを小さくすることができた
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

よく使う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
  • -trimpath
    • Go1.13から追加されたオプション。バイナリから全ファイルシステムパスを削除する
    • panic で表示されるパスからBuild時のディレクトリ構造を消すためにも指定

UPX

https://upx.github.io/

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.43

UPXを実行してみます。

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 は数分はかかった気がします。

--lzmaLZMA(Lempel-Ziv-Markov chain-Algorithm) を有効にできるとのこと。

LZMA利用
upx --lzma -o upx_lzma trimpath 

--brute, --ultra-bruteのオプションもあるようなので試してみます。bruteman で確認すると..

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の効果が素晴らしい
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

よく使う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
  • -trimpath
    • Go1.13から追加されたオプション。バイナリから全ファイルシステムパスを削除する
    • panic で表示されるパスからBuild時のディレクトリ構造を消すためにも指定

UPX

https://upx.github.io/

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.43

UPXを実行してみます。

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 は数分はかかった気がします。

--lzmaLZMA(Lempel-Ziv-Markov chain-Algorithm) を有効にできるとのこと。

LZMA利用
upx --lzma -o upx_lzma trimpath 

--brute, --ultra-bruteのオプションもあるようなので試してみます。bruteman で確認すると..

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の効果が素晴らしい
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go言語のdeferを正しく理解する | How defer in Golang works

deferってなんかかっこいい!
くらいの認識の人向け。

環境

go version go1.13.7 darwin/amd64

A 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 myIntmyIntはどのタイミングでインクリメントされているのか、気になりますね。

上記に追記して試してみましょう。

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

うーむ、これを見る限り、

  1. return文の式を評価する
  2. 遅延関数を実行する (返り値の書き換えがあれば書き換える)
  3. 返り値を呼び出し元に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が呼び出されると、

  1. panicが起きた時点でそれ以降に定義されている処理は行われず
  2. 呼び出し元(親)にreturnする
  3. 親ではF()部分がpanicの呼び出しのように振る舞う
  4. 親の処理が止まる
  5. さらにその上の呼び出し元(親の親)にreturnする
  6. 親の親では親関数がpanicの呼び出しのように振る舞う
  7. ...以下、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の機構は分かったけど、結局実用コードを書かないとね、感。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む