So you’ve deployed your web application to production and it’s running on a server far, far away.

When debugging problems it’s good to know what version of the code is running.

If you’re using git, that would be sha1 of the revision used to build the program.

We can embed that version in the executable during build thanks Go linker’s -X option which allows to change any variable in the program.

In our Go program we would have:

package main var ( sha1ver string // sha1 revision used to build the program buildTime string // when the executable was built )

We can set that variable to sha1 of the git revision in our build script build.sh :

# ! / bin / bash # notice how we avoid spaces in $ now to avoid quotation hell in go build command now = $ ( date + ' % Y -% m -% d_ % T ' ) go build - ldflags "-X main.sha1ver=`git rev-parse HEAD` -X main.buildTime=$now"

On Windows we would write powershell script build.ps1 :

# notice how we avoid spaces in $ now to avoid quotation hell in go build command $ now = Get - Date - UFormat "%Y-%m-%d_%T" $ sha1 = ( git rev - parse HEAD ). Trim () go build - ldflags "-X main.sha1ver=$sha1 -X main.buildTime=$now"

Let’s deconstruct:

git rev-parse HEAD returns sha1 of the current revision, e.g. e5ce06c1f604efb1de91d515d5de865e7e164d59

returns sha1 of the current revision, e.g. -X main.sha1ver=${foo} tells Go linker to set variable sha1ver in package main to ${foo}

tells Go linker to set variable in package to -ldflags "${flags}" tells Go build tool to pass ${flags} to go linker

-version cmd-line flag to We also need an easy way to see that version. We can addcmd-line flag to print it out

var ( flgVersion bool ) func parseCmdLineFlags () { flag . BoolVar ( & flgVersion , "version" , false , "if true, print version and exit" ) flag . Parse () if flgVersion { fmt . Printf ( "Build on %s from sha1 %s

" , buildTime , sha1ver ) os . Exit ( 0 ) } }

If this is a web application, we can additionally add a debug page that would show the version. I often do it like that

func servePlainText ( w http . ResponseWriter , s string ) { w . Header (). Set ( "Content-Type" , "text/plain" ) w . Header (). Set ( "Content-Length" , strconv . Itoa ( len ( s ))) w . WriteHeader ( http . StatusOK ) w . Write ([] byte ( s )) } // /app/debug func handleDebug ( w http . ResponseWriter , r * http . Request ) { s := fmt . Sprintf ( "url: %s %s" , r . Method , r . RequestURI ) a := [] string { s } a = append ( a , "Headers:" ) for k , v := range r . Header { if len ( v ) == 0 { a = append ( a , k ) } else if len ( v ) == 1 { s = fmt . Sprintf ( " %s: %v" , k , v [ 0 ]) a = append ( a , s ) } else { a = append ( a , " " + k + ":" ) for _ , v2 := range v { a = append ( a , " " + v2 ) } } } a = append ( a , "" ) a = append ( a , fmt . Sprintf ( "ver: https://github.com/kjk/go-cookbook/commit/%s" , sha1ver )) a = append ( a , fmt . Sprintf ( "built on: %s" , buildTime )) s = strings . Join ( a , "

" ) servePlainText ( w , s ) } func makeHTTPServer () * http . Server { mux := & http . ServeMux {} mux . HandleFunc ( "/app/debug" , handleDebug ) return & http . Server { ReadTimeout : 5 * time . Second , WriteTimeout : 5 * time . Second , IdleTimeout : 120 * time . Second , Handler : mux , } } func startHTTPServer () { httpAddr := "127.0.0.1:4040" httpSrv := makeHTTPServer () httpSrv . Addr = httpAddr fmt . Printf ( "Visit http://%s/app/debug

" , httpAddr ) err := httpSrv . ListenAndServe () if err != nil { log . Fatalf ( "httpSrv.ListendAndServe() failed with %s

" , err ) } }

In addition to printing version of the code I also show HTTP headers. During debugging, the more information, the better.