JavaRush /Courses /Go SELF /Go GOOS/GOARCH Cross-Compilation — the Minimal Set

Go GOOS/GOARCH Cross-Compilation — the Minimal Set

Go SELF
Level 54, Lesson 2
Available

1. Why cross-compilation is needed

Cross-compilation sounds like “wizardry on the level of ‘build Windows on a Mac without leaving Vim’”. In practice, it’s very down-to-earth: you have one project, but users may be on different OSes and CPUs. You want to build a binary for a Linux server, for a colleague’s Windows laptop, and for a small ARM device — without rewriting code. In Go, most of this is solved by two environment variables: GOOS and GOARCH.

To avoid getting lost in terms, keep one simple idea in mind. The compiler always produces a binary “for something specific”, and that “something” is called the target. And the environment where you run go build is called the host. These words will come up often, and we’ll reinforce them with examples next.

Host vs target

When you type go build, that action happens on your machine (or in CI). That is the host: the operating system and architecture on which the build tool runs. But the build result — the binary — is intended to run on some platform. That is the target: the operating system and architecture for which the program was compiled.

A useful everyday analogy: imagine you print instructions on a printer. The printer is at your home (host), but you print the instructions for a friend who has a different device model (target). The place where you print doesn’t change the meaning of the text — you just choose in advance “which device” the instructions are for. In Go, you “choose the device” via GOOS/GOARCH.

You can visualize it like this:

flowchart LR
    A[Host: where we run go build] -->|set via GOOS/GOARCH| B[Target: what we build the binary for]
    B --> C[Binary: runs only on the target platform]

And an immediate practical consequence: if you built a binary for windows/amd64, you won’t be able to “just run it” on Linux. The binary will honestly try to be a Windows program, and Linux will honestly say “I’m not Windows” (sometimes without any drama — just dryly).

2. GOOS/GOARCH: choosing a build platform

What GOOS and GOARCH are

GOOS is the target operating system, and GOARCH is the target CPU architecture. Together they define the target platform. For example, windows/amd64 means “Windows on 64-bit x86”, and linux/arm64 means “Linux on ARM64”.

If you’ve seen these parameters not only in builds but also in Go documentation, that’s normal: even standard-library pages on pkg sites may take GOOS/GOARCH into account (for example, for conditional parts of APIs that depend on the platform). In the Go website test data, query parameters like ?GOOS=windows&GOARCH=amd64 appear explicitly.

A minimal set of platforms that most often shows up in everyday development can be kept as a small table (and you don’t need to memorize the entire zoo right away — it’s big):

GOOS
GOARCH
What it usually means Where it shows up
linux
amd64
typical server/container backend, CI
linux
arm64
ARM servers, Raspberry Pi, some clouds IoT, ARM cloud
darwin
arm64
macOS on Apple Silicon modern Macs
windows
amd64
Windows 64-bit desktop

Sometimes you’ll also run into more exotic combinations. For example, in Go news you may see platforms like WASI (GOOS=wasip1, GOARCH=wasm). But for a “minimal set”, it’s enough to live confidently within the four rows above.

How to set GOOS/GOARCH when building

Now is a good time for a simple and honest “what it looks like by hand”. GOOS and GOARCH are environment variables. That means you can set them “for a single command” without changing anything globally (and this is the safest mode for a beginner: less chance of accidentally building everything “for the wrong thing”).

On Linux/macOS, it’s usually written like this (in one line before the command):

GOOS=windows GOARCH=amd64 go build -o todo.exe ./cmd/todo

On Windows (PowerShell) the syntax is different, but the idea is the same: you set the values in the environment and run go build. We won’t dive into shell differences here — the important part is the principle: the values of GOOS/GOARCH are taken from the environment at the moment you start go build.

Why is -o appropriate here? Because we’re building one main package and expect one output file. And we immediately see a useful nuance: for Windows it’s usually convenient to add the .exe extension in the file name. This is not a “no matter what” requirement of the compiler; it’s a requirement of the environment/users’ expectations: a Windows user without .exe starts to feel uneasy and suspect they were tricked.

And yes, an important everyday fact: if you built todo.exe on macOS, it doesn’t mean you can immediately run it there by double-clicking. You built “for Windows”, and macOS is not Windows. This is not a Go error; it’s just reality.

How to find out the target from inside the binary

With cross-compilation there’s a typical problem: you quickly generated several binaries, and then you look at the folder and think, “So… what is this one for? Either I’m a genius or I just forgot to label the files.” To avoid depending on the file name (which can be renamed), it’s useful to make it so that the binary itself can say what platform it was built for.

In Go this is done via the runtime package: it contains constant-like values runtime.GOOS and runtime.GOARCH. An important nuance: this is not “the OS on which go build is currently running”. These values are baked into the binary at build time, i.e., they describe the target.

Mini example (you can run it as a standalone toy to feel the idea):

package main

import (
	"fmt"
	"runtime"
)

func main() {
	fmt.Printf("target: %s/%s\n", runtime.GOOS, runtime.GOARCH) // target: linux/amd64
}

If you build this code for windows/amd64, it will print target: windows/amd64 — even if you built it on Linux/macOS. That’s exactly what we need to diagnose artifacts.

4. Adding the target to --version

