Fast, concurrent, compiled — 8 lessons covering variables, concurrency, error handling, file I/O, testing, CLI and API development.
Go is statically typed with type inference. Use := for short variable declarations and var for explicit typing.
package main
import "fmt"
func main() {
// Short declaration with type inference
name := "Mayank"
age := 15
score := 98.5
active := true
// Explicit var declaration
var language string = "Go"
var version int = 22
// Multiple declarations
var (
x int = 10
y float64 = 3.14
z bool = true
)
fmt.Println(name, age, score, active)
fmt.Println(language, version)
fmt.Println(x, y, z)
}
// Integers: int, int8, int16, int32, int64
// Unsigned: uint, uint8, uint16, uint32, uint64
// Floats: float32, float64
// Other: bool, string, byte (uint8), rune (int32)
func main() {
// Zero values
var i int // 0
var f float64 // 0
var s string // ""
var b bool // false
// Constants
const Pi = 3.14159
const (
StatusOK = 200
StatusError = 500
)
// Type conversion
x := 42
y := float64(x)
z := string(rune(x)) // converts int to Unicode char
fmt.Println(i, f, s, b)
fmt.Println(Pi, StatusOK, StatusError)
fmt.Println(y, z)
}
// Basic function
func add(a int, b int) int {
return a + b
}
// Multiple return values
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero")
}
return a / b, nil
}
// Named return values
func swap(a, b string) (first, second string) {
first = b
second = a
return // naked return
}
// Variadic functions
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
// Functions as values
func apply(f func(int, int) int, a, b int) int {
return f(a, b)
}
func main() {
fmt.Println(add(3, 5))
result, err := divide(10, 3)
fmt.Println(result, err)
x, y := swap("hello", "world")
fmt.Println(x, y)
fmt.Println(sum(1, 2, 3, 4, 5))
fmt.Println(apply(add, 10, 20))
}
package main
import (
"fmt"
"math"
"strings"
)
func main() {
// Using math package
fmt.Println(math.Sqrt(144)) // 12
fmt.Println(math.Pi)
// Using strings package
s := "Hello, Gophers!"
fmt.Println(strings.ToUpper(s))
fmt.Println(strings.Contains(s, "Go"))
fmt.Println(strings.Split("a,b,c", ","))
// Blank identifier for side effects
// _ "net/http/pprof" // registers pprof handlers
}
func main() {
// Print functions
fmt.Print("no newline")
fmt.Println("with newline")
fmt.Printf("formatted: %s is %d years old\n", "Mayank", 15)
// Format verbs
// %s string
// %d integer
// %f float
// %v default format
// %+v struct with field names
// %#v Go-syntax representation
// %T type of value
type Person struct {
Name string
Age int
}
p := Person{"Mayank", 15}
fmt.Printf("%v\n", p)
fmt.Printf("%+v\n", p)
fmt.Printf("%#v\n", p)
fmt.Printf("Type: %T\n", p)
}
_ to explicitly discard imported names.string in Go?Structs are Go's primary composite type. They hold named fields and support methods.
package main
import "fmt"
type User struct {
Name string
Email string
Age int
IsActive bool
}
// Constructor pattern (Go convention)
func NewUser(name, email string, age int) *User {
return &User{
Name: name,
Email: email,
Age: age,
IsActive: true,
}
}
// Method with value receiver
func (u User) Describe() string {
return fmt.Sprintf("%s (%d) - %s", u.Name, u.Age, u.Email)
}
// Method with pointer receiver (can modify struct)
func (u *User) Deactivate() {
u.IsActive = false
}
func main() {
// Named fields
user := User{Name: "Mayank", Email: "may@dev.com", Age: 15}
fmt.Println(user.Name)
// Constructor
admin := NewUser("Admin", "admin@dev.com", 30)
fmt.Println(admin.Describe())
admin.Deactivate()
fmt.Println(admin.IsActive) // false
// Anonymous struct
point := struct{ X, Y int }{3, 7}
fmt.Println(point)
}
type Address struct {
City string
Country string
}
type Employee struct {
User // embedded struct — promotes methods
Address // embedded struct
Role string
}
func main() {
emp := Employee{
User: User{Name: "Mayank", Age: 15},
Address: Address{City: "Nagpur", Country: "India"},
Role: "Developer",
}
// Promoted fields — accessed as if they belong to Employee
fmt.Println(emp.Name) // from User
fmt.Println(emp.City) // from Address
fmt.Println(emp.Describe()) // from User
// Explicit access
fmt.Println(emp.User.Name)
fmt.Println(emp.Address.City)
}
// Interface definition — collection of method signatures
type Shape interface {
Area() float64
Perimeter() float64
}
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
func (c Circle) Perimeter() float64 {
return 2 * math.Pi * c.Radius
}
type Rectangle struct {
Width, Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}
// Accepts any type that implements Shape (implicit satisfaction)
func PrintShapeInfo(s Shape) {
fmt.Printf("Area: %.2f, Perimeter: %.2f\n", s.Area(), s.Perimeter())
}
// Empty interface — holds any type
func PrintAny(v interface{}) {
fmt.Printf("Value: %v, Type: %T\n", v, v)
}
// Type assertion
func DescribeCircle(s Shape) {
if c, ok := s.(Circle); ok {
fmt.Printf("Circle with radius: %.2f\n", c.Radius)
} else {
fmt.Println("Not a circle")
}
}
type Writer interface {
Write([]byte) (int, error)
}
type ConsoleWriter struct{}
func (cw ConsoleWriter) Write(data []byte) (int, error) {
fmt.Print(string(data))
return len(data), nil
}
// Multiple interfaces satisfied by one type
type ReadWriter interface {
Reader
Writer
}
// io.Reader and io.Writer are core Go interfaces
// Many types satisfy them implicitly
func main() {
var w Writer = ConsoleWriter{}
w.Write([]byte("Hello Go!\n"))
// Interface slice
shapes := []Shape{
Circle{Radius: 5},
Rectangle{Width: 4, Height: 6},
}
for _, s := range shapes {
PrintShapeInfo(s)
}
}
implements keyword needed. This enables "accept interfaces, return structs" as a key Go idiom. Small interfaces (1-2 methods) are preferred.Goroutines are lightweight threads managed by the Go runtime. Launch thousands without worry.
package main
import (
"fmt"
"time"
)
func printNumbers(prefix string) {
for i := 1; i <= 5; i++ {
fmt.Printf("%s: %d\n", prefix, i)
time.Sleep(100 * time.Millisecond)
}
}
func main() {
// Launch goroutines
go printNumbers("A")
go printNumbers("B")
// Wait for goroutines to finish
time.Sleep(1 * time.Second)
fmt.Println("Done")
}
// Channels are typed conduits for goroutine communication
func producer(ch chan int) {
for i := 0; i < 10; i++ {
ch <- i // send value
}
close(ch) // close when done
}
func consumer(ch chan int) {
for val := range ch { // receive until closed
fmt.Printf("Received: %d\n", val)
}
}
func main() {
ch := make(chan int) // unbuffered channel
go producer(ch)
consumer(ch)
}
// Buffered channels
func bufferedExample() {
ch := make(chan string, 3) // buffer of 3
ch <- "one"
ch <- "two"
ch <- "three"
// ch <- "four" // blocks here — buffer full
fmt.Println(<-ch) // "one"
fmt.Println(<-ch) // "two"
fmt.Println(<-ch) // "three"
}
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "from channel 1"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "from channel 2"
}()
// Select waits for multiple channel operations
for i := 0; i < 2; i++ {
select {
case msg := <-ch1:
fmt.Println(msg)
case msg := <-ch2:
fmt.Println(msg)
case <-time.After(3 * time.Second):
fmt.Println("timeout")
}
}
}
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Printf("Worker %d done\n", id)
}(i)
}
wg.Wait() // blocks until all goroutines finish
fmt.Println("All workers done")
}
// Mutex for shared state
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
func (c *SafeCounter) Get() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
// sync.Once — execute exactly once
var once sync.Once
func initialize() {
once.Do(func() {
fmt.Println("Initialized once")
})
}
func fanOut(input <-chan int, workers int) []<-chan int {
channels := make([]<-chan int, workers)
for i := 0; i < workers; i++ {
channels[i] = process(input)
}
return channels
}
func fanIn(channels ...<-chan int) <-chan int {
var wg sync.WaitGroup
merged := make(chan int)
for _, ch := range channels {
wg.Add(1)
go func(c <-chan int) {
defer wg.Done()
for val := range c {
merged <- val
}
}(ch)
}
go func() {
wg.Wait()
close(merged)
}()
return merged
}
go vet to detect race conditions.package main
import (
"errors"
"fmt"
)
// errors.New creates simple error values
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
func main() {
result, err := divide(10, 0)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Result:", result)
}
// Custom error with context
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation error on field '%s': %s", e.Field, e.Message)
}
// Sentinel errors
var (
ErrNotFound = errors.New("not found")
ErrUnauthorized = errors.New("unauthorized")
ErrForbidden = errors.New("forbidden")
)
func findUser(id int) (string, error) {
if id == 0 {
return "", ErrNotFound
}
return "Mayank", nil
}
func main() {
_, err := findUser(0)
if errors.Is(err, ErrNotFound) {
fmt.Println("User not found")
}
// Custom error
err = &ValidationError{Field: "email", Message: "required"}
fmt.Println(err)
}
import "fmt"
func readConfig(path string) error {
file, err := os.Open(path)
if err != nil {
// %w wraps the error — preserves the original chain
return fmt.Errorf("reading config: %w", err)
}
defer file.Close()
// parse config...
return nil
}
func main() {
err := readConfig("/nonexistent/config.yaml")
if err != nil {
fmt.Println(err)
// output: reading config: open /nonexistent/config.yaml: no such file or directory
// Check wrapped errors
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Println("Path:", pathErr.Path)
}
// Check for specific error in chain
if errors.Is(err, os.ErrNotExist) {
fmt.Println("Config file not found")
}
}
}
// Pattern 1: Check and return
func process() error {
data, err := fetchData()
if err != nil {
return fmt.Errorf("process: %w", err)
}
result, err := transform(data)
if err != nil {
return fmt.Errorf("process: %w", err)
}
return save(result)
}
// Pattern 2: defer for cleanup
func readFile(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // guaranteed cleanup
// process file...
return nil
}
// Pattern 3: Custom error checking
func IsNotFoundError(err error) bool {
return errors.Is(err, ErrNotFound)
}
// Pattern 4: Error aggregation
type MultiError struct {
Errors []error
}
func (e *MultiError) Error() string {
msgs := make([]string, len(e.Errors))
for i, err := range e.Errors {
msgs[i] = err.Error()
}
return strings.Join(msgs, "; ")
}
func (e *MultiError) Unwrap() []error {
return e.Errors
}
if err != nil { return err }. Always return errors with context using fmt.Errorf. Check error identity with errors.Is and type with errors.As.package main
import (
"fmt"
"os"
)
func main() {
// Write file
err := os.WriteFile("data.txt", []byte("Hello, Go!\n"), 0644)
if err != nil {
fmt.Println("Error:", err)
return
}
// Read file
data, err := os.ReadFile("data.txt")
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Print(string(data))
// Using os.Open with io package
f, err := os.Open("data.txt")
if err != nil {
fmt.Println("Error:", err)
return
}
defer f.Close()
// Read with io.ReadAll
content, err := io.ReadAll(f)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Print(string(content))
}
import (
"fmt"
"os"
"path/filepath"
)
func main() {
// Create directory
os.MkdirAll("data/logs", 0755)
// List directory contents
entries, err := os.ReadDir(".")
if err != nil {
fmt.Println(err)
return
}
for _, entry := range entries {
info, _ := entry.Info()
fmt.Printf("%-10s %s\n", entry.Name(), info.Mode())
}
// Walk directory tree
filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
fmt.Println(path)
return nil
})
// Get file info
stat, _ := os.Stat("data.txt")
fmt.Println("Size:", stat.Size())
fmt.Println("Mode:", stat.Mode())
fmt.Println("Modified:", stat.ModTime())
}
import (
"fmt"
"io"
"net/http"
)
func main() {
// Simple GET request
resp, err := http.Get("https://api.github.com/users/mayank-dev-15")
if err != nil {
fmt.Println("Error:", err)
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Status:", resp.Status)
fmt.Println("Body:", string(body))
}
// POST request with JSON
func postJSON() {
payload := `{"name": "Mayank", "age": 15}`
resp, err := http.Post(
"https://api.example.com/users",
"application/json",
strings.NewReader(payload),
)
if err != nil {
fmt.Println(err)
return
}
defer resp.Body.Close()
}
import (
"bufio"
"fmt"
"net"
"strings"
)
func handleConnection(conn net.Conn) {
defer conn.Close()
scanner := bufio.NewScanner(conn)
for scanner.Scan() {
msg := strings.TrimSpace(scanner.Text())
fmt.Printf("Received: %s\n", msg)
conn.Write([]byte("Echo: " + msg + "\n"))
}
}
func main() {
listener, err := net.Listen("tcp", ":8080")
if err != nil {
fmt.Println(err)
return
}
defer listener.Close()
fmt.Println("Server listening on :8080")
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println(err)
continue
}
go handleConnection(conn) // handle each connection concurrently
}
}
import (
"fmt"
"net"
)
func main() {
addr, _ := net.ResolveUDPAddr("udp", ":9090")
conn, _ := net.ListenUDP("udp", addr)
defer conn.Close()
buf := make([]byte, 1024)
for {
n, remoteAddr, _ := conn.ReadFromUDP(buf)
fmt.Printf("Received from %s: %s\n", remoteAddr, string(buf[:n]))
conn.WriteToUDP([]byte("ACK"), remoteAddr)
}
}
defer. Use io.ReadAll (not ioutil.ReadAll) for reading entire responses. For large files, use streaming with bufio.Scanner to avoid loading everything into memory.Go has a built-in testing framework. Files end with _test.go.
// math.go
package math
func Add(a, b int) int {
return a + b
}
func Multiply(a, b int) int {
return a * b
}
func Divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero")
}
return a / b, nil
}
// math_test.go
package math
import "testing"
func TestAdd(t *testing.T) {
result := Add(2, 3)
if result != 5 {
t.Errorf("Add(2, 3) = %d; want 5", result)
}
}
func TestMultiply(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive", 3, 4, 12},
{"zero", 5, 0, 0},
{"negative", -2, 3, -6},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Multiply(tt.a, tt.b)
if result != tt.expected {
t.Errorf("Multiply(%d, %d) = %d; want %d",
tt.a, tt.b, result, tt.expected)
}
})
}
}
func TestDivide(t *testing.T) {
result, err := Divide(10, 2)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result != 5 {
t.Errorf("Divide(10, 2) = %f; want 5", result)
}
_, err = Divide(10, 0)
if err == nil {
t.Error("expected error for division by zero")
}
}
func TestFibonacci(t *testing.T) {
tests := []struct {
input int
expected int
}{
{0, 0},
{1, 1},
{2, 1},
{5, 5},
{10, 55},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("fib(%d)", tt.input), func(t *testing.T) {
result := Fibonacci(tt.input)
if result != tt.expected {
t.Errorf("Fibonacci(%d) = %d; want %d",
tt.input, result, tt.expected)
}
})
}
}
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(1, 2)
}
}
func BenchmarkFibonacci(b *testing.B) {
for i := 0; i < b.N; i++ {
Fibonacci(20)
}
}
// Run: go test -bench=. -benchmem
// Output:
// BenchmarkAdd-8 1000000000 0.25 ns/op 0 B/op 0 allocs/op
// BenchmarkFibonacci-8 50000 32000 ns/op 16 B/op 1 allocs/op
func FuzzReverse(f *testing.F) {
// Seed corpus
f.Add("hello")
f.Add("Go")
f.Add("")
f.Fuzz(func(t *testing.T, s string) {
rev := Reverse(s)
doubleRev := Reverse(rev)
if s != doubleRev {
t.Errorf("double reverse mismatch: %s vs %s", s, doubleRev)
}
})
}
// Run: go test -fuzz=FuzzReverse -fuzztime=10s
// Helper function
func assertEqual(t *testing.T, got, want int) {
t.Helper() // marks as test helper — better error messages
if got != want {
t.Errorf("got %d, want %d", got, want)
}
}
// Parallel tests
func TestWithTimeout(t *testing.T) {
t.Parallel() // run in parallel with other tests
result := slowOperation()
assertEqual(t, result, 42)
}
// Test cleanup
func TestWithResource(t *testing.T) {
tmpFile := createTempFile(t)
t.Cleanup(func() {
os.Remove(tmpFile)
})
// test using tmpFile...
}
go test ./.... Use -race flag to detect race conditions. Table-driven tests are the Go standard — keep test cases as data. Use t.Helper() in helper functions for clearer error lines.package main
import (
"fmt"
"os"
)
func main() {
// os.Args[0] is the program name
if len(os.Args) < 2 {
fmt.Println("Usage: program ")
os.Exit(1)
}
command := os.Args[1]
args := os.Args[2:]
switch command {
case "greet":
if len(args) > 0 {
fmt.Printf("Hello, %s!\n", args[0])
} else {
fmt.Println("Hello, World!")
}
case "version":
fmt.Println("v1.0.0")
default:
fmt.Printf("Unknown command: %s\n", command)
os.Exit(1)
}
}
package main
import (
"flag"
"fmt"
"strings"
)
func main() {
// Define flags
name := flag.String("name", "World", "Name to greet")
count := flag.Int("count", 1, "Number of greetings")
upper := flag.Bool("upper", false, "Uppercase output")
output := flag.String("output", "stdout", "Output file")
// Custom flag type
var colors flag.Value
colors = &stringSliceFlag{value: []string{"red", "blue"}}
flag.Var(colors, "colors", "Colors to use")
// Parse flags
flag.Parse()
// Usage
for i := 0; i < *count; i++ {
msg := fmt.Sprintf("Hello, %s!", *name)
if *upper {
msg = strings.ToUpper(msg)
}
fmt.Fprintln(os.Stdout, msg)
}
}
// String slice flag
type stringSliceFlag struct {
value []string
}
func (s *stringSliceFlag) String() string {
return strings.Join(s.value, ",")
}
func (s *stringSliceFlag) Set(val string) error {
s.value = strings.Split(val, ",")
return nil
}
// Run:
// ./cli -name Mayank -count 3 -upper
// ./cli -name="Go Developers" -count=5
// go get github.com/spf13/cobra
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
func main() {
var rootCmd = &cobra.Command{
Use: "myapp",
Short: "My awesome CLI tool",
Long: "A longer description of your application",
}
// Subcommand
var serveCmd = &cobra.Command{
Use: "serve [port]",
Short: "Start the server",
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
port := "8080"
if len(args) > 0 {
port = args[0]
}
fmt.Printf("Server starting on :%s\n", port)
},
}
var greetCmd = &cobra.Command{
Use: "greet [name]",
Short: "Greet someone",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
name := args[0]
upper, _ := cmd.Flags().GetBool("upper")
if upper {
name = strings.ToUpper(name)
}
fmt.Printf("Hello, %s!\n", name)
},
}
greetCmd.Flags().BoolP("upper", "u", false, "Uppercase output")
rootCmd.AddCommand(serveCmd, greetCmd)
rootCmd.Execute()
}
import (
"encoding/json"
"fmt"
"os"
"text/tabwriter"
)
// Pretty table output
func printTable(data [][]string) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
for _, row := range data {
fmt.Fprintln(w, strings.Join(row, "\t"))
}
w.Flush()
}
// JSON output
func printJSON(v interface{}) {
encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", " ")
encoder.Encode(v)
}
// Colored output (ANSI codes)
func colorPrint(color, text string) {
codes := map[string]string{
"red": "\033[31m",
"green": "\033[32m",
"yellow": "\033[33m",
"blue": "\033[34m",
"reset": "\033[0m",
}
fmt.Printf("%s%s%s\n", codes[color], text, codes["reset"])
}
// Simple progress bar
func progressBar(total int) {
for i := 0; i <= total; i++ {
percent := float64(i) / float64(total) * 100
bar := strings.Repeat("=", i) + strings.Repeat("-", total-i)
fmt.Printf("\r[%s] %.0f%%", bar, percent)
time.Sleep(50 * time.Millisecond)
}
fmt.Println()
}
// Spinner for async operations
func spinner(msg string) {
chars := []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
for i := 0; ; i++ {
fmt.Printf("\r%s %s", chars[i%len(chars)], msg)
time.Sleep(100 * time.Millisecond)
}
}
flag for simple CLIs and cobra for complex multi-command tools. Always provide --help text. Write output to stdout (not stderr) unless it's errors. Use os.Exit(1) for error codes.package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
var users = []User{
{ID: 1, Name: "Mayank", Email: "may@dev.com"},
{ID: 2, Name: "GoBot", Email: "bot@dev.com"},
}
func main() {
http.HandleFunc("/", homeHandler)
http.HandleFunc("/users", usersHandler)
http.HandleFunc("/users/", userHandler)
fmt.Println("Server running on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
func homeHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Welcome to the Go API")
}
func usersHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch r.Method {
case http.MethodGet:
json.NewEncoder(w).Encode(users)
case http.MethodPost:
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
user.ID = len(users) + 1
users = append(users, user)
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
// Logging middleware
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %s %v", r.RemoteAddr, r.Method, r.URL.Path, time.Since(start))
})
}
// CORS middleware
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
// Auth middleware
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// validate token...
next.ServeHTTP(w, r)
})
}
// Chain middlewares
func setupRoutes() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/api/users", usersHandler)
var handler http.Handler = mux
handler = authMiddleware(handler)
handler = corsMiddleware(handler)
handler = loggingMiddleware(handler)
return handler
}
// go get github.com/go-chi/chi/v5
import (
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
func main() {
r := chi.NewRouter()
// Built-in middleware
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(middleware.URLFormat)
r.Use(middleware.RealIP)
// Routes
r.Route("/api", func(r chi.Router) {
r.Route("/users", func(r chi.Router) {
r.Get("/", listUsers)
r.Post("/", createUser)
r.Route("/{userID}", func(r chi.Router) {
r.Get("/", getUser)
r.Put("/", updateUser)
r.Delete("/", deleteUser)
})
})
})
fmt.Println("Server running on :8080")
log.Fatal(http.ListenAndServe(":8080", r))
}
func getUser(w http.ResponseWriter, r *http.Request) {
userID := chi.URLParam(r, "userID")
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"id": userID})
}
type Article struct {
ID int `json:"id"`
Title string `json:"title"`
Content string `json:"content,omitempty"` // omitempty omits zero values
Author string `json:"-"`
CreatedAt time.Time `json:"created_at"`
Tags []string `json:"tags,omitempty"`
}
func createArticle(w http.ResponseWriter, r *http.Request) {
var article Article
// Decode request body
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields() // reject unknown fields
if err := decoder.Decode(&article); err != nil {
http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest)
return
}
// Set server-side fields
article.ID = len(articles) + 1
article.CreatedAt = time.Now()
articles = append(articles, article)
// Encode response
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
encoder := json.NewEncoder(w)
encoder.SetIndent("", " ")
encoder.Encode(article)
}
// Custom JSON response helper
func respondJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func respondError(w http.ResponseWriter, status int, message string) {
respondJSON(w, status, map[string]string{"error": message})
}
// Full REST API skeleton
type API struct {
router *chi.Mux
store *Store
port string
}
func NewAPI(port string) *API {
api := &API{
router: chi.NewRouter(),
store: NewStore(),
port: port,
}
api.setupRoutes()
return api
}
func (a *API) setupRoutes() {
a.router.Use(middleware.Logger)
a.router.Use(middleware.Recoverer)
a.router.Route("/api/v1", func(r chi.Router) {
r.Route("/users", func(r chi.Router) {
r.Get("/", a.ListUsers)
r.Post("/", a.CreateUser)
r.Get("/{id}", a.GetUser)
r.Put("/{id}", a.UpdateUser)
r.Delete("/{id}", a.DeleteUser)
})
})
}
func (a *API) ListUsers(w http.ResponseWriter, r *http.Request) {
users := a.store.ListUsers()
respondJSON(w, http.StatusOK, users)
}
func (a *API) Start() error {
addr := ":" + a.port
fmt.Printf("API server running on %s\n", addr)
return http.ListenAndServe(addr, a.router)
}
func main() {
api := NewAPI("8080")
log.Fatal(api.Start())
}
chi or gin for routing in production. Always validate input, set proper HTTP status codes, and return consistent JSON error responses. Use context.Context for request-scoped values and cancellation.