The Go programming language comes with a rich toolchain that makes obtaining packages and building executables incredibly easy. One of Go’s most powerful features is the ability to cross-build executables for any Go-supported foreign platform. This makes testing and package distribution much easier, because you don’t need to have access to a specific platform in order to distribute your package for it.
In this tutorial, you’ll use Go’s tools to obtain a package from version control and automatically install its executable. Then you’ll manually build and install the executable so you can be familiar with the process. Then you’ll build an executable for a different architecture, and automate the build process to create executables for multiple platforms. When you’re done, you’ll know how to build executables for Windows and macOS, as well as other platforms you want to support.
To follow this tutorial, you will need:
Now that Go is installed, you can try creating your Hello, World!
.
First, create a new directory for your Go workspace, which is where Go will build its files:
- mkdir hello
Then move into the directory you just created:
- cd hello
When importing packages, you have to manage the dependencies through the code’s own module. You can do this by creating a go.mod
file with the go mod init
command:
- go mod init hello
Next, create a Hello, World!
Go file in your preferred text editor:
- nano hello.go
Add the following text into your hello.go
file:
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
Then, save and close the file by pressing CTRL+X
, then Y
, and then ENTER
.
Test your code to check that it prints the Hello, World!
greeting:
- go run .
OutputHello, World!
The go run
command compiles and runs the Go package from a list of .go
source files from the new hello
directory you created and the path you imported. But, you can also use go build
to make an executable file that can save you some time.
The go run
command ran the code for your “Hello, World!” program, but you may want to build your program into a binary to run anywhere on your system, not just from the source. The go build
command builds executables.
- go build hello
As before, no output indicates successful operation. The executable will be generated in your current directory, with the same name as the directory containing the package. In this case, the executable will be named hello
.
If you’re located in the package directory, you can omit the path to the package and simply run go build
.
To specify a different name or location for the executable, use the -o
flag. Let’s build an executable called hello
and place it in a build
directory within the current working directory:
- go build -o build/hello hello
This command creates the executable, and also creates the ./build
directory if it doesn’t exist.
Now let’s look at installing executables.
Building an executable creates the executable file in the current directory or the directory of your choice. Installing an executable is the process of creating an executable and storing it in $GOPATH/bin
. The go install
command works just like go build
, but go install
takes care of placing the output file in the right place for you.
To install an executable, use go install
, followed by the package import path. Once again, use your “Hello, World!” program to try this out:
- go install hello
Just like with go build
, you’ll see no output if the command was successful. And like before, the executable is created with the same name as the directory containing the package. But this time, the executable is stored in $GOPATH/bin
. If $GOPATH/bin
is part of your $PATH
environment variable, the executable will be available from anywhere on your operating system. You can verify its location using which
command:
- which hello
You’ll see the following output:
Output of which/home/sammy/go/bin/hello
Now that you understand how go get
, go build
, and go install
work, and how they’re related, let’s explore one of the most popular Go features: creating executables for other target platforms.
The go build
command lets you build an executable file for any Go-supported target platform, on your platform. This means you can test, release and distribute your application without building those executables on the target platforms you wish to use.
Cross-compiling works by setting required environment variables that specify the target operating system and architecture. We use the variable GOOS
for the target operating system, and GOARCH
for the target architecture. To build an executable, the command would take this form:
- GOOS=target-OS GOARCH=target-architecture go build package-import-path
The OS and architecture are set before running the go build
command. This lets you use environment variables for the current command execution only. The variables are unset or reset after the command executes.
In order to find out what operating systems and platforms are available for building executables, you can use the dist
tool:
- go tool dist list
This will provide you a list of operating systems and architectures separated by /
characters:
Outputaix/ppc64
android/386
android/amd64
android/arm
android/arm64
darwin/amd64
darwin/arm64
dragonfly/amd64
freebsd/386
freebsd/amd64
freebsd/arm
freebsd/arm64
illumos/amd64
ios/amd64
ios/arm64
js/wasm
linux/386
linux/amd64
linux/arm
linux/arm64
linux/mips
linux/mips64
linux/mips64le
linux/mipsle
linux/ppc64
linux/ppc64le
linux/riscv64
linux/s390x
netbsd/386
netbsd/amd64
netbsd/arm
netbsd/arm64
openbsd/386
openbsd/amd64
openbsd/arm
openbsd/arm64
openbsd/mips64
plan9/386
plan9/amd64
plan9/arm
solaris/amd64
windows/386
windows/amd64
windows/arm
Warning: Cross-compiling executables for Android requires the Android NDK, and some additional setup which is beyond the scope of this tutorial.
Using the values in the list, we can build hello for Windows 64-bit like this:
- GOOS=windows GOARCH=amd64 go build hello
Once again, no output indicates that the operation was successful. The executable will be created in the current directory, using the package name as its name. However, since we built this executable for Windows, the name ends with the suffix .exe
.
You should have a hello.exe
file in your current directory, which you can verify with the ls
command.
- ls hello.exe
You’ll see the hello.exe
file listed in the output:
Outputhello.exe
Note: You can use the -o
flag to rename the executable or place it in a different location. However, when building an executable for Windows and providing a different name, be sure to explicitly specify the .exe
suffix when setting the executable’s name.
Let’s look at scripting this process to make it easier to release software for multiple target environments.
The process of creating executables for many platforms can be a little tedious, but we can create a script to make things easier.
The script will take the package import path as an argument, iterate through a predefined list of operating system and platform pairs, and generate an executable for each pair, placing the output in the current directory. Each executable will be named with the package name, followed by the target platform and architecture, in the form package-OS-architecture
. This will be a universal script that you can use on any project.
Within your project’s directory, create a new file called go-executable-build.bash
in your text editor:
- nano go-executable-build.bash
We will start our script with a shebang line. This line defines which interpreter will parse this script when it’s run as an executable. Add the following line to specify that bash
should execute this script:
#!/usr/bin/env bash
We want to take the package import path as a command line argument. To do this, we will use the $n
variable, where n
is a non-negative number. The variable $0
contains the name of the script you executed, while $1
and greater will contain user-provided arguments. Add this line to the script, which will take the first argument from the command line and store it in a variable called package
:
...
package=$1
Next, make sure that the user provided this value. If the value isn’t provided, exit the script with a message explaining how to use the script:
...
if [[ -z "$package" ]]; then
echo "usage: $0 <package-name>"
exit 1
fi
This if
statement checks the value of the $package
variable. If it’s not set, we use echo
to print the correct usage, and then terminate the script using exit
. exit
takes a return value as an argument, which should be 0
for successful executions and any non-zero value for unsuccessful executions. We use 1
here since the script wasn’t successful.
Note: If you want to make this script work with a predefined package, change the package
variable to point to that import path:
...
package="github.com/user/hello"
Next, we want to extract the package name from the path. The package import path is delimited by /
characters, with the package name located at the end of the path. First, we will split the package import path into an array, using the /
as the delimiter:
package_split=(${package//\// })
The package name should be the last element of this new $package_split
array. In Bash, you can use a negative array index to access an array from the end instead of the beginning. Add this line to grab the package name from the array and store it in a variable called package_name
:
...
package_name=${package_split[-1]}
Now, you’ll need to decide what platforms and architectures you want to build executables for. In this tutorial, we’ll build executables for Windows 64-bit, Windows 32-bit, and 64-bit macOS. We will put these targets in an array with the format OS/Platform
, so we can split each pair into GOOS
and GOARCH
variables using the same method we used to extract the package name from the path. Add the platforms to the script:
...
platforms=("windows/amd64" "windows/386" "darwin/amd64")
Next, we’ll iterate through the array of platforms, split each platform entry into values for the GOOS
and GOARCH
environment variables, and use those to build the executable. We can do that with the following for
loop:
...
for platform in "${platforms[@]}"
do
...
done
The platform
variable will contain an entry from the platforms
array in each iteration. We need to split platform
into two variables - GOOS
and GOARCH
. Add the following lines to the for
loop:
for platform in "${platforms[@]}"
do
platform_split=(${platform//\// })
GOOS=${platform_split[0]}
GOARCH=${platform_split[1]}
done
Next, we will generate the executable’s name by combining the package name with the OS and architecture. When we’re building for Windows, we also need to add the .exe
suffix to the filename. Add this code to the for
loop:
for platform in "${platforms[@]}"
do
platform_split=(${platform//\// })
GOOS=${platform_split[0]}
GOARCH=${platform_split[1]}
output_name=$package_name'-'$GOOS'-'$GOARCH
if [ $GOOS = "windows" ]; then
output_name+='.exe'
fi
done
With the variables set, we use go build
to create the executable. Add this line to the body of the for
loop, right above the done
keyword:
...
if [ $GOOS = "windows" ]; then
output_name+='.exe'
fi
env GOOS=$GOOS GOARCH=$GOARCH go build -o $output_name $package
done
Finally, we should check to see if there were errors building the executable. For example, we might run into an error if we try to build a package we don’t have sources for. We can check the go build
command’s return code for a non-zero value. The variable $?
contains the return code from a previous command’s execution. If go build
returns anything other than 0
, there is a problem, and we’ll want to exit the script. Add this code to the for
loop, after the go build
command and above the done
keyword.
...
env GOOS=$GOOS GOARCH=$GOARCH go build -o $output_name $package
if [ $? -ne 0 ]; then
echo 'An error has occurred! Aborting the script execution...'
exit 1
fi
With that, we now have a script that will build multiple executables from our Go package. Here’s the completed script:
#!/usr/bin/env bash
package=$1
if [[ -z "$package" ]]; then
echo "usage: $0 <package-name>"
exit 1
fi
package_split=(${package//\// })
package_name=${package_split[-1]}
platforms=("windows/amd64" "windows/386" "darwin/amd64")
for platform in "${platforms[@]}"
do
platform_split=(${platform//\// })
GOOS=${platform_split[0]}
GOARCH=${platform_split[1]}
output_name=$package_name'-'$GOOS'-'$GOARCH
if [ $GOOS = "windows" ]; then
output_name+='.exe'
fi
env GOOS=$GOOS GOARCH=$GOARCH go build -o $output_name $package
if [ $? -ne 0 ]; then
echo 'An error has occurred! Aborting the script execution...'
exit 1
fi
done
Verify that your file matches the preceding code. Then save the file and exit the editor.
Before we can use the script, we have to make it executable with the chmod
command:
- chmod +x go-executable-build.bash
Finally, test the script by building executables for hello:
- ./go-executable-build.bash hello
If everything goes well, you should have executables in your current directory. No output indicates successful script execution. You can verify are executables created using ls
command:
- ls hello*
You should see all three versions:
Example ls outputhello-darwin-amd64 hello-windows-386.exe hello-windows-amd64.exe
To change the target platforms, just change the platforms
variable in your script.
In this tutorial, you’ve learned how to use Go’s tooling to obtain packages from version control systems, as well as build and cross-compile executables for different platforms.
You also created a script that you can use to cross-compile a single package for many platforms.
To make sure your application works correctly, you can take a look at testing and continuous integration like Travis-CI and AppVeyor for testing on Windows.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!