Skip to content

cmd/compile: can't override variable with ldflags when initial value is a func #64246

@sethvargo

Description

@sethvargo

(This might be better categorized under the linker, please edit as appropriate)

What version of Go are you using (go version)?

$ go version
go version go1.21.4 darwin/arm64

Does this issue reproduce with the latest release?

Go 1.21.4 is the latest version as of this posting.

What operating system and processor architecture are you using (go env)?

go env Output
$ go env
GO111MODULE='on'
GOARCH='arm64'
GOBIN=''
GOCACHE='/Users/sethvargo/Library/Caches/go-build'
GOENV='/Users/sethvargo/Library/Application Support/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFLAGS=''
GOHOSTARCH='arm64'
GOHOSTOS='darwin'
GOINSECURE=''
GOMODCACHE='/Users/sethvargo/Development/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='darwin'
GOPATH='/Users/sethvargo/Development/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/opt/homebrew/Cellar/go/1.21.4/libexec'
GOSUMDB='sum.golang.org'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/opt/homebrew/Cellar/go/1.21.4/libexec/pkg/tool/darwin_arm64'
GOVCS=''
GOVERSION='go1.21.4'
GCCGO='gccgo'
AR='ar'
CC='cc'
CXX='c++'
CGO_ENABLED='1'
GOMOD='/Users/sethvargo/Development/project/go.mod'
GOWORK=''
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
PKG_CONFIG='pkg-config'
GOGCCFLAGS='-fPIC -arch arm64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -ffile-prefix-map=/var/folders/cp/qb9vbbkx4w36f6dclng481br00gy5b/T/go-build715077767=/tmp/go-build -gno-record-gcc-switches -fno-common'

What did you do?

I tried to override a package variable (that has a default value) using ldflags, all using the same command:

go run \
  -a \
  -trimpath \
  -ldflags="-s -w -X main.Version=1.2.3. -extldflags=-static"

My expectation is that the Version value is set to "1.2.3" and therefore the program prints "1.2.3".

✅ Explicit string type

package main
import "fmt"
var Version string = "development"
func main() {
  fmt.Println(Version) // correctly prints "1.2.3"
}

✅ Implicit string type

package main
import "fmt"
var Version = "development"
func main() {
  fmt.Println(Version) // correctly prints "1.2.3"
}

✅ Explicit string type func() string

package main
import "fmt"
var Version string = func() string {
  return "development"
}()
func main() {
  fmt.Println(Version) // prints "1.2.3"
}

✅ Implicit string type func() string

package main
import "fmt"
var Version = func() string {
  return "development"
}()
func main() {
  fmt.Println(Version) // prints "1.2.3"
}

Run with -x output

❌ Explicit string type complex func() string

package main
import "fmt"
var Version string = func() string {
  if true {
    return "development"
  }
  return "production"
}()
func main() {
  fmt.Println(Version) // ❌ always prints "development"
}

❌ String concatenation with runtime variable

package main
import "fmt"
var Version string = "devel" + os.Getenv("FOO")
func main() {
  fmt.Println(Version) // ❌ always prints "devel"
}

Run with -x output

At first I thought this was because the compiler was optimizing away the function call, but surely the compiler should also optimize out the "always-true" branch.

What did you expect to see?

I expect the ldflags to override the variable.

What did you see instead?

ldflags conditionally overrides the variable, depending on other variables I don't fully understand.

What problem am I trying to solve?

I'd like to inject build information into the compiled binary, with sane fallback values if none are provided. Some of these values will be injected by the build process into the final binary, but if someone builds the binary themselves (or if they go install it), then I'd like some reasonable build information. Fortunately modern versions of Go expose this, but it seems to be incompatible with allowing the values to be overridden:

var Version string = func() string {
  if info, ok := debug.ReadBuildInfo(); ok {
    if v := info.Main.Version; v != "" {
      return v // e.g. "v0.0.1-alpha1.0.20231115..."
    }
  }

  return "source"
}()

I would like the default version to come from the debug package, but still provide a mechanism for builders to inject/override with their own values. With the function definition above, it's impossible for a builder to override Version. Since some of these functions could be called hundreds or thousands of times, I don't want to make this a pure function call. The best I could come up with was to move the package into internal and leverage a combination of private variables and once functions, but it's not pleasing:

package buildinfo

var version string
var Version string = sync.OnceValue(func() string {
  if v := version; v != "" {
    return v
  }

  if info, ok := debug.ReadBuildInfo(); ok {
    if v := info.Main.Version; v != "" {
      return v // e.g. "v0.0.1-alpha1.0.20231115..."
    }
  }

  return "source"
})()

Metadata

Metadata

Assignees

Labels

NeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.compiler/runtimeIssues related to the Go compiler and/or runtime.

Type

No type

Projects

Status

Todo

Relationships

None yet

Development

No branches or pull requests

Issue actions