Quick reference guide for concepts, patterns, and best practices
- Module Paths & Naming Convention
- Running Go
- Networking Fundamentals
- HTTP Architecture Components
- URL Pattern Matching
- DefaultServeMux
- HTTP Response Headers & Status Codes
- File Serving & Security
- Concurrency & Race Conditions
- Managing Configuration Settings
- Alternative Configuration Methods
- Configuration Structs with flag.StringVar()
- Logging
- Dependency Injection
- Quick Reference Checklist
A module path is the identifier for your Go project when it's downloadable and used by others.
Best Practice: Base your module path on a URL that you own.
Example: github.com/foo/bar → use as module path if hosted at https://github.com/foo/bar
- Ensure the module path is globally unique
- Avoid conflicts with existing packages
- Choose something clear and succinct
- Common pattern: Use a domain you own (e.g.,
snippetbox.alexedwards.net)
These three commands are functionally identical:
$ go run .
$ go run main.go
$ go run snippetbox.alexedwards.netAll will compile and execute your Go program.
Go allows you to specify network addresses using named ports or numeric ports.
| Type | Example | Behavior |
|---|---|---|
| Named Port | :http or :http-alt |
Go looks up port number in /etc/services file; returns error if not found |
| Numeric Port | :4000 |
Directly uses the specified port number |
When omitting the host (e.g., :4000), the server will listen on all available network interfaces on your computer.
// Listens on all network interfaces
server := http.ListenAndServe(":4000", mux)Role: Execute application logic and write HTTP responses
Responsibilities:
- Process requests
- Execute business logic
- Write HTTP response headers
- Write HTTP response bodies
Definition: Stores a mapping between URL patterns and their corresponding handlers
Key Points:
- Usually one servemux per application
- Contains all routes for your application
- Go terminology: called a "servemux" instead of "router"
Advantage of Go: You can create and manage a web server directly within your application
- No external third-party server required (unlike Nginx or Apache)
- Built-in HTTP server capabilities
Type: http.ResponseWriter parameter in handler functions
Purpose: Provides methods for assembling and sending HTTP responses to users
- Format: Do NOT end with a trailing slash
- Example:
/snippet/view,/snippet/create - Matching: Only matches when request URL path exactly matches the fixed path
- Behavior: Exact matching only
- Format: END with a trailing slash
- Example:
/,/static/ - Matching: Matches whenever the start of the request URL path matches the subtree path
- Behavior: Acts like a wildcard pattern (e.g.,
/**or/static/**) - Catch-all: The
/pattern matches any request path
| Pattern Type | Example | Matches | Does Not Match |
|---|---|---|---|
| Fixed Path | /snippet/view |
/snippet/view only |
/snippet/view/ |
| Subtree Path | /static/ |
/static/, /static/css, /static/css/style.css |
/static (no trailing slash) |
| Root Catch-all | / |
Any path | Nothing (catches all) |
DefaultServeMux is a global variable accessible to any package, including third-party packages.
Security Risk:
- Any package can register routes on the global DefaultServeMux
- Compromised third-party packages could expose malicious handlers
- Recommendation: Avoid using DefaultServeMux in production applications
Best Practice: Create your own custom servemux instance instead.
- If you don't explicitly call
w.WriteHeader(), the first call tow.Write()automatically sends 200 OK - This is the default behavior
- To send a non-200 status code, you MUST call
w.WriteHeader()before anyw.Write()call - Order matters: headers must be written before the body
w.WriteHeader(http.StatusNotFound) // Must come before w.Write()
w.Write([]byte("Not found"))Purpose: Lightweight shortcut for sending error responses
What It Does Behind the Scenes:
- Calls
w.WriteHeader()with your specified status code - Calls
w.Write()with your error message
http.Error(w, "Page not found", http.StatusNotFound)
// Equivalent to:
// w.WriteHeader(http.StatusNotFound)
// w.Write([]byte("Page not found"))http.ServeFile() does NOT automatically sanitize file paths.
Vulnerability: Directory Traversal Attacks
- Untrusted user input can be manipulated to access files outside intended directory
- Attacker could use
../to navigate to sensitive files
Mitigation: Always sanitize input with filepath.Clean()
// ❌ UNSAFE - Vulnerable to directory traversal
filePath := userInput // e.g., "../../../etc/passwd"
http.ServeFile(w, r, filePath)
// ✅ SAFE - Sanitized with filepath.Clean()
filePath := filepath.Clean(userInput)
http.ServeFile(w, r, filePath)Remember: If constructing file paths from any untrusted user input, always use filepath.Clean() first.
Important: All incoming HTTP requests are served in their own goroutine.
Implications:
- Go is blazingly fast due to concurrent request handling
- Multiple handlers can execute simultaneously
- Code in or called by handlers will likely run concurrently on busy servers
When accessing shared resources from handlers, you must be aware of and protect against race conditions.
Key Consideration: If multiple goroutines access the same data simultaneously without synchronization, data corruption or unexpected behavior can occur.
Best Practice: Use synchronization primitives (mutexes, channels) when sharing data between handlers.
Define flags using the flag package to accept command-line arguments.
addr := flag.String("addr", ":4000", "HTTP network address")
flag.Parse() // Must call to parse the flags
// Usage: $ go run ./cmd/web -addr=":80"Benefits:
- Flexible runtime configuration
- Default values provided
- Automatic
-helpflag support - Type-safe (flag.String, flag.Int, flag.Bool, etc.)
Use the -help flag to list all available command-line flags and their descriptions.
$ go run ./cmd/web -help
Usage of /tmp/go-build3672328037/b001/exe/web:
-addr string
HTTP network address (default ":4000")- Restriction: Ports 0-1023 are reserved for system services
- Access: Typically requires root/administrator privileges
- Error: If you try to use a restricted port without privileges, you'll get:
bind: permission denied
// ❌ Will fail on most systems without root
addr := flag.String("addr", ":80", "HTTP network address")
// ✅ Safe - Use ports above 1023
addr := flag.String("addr", ":4000", "HTTP network address")addr := os.Getenv("SNIPPETBOX_ADDR")
// Usage: $ SNIPPETBOX_ADDR=":80" go run ./cmd/web- ❌ No default value support (returns empty string if not set)
- ❌ No automatic
-helpfunctionality - ❌ Less convenient for users
Recommendation: Use command-line flags for better user experience.
Parse flag values directly into the memory addresses of pre-existing variables.
Useful for: Storing all configuration settings in a single struct
type config struct {
addr string
staticDir string
}
var cfg config
flag.StringVar(&cfg.addr, "addr", ":4000", "HTTP network address")
flag.StringVar(&cfg.staticDir, "static-dir", "./ui/static", "Path to static assets")
flag.Parse()
// Access configuration: cfg.addr, cfg.staticDir| Function | Type | Example |
|---|---|---|
flag.StringVar() |
string | flag.StringVar(&s, "name", "default", "help") |
flag.IntVar() |
int | flag.IntVar(&i, "port", 4000, "help") |
flag.BoolVar() |
bool | flag.BoolVar(&b, "debug", false, "help") |
- Single struct for all configuration
- Clean, organized code
- Easy to pass configuration around your application
- Type-safe configuration management
Create separate loggers for different log levels using log.New():
infoLog := log.New(os.Stdout, "INFO\t", log.Ldate|log.Ltime)
errorLog := log.New(os.Stderr, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile)
infoLog.Printf("Starting server on %s", *addr)
err := http.ListenAndServe(*addr, mux)
errorLog.Fatal(err)Parameters:
- First argument: Destination (os.Stdout or os.Stderr)
- Second argument: Prefix (e.g., "INFO\t", "ERROR\t")
- Third argument: Flags for formatting (see table below)
| Flag | Description |
|---|---|
log.Ldate |
Include date (YYYY-MM-DD) |
log.Ltime |
Include time (HH:MM:SS) |
log.Lshortfile |
Include short filename (e.g., main.go:42) |
log.Llongfile |
Include full file path (e.g., /home/user/project/main.go:42) |
log.LUTC |
Use UTC datetimes instead of local time |
Use the bitwise OR operator (|) to combine multiple flags:
log.Ldate | log.Ltime | log.Lshortfile // Date, time, and short filename| Method | Output | Use Case | Notes |
|---|---|---|---|
Print() / Printf() / Println() |
Normal message | General logging | No special behavior |
Fatal() / Fatalf() / Fatalln() |
Error message, then exit | Critical errors | Calls os.Exit(1) after logging |
Panic() / Panicf() / Panicln() |
Error message, then panic | Exceptional conditions | Panics after logging; can be recovered |
Best Practice: Avoid using Panic() and Fatal() outside of main() function. Return errors instead and only exit/panic from main.
Logging to stdout and stderr decouples your application from log storage and routing.
Advantages:
- Application doesn't need to manage log files
- Easy to redirect logs to different destinations depending on environment
- Follows Unix philosophy (small, focused tools)
Redirect stdout and stderr to files when starting the application:
$ go run ./cmd/web >>/tmp/info.log 2>>/tmp/error.logBreakdown:
>>- Append to file (create if doesn't exist)1- stdout (implicit, so>>/tmp/info.logis same as1>>/tmp/info.log)2- stderr/tmp/info.log- INFO logger output destination/tmp/error.log- ERROR logger output destination
Custom loggers created by log.New() are concurrency-safe. You can safely:
- Share a single logger across multiple goroutines
- Use the same logger in all handlers
- No need to worry about race conditions on the logger itself
// ✅ Safe to use across multiple goroutines
infoLog := log.New(os.Stdout, "INFO\t", log.Ldate|log.Ltime)
// Use in multiple handlers concurrently
handler1(w, r) { infoLog.Println("Handler 1") }
handler2(w, r) { infoLog.Println("Handler 2") }Write() method is also safe for concurrent use.
Example of Potential Issue:
// ⚠️ Risky - Multiple loggers to same file
f, _ := os.OpenFile("app.log", os.O_WRONLY|os.O_CREATE, 0666)
log1 := log.New(f, "LOG1\t", log.Ldate|log.Ltime)
log2 := log.New(f, "LOG2\t", log.Ldate|log.Ltime)
// log1 and log2 may interfere with each otherAlternative to redirecting streams at runtime - open a file directly in Go.
f, err := os.OpenFile("/tmp/info.log", os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
log.Fatal(err)
}
defer f.Close()
infoLog := log.New(f, "INFO\t", log.Ldate|log.Ltime)| Flag | Meaning |
|---|---|
os.O_RDWR |
Open file for reading and writing |
os.O_CREATE |
Create file if it doesn't exist |
0666 |
File permissions (rw-rw-rw-) |
General Best Practice: Log to standard streams and redirect at runtime (more flexible). Only log directly to files if you have specific requirements.
Most web applications need multiple dependencies that handlers access, such as:
- Database connection pools
- Centralized error handlers
- Template caches
- Logging utilities
Problem with Global Variables: Less explicit, more error-prone, harder to unit test.
Solution: Inject dependencies into handlers through structured patterns.
For applications where all handlers are in the same package, define an application struct containing all dependencies and define handler functions as methods on that struct.
type application struct {
errorLog *log.Logger
infoLog *log.Logger
}Create and initialize your application struct with all dependencies:
func main() {
addr := flag.String("addr", ":4000", "HTTP network address")
flag.Parse()
infoLog := log.New(os.Stdout, "INFO\t", log.Ldate|log.Ltime)
errorLog := log.New(os.Stderr, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile)
// Initialize application struct with dependencies
app := &application{
errorLog: errorLog,
infoLog: infoLog,
}
// Pass to server...
}Define handlers as methods on the application receiver:
func (app *application) snippetCreate(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.Header().Set("Allow", http.MethodPost)
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
return
}
// Access dependencies via app receiver
app.infoLog.Println("Creating a new snippet...")
}- ✅ Dependencies explicitly available via struct fields
- ✅ Less error-prone than global variables
- ✅ Easier to unit test (mock dependencies)
- ✅ Clean, organized code
- ✅ Type-safe
The struct method pattern doesn't work when handlers are spread across multiple packages.
Create a config package exporting an Application struct and have handler functions close over this to form a closure.
// config/application.go
package config
type Application struct {
ErrorLog *log.Logger
InfoLog *log.Logger
}Create the application and pass it to handler factory functions:
func main() {
app := &config.Application{
ErrorLog: log.New(os.Stderr, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile),
InfoLog: log.New(os.Stdout, "INFO\t", log.Ldate|log.Ltime),
}
mux := http.NewServeMux()
mux.Handle("/", examplePackage.ExampleHandler(app))
}Create a function that accepts dependencies and returns an http.HandlerFunc:
// examplePackage/handlers.go
package examplePackage
func ExampleHandler(app *config.Application) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// The handler closes over 'app', accessing dependencies
ts, err := template.ParseFiles(files...)
if err != nil {
app.ErrorLog.Println(err.Error())
http.Error(w, "Internal Server Error", 500)
return
}
// Handler logic...
}
}- Handler factory function accepts dependencies (
*config.Application) - Returns an
http.HandlerFuncclosure - The closure captures the
appvariable in its scope - When the handler runs, it has access to all dependencies
- ✅ Works across multiple packages
- ✅ Dependencies still injected (not global)
- ✅ Factory function pattern is flexible
- ✅ Each handler can have different dependencies
| Aspect | Single Package Struct | Multi-Package Closure |
|---|---|---|
| Works across packages? | ❌ No | ✅ Yes |
| Code clarity | ✅ Very clear | ✅ Clear |
| Testing | ✅ Easy | ✅ Easy |
| Setup complexity | ✅ Simple | |
| Use case | Small to medium apps | Larger projects |
- Module path matches project repository URL
- Using custom servemux, not DefaultServeMux
- Fixed paths used for exact matches (no trailing slash)
- Subtree paths used for wildcard-like matching (trailing slash)
- Response headers written before response body
- File paths sanitized with
filepath.Clean()if from user input - Named ports checked against
/etc/serviceswhen used - Aware of goroutine concurrency and potential race conditions
- Using command-line flags for configuration (not environment variables)
- Avoiding privileged ports (0-1023) unless running with root privileges
- Configuration stored in struct using flag.*Var() functions
- Using leveled logging (separate loggers for INFO and ERROR)
- Logging to stdout/stderr with runtime redirection
- Aware of logger concurrency safety and multiple logger issues
- Using dependency injection pattern (struct methods or closures)
- Dependencies passed explicitly, not via global variables