CLOVER🍀

That was when it all began.

Goでのビルド時に使う、-ldflagsフラグと-Xについて調べてみた(go tool link)

これは、なにをしたくて書いたもの?

記事や書籍などで、以下のような記述を見かけます。

$ go build -ldflags '-X main.xxxx=....'

この-ldflags-Xの指定でプログラム内の値を変えているようなのですが、「変えられます」という情報以外のことを
あまり見かけないので調べてみることにしました。

環境

今回の環境は、こちらです。

$ go version
go version go1.16 linux/amd64

Go 1.16ですね。

まずは試してみる

Goのプロジェクトを作成。

$ go mod init sample
go: creating new go.mod: module sample

こんなプログラムを用意。

main.go

package main

import (
    "fmt"
)

var (
    Message = "Hello World!!"
)

func main() {
    fmt.Printf("Message = %s\n", Message)
}

ビルド。

$ go build

実行。

$ ./sample 
Message = Hello World!!

このMessage変数の値を、ビルド時に変更してみましょう。

$ go build -ldflags '-X main.Message=Wow'

確かに変わりました。

$ ./sample
Message = Wow

ソースコードは変えていないのに、ビルド時のフラグ指定だけで変わりましたね。

スペースを含めたい場合は、変数名ごとクォートで囲えばよさそうです。

$ go build -ldflags '-X "main.Message=Hello Go!!!"'
$ ./sample
Message = Hello Go!!!

で、変えられることはわかったのですが、このフラグ、-ldflags自体の意味をもう少し調べたい、と。

go buildのヘルプを見る

まずは、go buildコマンドのヘルプを見てみます。

$ go help build
usage: go build [-o output] [build flags] [packages]

〜省略〜

-ldflagsについて見てみましょう。

  -ldflags '[pattern=]arg list'
        arguments to pass on each go tool link invocation.

go tool linkの呼び出しに渡す、と書かれています。

ところで、似たような考えでgo runのヘルプも見てみましょう。

$ go help run
usage: go run [build flags] [-exec xprog] package [arguments...]

〜省略〜

For more about build flags, see 'go help build'.
For more about specifying packages, see 'go help packages'.

See also: go build.

build flagsについてはgo buildを見て、と言っています。

ということは、-ldflagsも指定できそうですね。試してみましょう。

$ go run -ldflags '-X "main.Message=Hello Go!!!"' main.go
Message = Hello Go!!!

やっぱりできましたね。なるほど。

go testでも使えそうですね。今回は、確認まではしませんが…。

$ go help test
usage: go test [build/test flags] [packages] [build/test flags & test binary flags]

〜省略〜

For more about build flags, see 'go help build'.
For more about specifying packages, see 'go help packages'.

See also: go build, go vet.

以降は簡単のため、go runコマンドで-ldflagsフラグと-Xフラグを使っていこうと思います。

ドキュメントを見る

次は、goコマンドのドキュメントを見てみましょう。

go - The Go Programming Language

The -asmflags, -gccgoflags, -gcflags, and -ldflags flags accept a space-separated list of arguments to pass to an underlying tool during the build.

フラグは、スペースで区切ったリストで渡します、と。

To embed spaces in an element in the list, surround it with either single or double quotes.

要素自体にスペースを含めたい場合は、シングルクォートまたはダブルクォートで囲ってください。

The argument list may be preceded by a package pattern and an equal sign, which restricts the use of that argument list to the building of packages matching that pattern (see 'go help packages' for a description of package patterns).

ヘルプには、このフラグはgo tool linkに渡すとも書かれていました。

というわけで、go tool linkのヘルプを見てみます。

link - The Go Programming Language

こちらに、オプションの意味が書かれていました。

-Xの意味は、以下になります。

-X importpath.name=value
Set the value of the string variable in importpath named name to value.
This is only effective if the variable is declared in the source code either uninitialized
or initialized to a constant string expression. -X will not work if the initializer makes
a function call or refers to other variables.
Note that before Go 1.5 this option took two separate arguments.

importpath内のnameで指定された変数の値を設定します。ソースコード内で宣言された"変数"(variable)に対してのみ、
効果があるそうです。

他には、たとえばgdbを使ったドキュメントについては-ldflags=-wを使うことが書かれていますが、

Debugging Go Code with GDB - The Go Programming Language

これはDWARFシンボルテーブルをバイナリに含めないフラグです。要するに、デバッグ情報を含めません、と。

-w
Omit the DWARF symbol table.

DWARFについては、こちら。

dwarf - The Go Programming Language

ちなみに、これらの説明はgo doc cmd/linkでも表示できます。

$ go doc cmd/link

go tool linkでなにも指定しないで実行すると、フラグは見れますがちょっと説明が簡易ですね。

$ go tool link
usage: link [options] main.o

〜省略〜

  -X definition
        add string value definition of the form importpath.name=value

〜省略〜

とりあえず、情報の見方はわかりました。

バリエーションを試してみる

では、いくつかバリエーションを試してみましょう。-ldflagsフラグと-Xの組み合わせを試してみたいと思います。

変数を2つ定義してみます。

main.go

package main

import (
    "fmt"
)

var (
    Message1 = "Message1"
    Message2 = "Message2"
)

