Hello, World! — Your First Go Program
With your Go development environment ready, it's time to write your first program. We'll go beyond a simple Hello World output and take a deep dive into Go program structure, the package system, build methods, and advanced import patterns.
Hello, World! — Full Code Breakdown
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
Just 5 lines of code, yet every core concept of Go is represented. Let's analyze it line by line.
package main
Every Go file must begin with a package declaration.
- Go programs are organized into packages
- The
mainpackage is special — it tells the Go compiler this is the entry point of an executable program - Library packages use other names, like
package mylib - All
.gofiles in the same directory must belong to the same package
import "fmt"
This imports the fmt package from the standard library.
fmtstands for "format" — it handles formatting and I/O- Go treats unused imports as compile errors— a strict rule that prevents unnecessary dependencies
- Import paths are expressed as string literals
func main()
- The
funckeyword declares a function - The
main()function is the program's entry point in themainpackage - It takes no parameters and returns no values
- Command-line arguments are accessed through
os.Args
fmt.Println("Hello, World!")
- Calls the
Printlnfunction from thefmtpackage - Prints a string and automatically appends a newline (
\n) fmt.Print(no newline) andfmt.Printf(formatted output) are also frequently used
Creating and Running the File
# Create a project directory
mkdir hello-go && cd hello-go
go mod init github.com/username/hello-go
# Create the file
cat > main.go << 'EOF'
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
fmt.Printf("Formatted output: %s\n", "Go 1.24")
fmt.Print("No newline here")
fmt.Print(" — continued on the same line\n")
}
EOF
go run vs go build
go run— compile and execute in one step (for quick testing during development):
go run main.go
# Output:
# Hello, World!
# Formatted output: Go 1.24
# No newline here — continued on the same line
go run creates a binary in a temporary directory, executes it, and then deletes it.
go build— produces a binary file:
# Build a binary in the current directory (default name: module name or directory name)
go build
# Specify the output file name
go build -o hello ./...
# Build and run
./hello
Summary of differences:
| Aspect | go run | go build |
|---|---|---|
| Use case | Quick testing during development | Producing a deployable binary |
| Speed | Fast (temporary build) | Slightly slower |
| Binary storage | Not saved | Saved to the specified path |
| Debug symbols | Included | Included by default (removable with -ldflags="-s -w") |
Cross-compilation
One of Go's most powerful features. Build binaries for a different OS or architecture from your current machine — with a single command.
# Build for Linux AMD64 (works from macOS or Windows too)
GOOS=linux GOARCH=amd64 go build -o hello-linux ./...
# Build for Windows 64-bit
GOOS=windows GOARCH=amd64 go build -o hello.exe ./...
# Build for macOS Apple Silicon
GOOS=darwin GOARCH=arm64 go build -o hello-mac-arm ./...
# Embed version information
go build -ldflags="-X main.version=1.0.0" -o hello ./...
Check all supported GOOS/GOARCH combinations:
go tool dist list
Advanced Import Patterns
Single vs Group Imports
// Single imports
import "fmt"
import "os"
import "strings"
// Group import (preferred — goimports sorts these automatically)
import (
"fmt"
"os"
"strings"
)
Go community convention groups imports in this order:
import (
// 1. Standard library
"fmt"
"os"
"strings"
// 2. External packages (separated by a blank line)
"github.com/gin-gonic/gin"
"go.uber.org/zap"
// 3. Internal packages (separated by a blank line)
"github.com/username/myapp/internal/config"
"github.com/username/myapp/pkg/database"
)
Alias Import
Use aliases when package names conflict or are too long:
package main
import (
"fmt"
fm "fmt" // alias: use as fm (not actually recommended in practice)
)
func main() {
fmt.Println("Using fmt directly")
fm.Println("Using the alias fm")
}
A genuinely useful case for aliases:
import (
"crypto/rand"
mrand "math/rand" // both packages are named "rand"
)
// Now distinguishable by rand and mrand
bytes := make([]byte, 16)
rand.Read(bytes) // crypto/rand
n := mrand.Intn(100) // math/rand
Blank Import (_)
Use this when you need a package's init() function to run, but don't directly use the package name. Commonly used for driver registration:
import (
"database/sql"
_ "github.com/lib/pq" // register PostgreSQL driver
_ "github.com/go-sql-driver/mysql" // register MySQL driver
)
func main() {
db, err := sql.Open("postgres", "user=postgres dbname=mydb sslmode=disable")
// The pq driver is registered even though we never reference it directly
_ = db
_ = err
}
Dot Import (.) — Avoid This
import . "fmt" // brings all exported names from fmt into the current scope
func main() {
Println("No fmt. prefix needed") // instead of fmt.Println
}
Dot imports harm readability because it's impossible to tell at a glance which package a name comes from. The rule is: never use dot imports outside of test code.
Multi-File Packages
In real projects, a single package is split across multiple files.
Directory structure:
hello-go/
go.mod
main.go
greet.go
greet.go:
package main
import "fmt"
// Greet takes a name and prints a greeting.
// Exported functions start with an uppercase letter.
func Greet(name string) {
fmt.Printf("Hello, %s!\n", name)
}
// greetLowercase is an unexported function — only accessible within this package.
func greetLowercase(name string) {
fmt.Printf("hi, %s\n", name)
}
main.go:
package main
import "fmt"
func main() {
fmt.Println("=== Greeting Program ===")
Greet("Go Learner") // calls the function defined in greet.go
greetLowercase("internal") // accessible because it's in the same package
}
# Compile and run both files
go run main.go greet.go
# Or run the entire package (preferred)
go run .
Output:
=== Greeting Program ===
Hello, Go Learner!
hi, internal
Go's visibility rule:
- Uppercase first letter: accessible from outside the package (exported)
- Lowercase first letter: only accessible within the package (unexported)
The Role of go.mod and go.sum
hello-go/
go.mod ← module definition + dependency list
go.sum ← dependency checksums (security verification)
go.mod: The project's identity and dependency declarations
module github.com/username/hello-go
go 1.24
require (
github.com/fatih/color v1.16.0
)
go.sum: SHA-256 hashes for each dependency (prevents tampering)
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj/K049bybjyNeximgwfLJ+3X4MoO5Bi+5I=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
Using External Packages
Let's build a colorful output example using the github.com/fatih/color package.
# Add the package
go get github.com/fatih/color@v1.16.0
main.go:
package main
import (
"fmt"
"github.com/fatih/color"
)
func main() {
// Basic color output
color.Red("Error: file not found")
color.Green("Success: server started")
color.Yellow("Warning: low disk space")
color.Cyan("Info: current version is 1.24")
color.Blue("Debug: attempting connection...")
// Custom styles
bold := color.New(color.Bold)
bold.Println("Bold text")
boldRed := color.New(color.FgRed, color.Bold)
boldRed.Printf("Critical error: %s\n", "database connection failed")
// Mix with fmt
fmt.Println(color.GreenString("✓ Test passed"))
}
# Clean up dependencies and run
go mod tidy
go run .
Exit Codes
When main() returns normally, a Go program exits with code 0. Use os.Exit to signal abnormal termination:
package main
import (
"fmt"
"os"
)
func main() {
args := os.Args // os.Args[0] is the program name, [1] onward are arguments
if len(args) < 2 {
fmt.Fprintln(os.Stderr, "Usage: hello <name>")
os.Exit(1) // non-zero exit code signals failure
}
name := args[1]
fmt.Printf("Hello, %s!\n", name)
// os.Exit(0) — normal exit (can be omitted)
}
go run main.go
# Output: Usage: hello <name>
# Exit code: 1
go run main.go "Gopher"
# Output: Hello, Gopher!
# Exit code: 0
# Check the exit code
echo $?
Note: os.Exit does not run defer-ed functions. If you have cleanup logic registered with defer, make sure it's handled before calling os.Exit.
Practical Example: A Simple HTTP Server
You can build an HTTP server using only the standard library:
package main
import (
"fmt"
"log"
"net/http"
"time"
)
func helloHandler(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
if name == "" {
name = "World"
}
fmt.Fprintf(w, "Hello, %s! Current time: %s\n", name, time.Now().Format("2006-01-02 15:04:05"))
}
func main() {
http.HandleFunc("/hello", helloHandler)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Welcome to the Go HTTP server!")
fmt.Fprintln(w, "Usage: /hello?name=YourName")
})
port := ":8080"
log.Printf("Server starting: http://localhost%s\n", port)
if err := http.ListenAndServe(port, nil); err != nil {
log.Fatal("Server error:", err)
}
}
go run main.go
# Open http://localhost:8080/hello?name=Gopher in your browser
The fact that you can write a server like this without any external packages is a testament to the power of Go's standard library.
Summary
What we covered in this section:
package main: The entry-point package for executable programsimport: Single, group, alias, blank, and dot import patterns and their purposesgo runvsgo build: Development testing vs producing a deployable binary- Cross-compilation: Building for other platforms using
GOOS/GOARCH - Multi-file packages: The uppercase/lowercase rule for exported vs unexported identifiers
- Using external packages:
go get,go.mod, andgo.sumworking together os.Exit: Controlling exit codes
In the next chapter, we'll take a deep dive into Go's variables, types, and constants.