Now we’ll make a small but very practical upgrade to our training app todo. In the previous lecture, we already introduced the internal/buildinfo package, which contains var Version, var Commit, var Date, and main can print that information and exit when passed the --version flag.

Now let’s add the target platform there as well. The point is that the command:

todo --version

should show not only the version and commit, but also, for example, linux/amd64 or windows/amd64. Then anyone who receives the binary can check “what this even is” without resorting to shamanic dances.

Update internal/buildinfo: add Target()

Before entering this section, it’s important to understand the design idea: build info should live in one place so you don’t smear it across the project. That’s why we’ll also add the target to buildinfo. We’ll format the output there, too.

package buildinfo

import "runtime"

// Target returns the platform the binary was built for.
func Target() string {
	return runtime.GOOS + "/" + runtime.GOARCH
}

The code is short, almost “too simple”, but these are exactly the kinds of things that make a product convenient: the user doesn’t have to guess — you tell them everything.

Build a single version format

Now we want to assemble everything into one line so that main can be as dumb as possible: “if version was requested, print the ready-made string and exit.” This reduces the chance that you print the version one way in one place and differently elsewhere.

package buildinfo

import "fmt"

var Name = "todo"
var Version = "dev"
var Commit = "none"

func FullVersion() string {
	return fmt.Sprintf("%s %s (%s) %s", Name, Version, Commit, Target())
}

Here we used var, because Version and Commit are things we want to be able to inject via -ldflags -X (as discussed earlier). And Target() comes from runtime and is “automatically correct” for the built binary.

Wire it into main

Now for an important CLI UX point: --version should work fast and should not start the “main logic” of the application. This is especially important when the main logic may require config, files, network — anything. Version is diagnostics, not “running a business process”.

package main

import (
	"flag"
	"fmt"

	"example.com/todo/internal/buildinfo"
)

func main() {
	showVersion := flag.Bool("version", false, "print version and exit")
	flag.Parse()

	if *showVersion {
		fmt.Println(buildinfo.FullVersion()) // todo dev (none) linux/amd64
		return
	}

	fmt.Println("todo: app started")
}

Now any of your binaries can tell you what it is. This is especially convenient when you have two files: one for Linux and one for Windows, and you don’t want to guess based on the name.

5. Practice: the .exe extension, running, and expectations

Cross-compilation often breaks a beginner’s brain not because it’s hard, but because expectations don’t match reality. It feels like: “I just built it — why won’t it run?” — because you built it “for a different planet”. So it’s useful to keep three practical rules in mind right away.

Rule one: the binary name should hint at the platform. Even if you added the target to --version, it’s still easier for a person to see from the file name what they downloaded. Typically the name includes at least GOOS and GOARCH, and on Windows you add .exe.

Rule two: if you built for Windows, you need to run it on Windows. If you built for Linux, run it on Linux. It sounds obvious, but the first time your brain resists and says, “But it’s the same computer…”. No — the OS is part of the platform.

Rule three: don’t overcomplicate cross-building in advance. Yes, there are nuances (for example, cgo and external C dependencies), but in the “minimal set” we assume a regular Go app using the standard library, and cross-building for it is literally two environment variables. If someday you run into nuances, you’ll already understand the basic model and will be able to pinpoint what exactly is blocking you.

6. Common cross-compilation mistakes with GOOS/GOARCH

Error #1: mixing up host and target and expecting the “built” binary to have to run on the current machine.
A very common scenario: on macOS you built with GOOS=windows, got a file, tried to run it — and nothing. This is not a “build bug”; it’s correct behavior: you made a Windows program and you’re running it on macOS. The fix is to return to the host/target model: where you build is not the same as where you run.

Error #2: swapping GOOS and GOARCH or setting an impossible pair.
When everything is still a mess in your head, it’s easy to write something like GOOS=amd64 GOARCH=windows. Go will usually tell you quickly that you can’t do that, but you’ll waste time on “why is it complaining”. A useful habit is to keep a short table of popular combinations nearby and not try to guess.

Error #3: forgetting .exe for Windows and then being surprised “why won’t it run for the user”.
Technically, a binary can exist without an extension, but a Windows user expects .exe, and many tools/File Explorer rely on it. So if the target is Windows, it’s better to give it a clear name with .exe right away via -o. This is not a Go “whim”; it’s respect for the platform.

Error #4: not being able to check what the binary was built for, and living by the principle “I guess it’s Linux”.
Once there are multiple artifacts, guessing breaks immediately. The solution is simple: add printing of runtime.GOOS/runtime.GOARCH to --version (for example, via buildinfo.Target()), and verification becomes a single command. You stop guessing — the binary tells you everything.

Error #5: building for different platforms, but printing the same “version” without the target, and then mixing up bug reports.
If a user says, “My todo v1.2.3 crashes,” and you have no platform info, you’ll have to ask extra questions. It’s much nicer when the version looks like “todo 1.2.3 (abc123) windows/amd64”: it immediately saves time for both you and the user.

1
Task
Go SELF, level 54, lesson 2
Locked
Platform passport
Platform passport
1
Task
Go SELF, level 54, lesson 2
Locked
Diagnostic flag
Diagnostic flag
1
Task
Go SELF, level 54, lesson 2
Locked
Metadata package
Metadata package
1
Task
Go SELF, level 54, lesson 2
Locked
Cross-compilation into dist
Cross-compilation into dist
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION