This page looks best with JavaScript enabled

Cross Compilation Adventures with Go

 ·  β˜• 7 min read  ·  ✍️ Nishant
Cross Compilation Adventures Go

This post is part of the series Cross Compilation Adventures. Read the first part here to know about the premise of this series.

TLDR; I want to build cross platform CLI utility tools. Being able to cross compile from my laptop is a required.

It is quite obvious for someone reading the blog posts in this series, that the author (me) loves using a lot of CLI tools. Afterall I am going to extents of exploring programming languages that fit my usecase in the best possible manner. Many of them actually simplify my day to day dev-life in many ways.
Interestingly, many of them are built using Go lang. Notable mention of gh-cli, GitHub’s official command line tool. It is something I use quite often. It is natural that I wanted to explore Go lang for building CLI utility tool for myself.

Generally speaking about Go lang:

  • Go is a statically typed, compiled language developed by Google in 2009. It’s efficient and easy to write, making it popular for scalable apps. Go’s concise syntax allows developers to write code quickly with fewer lines than other languages, reducing binary size and improving performance.
  • Go’s strong type system and built-in support for concurrent programming make it well-suited for tasks that require heavy computational resources. Additionally, Go allows developers to cross-compile their code to run on other platforms without needing to recompile, making it ideal for building applications that can be deployed across multiple environments.

Sounds good! Let’s dive into building a very basic CLI tool.

You will build the same example as in the last post.

A good example to showcase would be to build a CLI tool that can convert from Β°C to F and vice versa. Our tool will take an input for value and the unit to be converted to, then output would be converted temprature value.

NOTE: I am using macOS (M2 Pro, Apple Silicon), so the instructions follow through using that only. However the steps should work on all platform with little tweaks.

First we need to install go-lang. Open your Terminal app and execute the command

1
brew install go

Once installed, you should have access to go compiler in your Terminal. If not restart your session or open a new Terminal window so it is loaded in the PATH. Follow through next steps

  • Create a file named run.go.

    1
    
    touch run.go
    
  • Add the below code to the run.go file and save the file.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    
    package main
    
    import (
      "fmt"
      "os"
      "strconv"
      "strings"
    )
    
    func celsiusToFahrenheit(celsius float64) float64 {
      return celsius*9/5 + 32
    }
    
    func fahrenheitToCelsius(fahrenheit float64) float64 {
      return (fahrenheit - 32) * 5 / 9
    }
    
    func main() {
      if len(os.Args) != 3 {
        fmt.Println("Usage: ./run <value> <unit_to_convert_to>")
        return
      }
    
      value, err := strconv.ParseFloat(os.Args[1], 64)
      if err != nil {
        fmt.Println("Invalid temperature value.")
        return
      }
    
      unit := strings.ToUpper(os.Args[2])
    
      var convertedTemperature float64
    
      switch unit {
      case "C":
        convertedTemperature = celsiusToFahrenheit(value)
      case "F":
        convertedTemperature = fahrenheitToCelsius(value)
      default:
        fmt.Println("Invalid unit. Please use C or F.")
        return
      }
    
      unitSymbol := "Β°F"
      if unit == "C" {
        unitSymbol = "Β°C"
      }
    
      fmt.Printf("Converted temperature: %v%s\n", convertedTemperature, unitSymbol)
    }
    

    I am not going to explain this code as it is simple and self explanatory.

    To understand and learn the language you can use Learn X in Y minutes: Go πŸš€

  • Now to compile, execute the go compiler with build argument and the run.go source file:

    1
    
    go build run.go
    

    You should now have a binary generated in the same directory with the same name as the go file i.e run

    run

    NOTE: I use dust CLI tool to list files in directory with their sizes.
    TIP: You can generate an optimized binary by passing -ldflags "-s -w" flags at the time of compilation. i.e go build -ldflags "-s -w" run.go. Result is just a smaller binary.

    run optimized

  • Time to execute our generated run binary file:

    1
    2
    
    ❯ ./run
    Usage: ./run <value> <unit_to_convert_to>
    

    Didn’t work πŸ™„, but we have a helpful message stating how to use the CLI tool 😊

    1
    2
    
    ❯ ./run 49 C                         
    Converted temperature: 120.2Β°C
    

