Skip to main content

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 main package 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 .go files in the same directory must belong to the same package

import "fmt"

This imports the fmt package from the standard library.

  • fmt stands 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 func keyword declares a function
  • The main() function is the program's entry point in the main package
  • It takes no parameters and returns no values
  • Command-line arguments are accessed through os.Args

fmt.Println("Hello, World!")

  • Calls the Println function from the fmt package
  • Prints a string and automatically appends a newline (\n)
  • fmt.Print (no newline) and fmt.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:

Aspectgo rungo build
Use caseQuick testing during developmentProducing a deployable binary
SpeedFast (temporary build)Slightly slower
Binary storageNot savedSaved to the specified path
Debug symbolsIncludedIncluded 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 programs
  • import: Single, group, alias, blank, and dot import patterns and their purposes
  • go run vs go 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, and go.sum working together
  • os.Exit: Controlling exit codes

In the next chapter, we'll take a deep dive into Go's variables, types, and constants.