Go
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions and Methods
-
Section 4: Concurrency
-
Section 5: Packages and Tooling
-
Section 6: More Standard Library
-
Section 7: Building Services
-
Section 8: Advanced Go
-
Section 9: Go in the Cloud-Native Ecosystem
-
Section 10: Data Structures and Algorithms in Go
-
Section 11: Testing and Deployment
-
Section 12: Practical Projects
-
Section 13: More Standard Library Practice
-
Section 14: More Practice Projects
-
Section 15: Design Patterns in Go
-
Section 16: Interview Practice
-
Section 17: Package fmt In Depth
-
Section 18: Package strings and strconv
-
Section 19: Package os and io
-
Section 20: Package time
-
Section 21: Package sort and container
-
Section 22: Package encoding
-
Section 23: Package net/http In Depth
-
Section 24: Package context
-
Section 25: Package regexp and bytes
-
Section 26: Package errors In Depth
-
Section 27: Package crypto and hash
-
Section 28: Package flag and log
-
Section 29: Package sync In Depth
-
Section 30: More Practice Exercises
-
Section 31: Go Modules and Workspaces In Depth
-
Section 32: Generics Deep Dive (Go 1.18+)
-
Section 33: Testing Package In Depth
-
Section 34: More Interview and Whiteboard Practice
-
Section 35: Package math and unicode
-
Section 36: Package path and filepath
-
Section 37: Package database/sql
-
Section 38: Package text/template and html/template
-
Section 39: Package archive and compress
-
Section 40: Lower-Level net Package
-
Section 41: Package runtime and reflect
-
Section 42: Package embed and io/fs
-
Section 43: Go Assembly and CGO Basics
-
Section 44: Building CLIs and TUIs
-
Section 45: Go Performance Tuning
-
Section 46: More Real-World Projects
-
Section 47: Go in Production
-
Section 48: Go Design Patterns
-
Section 49: Go Interfaces Deep Dive
-
Section 50: Final Practice and Review
79: Building and Cross-Compiling Binaries
One of the things that made me fall in love with Go early on was the binary. Unlike Python or Node, where you're constantly wrestling with pip install or npm install on your production server, Go just gives you a single, statically linked file. You drop that file onto a server, and it just works. No runtime, no dependencies, no drama.
Writing a simple system probe
To see this in action, let's build a tiny utility. I want a tool that tells me exactly what environment it's running in—specifically the OS and the architecture. It's a simple program, but it's the perfect way to prove that our cross-compiled binaries are actually targeting the right systems.
package main import ( "fmt" "runtime" ) func main() { fmt.Printf("Hello from %s/%s!\n", runtime.GOOS, runtime.GOARCH) }I'll call this
sysprobe.go. If I'm sitting at my desk on a MacBook (Apple Silicon), I can just rungo build sysprobe.go. This creates an executable namedsysprobein my current directory. When I run it, it tells medarwin/arm64. Easy.The "Exec Format Error" trap
Here is where I usually trip up when I'm moving fast. A few years ago, I was building a tool for a Linux server and I remember thinking, "I'll just build it here and SCP it over." I ran the standard
go buildand uploaded the resulting binary to my Ubuntu VPS.When I tried to run it on the server, I got this:
bash: ./sysprobe: cannot execute binary file: Exec format error.I spent five minutes questioning my sanity before remembering the obvious: I had built a macOS binary and tried to run it on Linux. The operating system had no idea how to interpret the Mach-O binary format of macOS because it was expecting an ELF binary. It's a classic mistake, but it's the exact reason why cross-compiling is a first-class citizen in Go.
Targeting Linux and Windows from a Mac
The beauty of Go is that you don't need a separate "build machine" for every OS. You just need to tell the Go toolchain which target you're aiming for using environment variables:
GOOS(the target operating system) andGOARCH(the target architecture).To fix my mistake and build for that Linux server, I'll run this in my terminal:
GOOS=linux GOARCH=amd64 go build -o sysprobe-linux sysprobe.goNotice the
-oflag there. I used it to name the outputsysprobe-linuxso I don't overwrite my local Mac version. If I were targeting a Windows machine, it's just as simple:GOOS=windows GOARCH=amd64 go build -o sysprobe.exe sysprobe.goI've personally found that if you're doing this often, it's worth throwing these into a simple Makefile or a shell script. You don't want to be typing these environment variables manually every single time you push a release.
Stripping the binary for production
One last tip: if you look at the file size of these binaries, they might feel a bit chunky. That's because Go includes debug information and a symbol table by default. For a production tool where every megabyte counts (like in a tiny Docker image), I like to strip those out using linker flags.
GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o sysprobe-linux sysprobe.goThe
-sremoves the symbol table and-wremoves the DWARF debugging information. The binary will still run perfectly, but it'll be significantly smaller. Just keep in mind that if the app crashes, your stack traces won't be as descriptive, so I only do this for final release builds.
📋 Practical Task
Build a Cross-Platform Hardware Identifier
Your task is to create a utility called hwid. This program should print the current OS, the architecture, and the number of CPUs available to the process (using the runtime.NumCPU() function).
- Write the code in a file named
hwid.go. - Build a binary targeting Linux (amd64) and name the output
hwid-linux. - Build a binary targeting Windows (amd64) and name the output
hwid.exe. - Build a final Linux (amd64) binary that is stripped of debug symbols using
-ldflags="-s -w", naming ithwid-linux-slim.
To verify your work, compare the file sizes of hwid-linux and hwid-linux-slim using your file explorer or the ls -lh command.
There are no comments for now.