Done! That was a super quick intro to working with Go Compiler and Go Language in less than 5 mins πŸ˜…

But we aren’t done yet. This generated binary would work on *nix systems. I mentioned earlier that I would like to have cross-(platform + compilation).

Go allows to do that easily. Since we already have *nix compatible binary i.e Linux and macOS are sorted for us. We need to cross compile to a format that Windows understands i.e exe/executable. Let’s do that next.

Since v1.5, Go comes with support for all architectures built in. That means cross compiling is as simple as setting the right $GOOS and $GOARCH environment variables before building.

NOTE: GOOS (pronounced “goose”) stands for Go Operating System, while GOARCH (pronounced “gore-ch”) stands for Go Architecture. GOOS and GOARCH are both pronounced “gore”.

To find the list of possible platforms (the values you can use for these env variables), run the below command:

1
go tool dist list

Your output might be different, but this is how it looks like on my machine:

output

TIP: To find all the Go supported architectures for a specific platform, use grep. For example to find for windows, run go tool dist list | grep windows:

output

  • This output windows/amd64 is a set of key-value pairs separated by a /
  • It is structured as operating_system/architecture which corresponds to GOOS/GOARCH i.e $GOOS=operating_system and $GOARCH=architecture
  • Based on which platform and which architecture you want to compile to, you just need to pass the correct value to GOOS and GOARCH before calling go build.

For our usecase (and an example) compiling to Windows platform, build the run.go file with GOOS=windows flag before the go build command:

1
GOOS=windows go build run.go

NOTE: You can also pass GOARCH with the architecture value, if you are compiling for a specific architecture for the platform.
For example, for windows platform and arm architecture you should run the command:
GOOS=windows GOARCH=arm go build run.go

Something to note, when building for windows os, the values of GOOS and GOARCH:

  • For 64 bit: GOOS=windows GOARCH=amd64

  • For 32 bit: GOOS=windows GOARCH=386

    You should now have a .exe binary generated in the same directory with the same name as the go file i.e run.exe

    run.exe

    TIP: ou can generate an optimized binary by passing -ldflags "-s -w" flags at the time of compilation. i.e GOOS=windows go build -ldflags "-s -w" run.go. Result is just a smaller binary.

    run.exe optimized

    NOTE: In order to run this .exe file, you need to either execute this on Windows directly or if on a *nix system then make use of Wine.

Thats it. I think Go Language pretty much does what I wanted to get out of it:

βœ… Generate cross-platform binaries
βœ… Can cross-compile to platforms
βœ… Easy syntax, so maintainable code

All check boxes ticked is good 😊

Something to note though, the generated binary’s size isn’t small. Compared to what nim-lang can generate (in KBs), the size of binary seems to be quite large (in MBs). It is comparable to what kotlin-native was able to generate.

However, Go has a very thriving ecosystem of libraries and frameworks. This is an added advantage as most of the funtionality can be leveraged from using one of the libraries. This can be helpful to quickly build the CLI tool, rather than having to build everything from sratch.

BONUS: While my requirement isn’t about compiling to other platforms, but Go lang is quite capable to compile for many platforms such as compiling for Android, iOS, JavaScript, FreeBSD, etc. You can just pass a different value to GOOS and GOARCH before the go build command.

I’ll be trying this approach of evaluating more languages in the future. You can find the code for this post here.

Share on
Support the author with

Nishant Srivastava
WRITTEN BY
Nishant
πŸ‘¨β€πŸ’» Android Engineer/🧒 Opensource enthusiast

What's on this Page