func main() {
    fmt.Printf("Message1 = %s\n", Message1)
    fmt.Printf("Message2 = %s\n", Message2)
}

これを1度に変えるには…?

-ldflagsの中に、2回書けばOKですね。

$ go run -ldflags '-X main.Message1=Hello -X main.Message2=World' main.go
Message1 = Hello
Message2 = World

-ldflags自体を繰り返すのはダメなようです。

$ go run -ldflags '-X main.Message1=Hello' -ldflags '-X main.Message2=World' main.go
Message1 = Message1
Message2 = World

次は、こうしてみましょう。

main.go

package main

import (
    "fmt"
)

var (
    ExportedStringVar  = "ExportedStringVar"
    unxportedStringVar = "unxportedStringVar"
    ExportedIntVar     = 10
    unxportedIntVar    = 20
)

const (
    ExportedStringConst   = "ExportedStringConst"
    unexportedStringConst = "unexportedStringConst"
    ExportedIntConst      = 100
    unexportedIntConst     = 200
)

func main() {
    fmt.Printf(`
  ExportedStringVar = %s
  unxportedStringVar = %s
  ExportedIntVar = %d
  unxportedIntVar = %d

  ExportedStringConst = %s
  unexportedStringConst = %s
  ExportedIntConst = %d
  unxportedIntConst = %d

`,
        ExportedStringVar,
        unxportedStringVar,
        ExportedIntVar,
        unxportedIntVar,
        ExportedStringConst,
        unexportedStringConst,
        ExportedIntConst,
        unexportedIntConst,
    )
}

stringおよびintvarおよびconst、そしてそれぞれエクスポートされているもの、されていないものを用意して、変更を試みます。

var (
    ExportedStringVar  = "ExportedStringVar"
    unxportedStringVar = "unxportedStringVar"
    ExportedIntVar     = 10
    unxportedIntVar    = 20
)

const (
    ExportedStringConst   = "ExportedStringConst"
    unexportedStringConst = "unexportedStringConst"
    ExportedIntConst      = 100
    unexportedIntConst     = 200
)

最初に、varを指定してみましょう。

$ go run -ldflags '-X main.ExportedStringVar=Foo -X main.unxportedStringVar=Bar -X main.ExportedIntVar=50 -X main.unxportedIntVar=60' main.go

すると、intの部分についてはエラーになります。

# command-line-arguments
main.ExportedIntVar: cannot set with -X: not a var of type string (type.int)
main.unxportedIntVar: cannot set with -X: not a var of type string (type.int)

ドキュメントを見た時から予想はついていましたが、変更できるのはstringのみのようですね。

修正版。

$ go run -ldflags '-X main.ExportedStringVar=Foo -X main.unxportedStringVar=Bar' main.go

    ExportedStringVar = Foo
    unxportedStringVar = Bar
    ExportedIntVar = 10
    unxportedIntVar = 20

    ExportedStringConst = ExportedStringConst
    unexportedStringConst = unexportedStringConst
    ExportedIntConst = 100
    unxportedIntConst = 200

stringであれば、エクスポートの有無に関わず変更できそうです。

続いて、const。せっかく用意しましたが、先ほどの結果でintは変えられないことはわかったのでstringのみ指定します。

$ go run -ldflags '-X main.ExportedStringConst=Foo -X main.unexportedStringConst=Bar' main.go

    ExportedStringVar = ExportedStringVar
    unxportedStringVar = unxportedStringVar
    ExportedIntVar = 10
    unxportedIntVar = 20

    ExportedStringConst = ExportedStringConst
    unexportedStringConst = unexportedStringConst
    ExportedIntConst = 100
    unxportedIntConst = 200

こちらは、変更できません。

説明の時点で、「変数」と書いていましたからね。やっぱりそうですか。

Set the value of the string variable in importpath named name to value.

サブパッケージでも試してみましょう。

$ mkdir sub

sub/message.go

package sub

var (
    Message = "Hello World"
)

func GetMessage() string {
    return Message
}

mainパッケージ側は、これを呼ぶだけにします。

main.go

package main

import (
    "fmt"
    "sample/sub"
)

func main() {
    fmt.Printf("sub.GetMessage = %s\n", sub.GetMessage())
}

まずは、ふつうに実行。

$ go run main.go
sub.GetMessage = Hello World

varを変更。

$ go run -ldflags '-X sample/sub.Message=Foo' main.go 
sub.GetMessage = Foo

サブパッケージのものも、変更できましたね。

最後は、関数呼び出しの結果を使うvar

main.go

package main

import (
    "fmt"
)

var (
    Message = GetMessage()
)

func GetMessage() string {
    return "Hello World"
}

func main() {
    fmt.Printf("Message = %s\n", Message)
}

さすがに、これはムリですね。

$ go run -ldflags '-X main.Message=Foo' main.go
Message = Hello World

説明にもこう書いていましたからね。

-X will not work if the initializer makes
a function call or refers to other variables.

つまり、パッケージのトップレベルに定義されたvarであること、リテラルで指定された値であること、が条件のようですね。
覚えておきましょう。
※トップレベルでないとダメかどうかはちゃんと確認してはいませんが、まあいいでしょう…