Golang Developer Roadmap — The Complete Free Guide
Go is one of the most quietly powerful languages in the industry right now.
Google built it in 2007 because they were frustrated with the complexity of C++ and the slowness of Python at scale. The result is a language that's fast like C, simple like Python, and has concurrency built into the language itself — not bolted on as an afterthought.
Companies like Google, Uber, Dropbox, Cloudflare, Docker, Kubernetes, and thousands of startups run their most performance-critical systems in Go. If you want to build backend systems, microservices, CLI tools, or infrastructure software that actually scales — Go is worth your time.
And here's what makes Go genuinely different from other languages: it's designed to be boring in the best possible way. No magic, no hidden complexity, no framework debates. You read Go code and immediately know what it does. That's a feature, not a limitation.
This roadmap takes you from your first Hello World to building production-quality Go services — with only free resources.
Who Is This Roadmap For?
- Backend developers who already know Python, Java, or JavaScript and want to add Go to their stack
- Students who want to break into high-paying backend / DevOps / infrastructure roles
- Developers who've heard "Go is great for microservices" and want to actually learn why
- Anyone who wants to build fast, efficient backend systems without the complexity of other compiled languages
You don't need prior experience with compiled languages. If you know any programming language, Go will feel surprisingly approachable.
Why Learn Go in the First Place?
Before you commit months to learning a language, you deserve a straight answer on why Go specifically.
Performance: Go compiles to native machine code. It's 10–100x faster than Python and Node.js for CPU-intensive tasks. It starts up in milliseconds (unlike the JVM which takes seconds).
Concurrency that actually works: Goroutines are Go's answer to threads — but they're incredibly lightweight (start with 2KB of stack) and you can run thousands of them simultaneously. Channels make communication between goroutines clean and safe. This is not an add-on — it's built into the language.
Simple by design: Go has no classes (just structs and methods), no inheritance, no exceptions (just error values), no generics complexity (well, generics were added in 1.18 but kept simple). The language spec is small enough to read in a day.
Blazing fast compilation: Even large Go projects compile in seconds. No waiting around.
Single binary deployment: Go compiles to a single self-contained binary. No runtime required, no dependency hell, no virtual environment, no Docker just to run the app.
The jobs: Go developers are among the highest-paid backend developers. Go is the language of cloud infrastructure — Kubernetes, Docker, Terraform, Prometheus, InfluxDB, CockroachDB are all written in Go.
Phase 1 — Go Fundamentals (5–6 weeks)
Setting Up Your Environment
- Download and install Go from golang.org/dl — free
- Install VS Code with the Go extension — the best free Go IDE
- Run
go versionin terminal to verify installation - Set up your Go workspace — understand
GOPATHand modules
Go Modules (the modern way):
mkdir myproject && cd myproject
go mod init github.com/yourusername/myproject
The go.mod file tracks your dependencies — like package.json in Node or requirements.txt in Python.
Free Resource:
Your First Go Program
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
Run it: go run main.go
Build it: go build -o myapp main.go
Everything in Go belongs to a package. main is special — it's the entry point. fmt is the formatting package from the standard library.
Variables and Types
Variable declaration — three ways:
// Explicit type
var name string = "Rahul"
// Type inferred
var age = 25
// Short declaration (most common inside functions)
city := "Mumbai"
Basic types:
int,int8,int16,int32,int64uint,uint8,uint16,uint32,uint64float32,float64string— immutable, UTF-8 encodedbool— true or falsebyte— alias for uint8rune— alias for int32, represents a Unicode code point
Zero values — Go variables always have a default value. No undefined, no null surprises:
- int → 0
- float → 0.0
- string → ""
- bool → false
- pointer → nil
Constants:
const Pi = 3.14159
const (
StatusOK = 200
StatusNotFound = 404
)
// iota — auto-incrementing constants
type Direction int
const (
North Direction = iota // 0
South // 1
East // 2
West // 3
)
Control Flow
if / else — no parentheses needed, braces required:
if score >= 90 {
fmt.Println("A grade")
} else if score >= 80 {
fmt.Println("B grade")
} else {
fmt.Println("Try harder")
}
// if with init statement — very Go-idiomatic
if err := doSomething(); err != nil {
log.Fatal(err)
}
switch — cleaner than if-else chains:
switch day {
case "Monday", "Tuesday", "Wednesday", "Thursday", "Friday":
fmt.Println("Weekday")
case "Saturday", "Sunday":
fmt.Println("Weekend")
default:
fmt.Println("Invalid day")
}
// Typeless switch (replaces long if-else)
switch {
case score >= 90:
grade = "A"
case score >= 80:
grade = "B"
default:
grade = "C"
}
for — Go's only loop (but it does everything):
// Traditional for
for i := 0; i < 10; i++ {
fmt.Println(i)
}
// While equivalent
for count < 100 {
count++
}
// Infinite loop
for {
// break when done
}
// Range — iterate over slices, maps, strings, channels
for index, value := range numbers {
fmt.Printf("numbers[%d] = %d\n", index, value)
}
// Ignore index with blank identifier
for _, name := range names {
fmt.Println(name)
}
Functions
Functions are first-class citizens in Go.
// Basic function
func add(a, b int) int {
return a + b
}
// Multiple return values — this is a Go superpower
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("cannot divide by zero")
}
return a / b, nil
}
// Named return values
func minMax(arr []int) (min, max int) {
min, max = arr[0], arr[0]
for _, v := range arr {
if v < min { min = v }
if v > max { max = v }
}
return // naked return — returns named values
}
// Variadic functions
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
// Functions as values (first-class)
add := func(a, b int) int { return a + b }
result := add(3, 4) // 7
// Closures
func makeCounter() func() int {
count := 0
return func() int {
count++
return count
}
}
// Defer — runs when function returns
func readFile() {
f, _ := os.Open("file.txt")
defer f.Close() // always runs, even if function panics
// read file...
}
Free Resources:
- A Tour of Go — official interactive tutorial, completely free, the best starting point
- Go by Example — code-first examples for every Go concept, free
- Learn Go with Tests — free online book, learn Go by writing tests
Phase 2 — Data Structures in Go (3–4 weeks)
Arrays
Fixed-length, same-type elements. Rarely used directly — slices are almost always better.
var arr [5]int // [0 0 0 0 0]
arr := [3]string{"Go", "is", "awesome"}
arr := [...]int{1, 2, 3, 4, 5} // compiler counts the length
Slices — The Workhorse of Go
Slices are dynamic, resizable views over arrays. You'll use them constantly.
// Create
nums := []int{1, 2, 3, 4, 5}
names := make([]string, 0, 10) // length 0, capacity 10
// Append
names = append(names, "Rahul", "Priya")
// Slice of a slice
first3 := nums[0:3] // [1 2 3]
last2 := nums[3:] // [4 5]
// len vs cap
fmt.Println(len(nums)) // 5
fmt.Println(cap(nums)) // 5
// 2D slice
matrix := [][]int{
{1, 2, 3},
{4, 5, 6},
{7, 8, 9},
}
// copy
dst := make([]int, len(nums))
copy(dst, nums)
// Delete element at index i (order doesn't matter)
nums = append(nums[:i], nums[i+1:]...)
Important: Slices are reference types. When you pass a slice to a function, the function can modify the original. When you assign a slice to another variable, they share the same underlying array. This bites beginners — understand it early.
Maps
Key-value pairs, like HashMap in Java or dict in Python.
// Create
ages := map[string]int{
"Rahul": 22,
"Priya": 21,
}
// Or with make
scores := make(map[string]int)
// Insert / update
ages["Amit"] = 23
// Read
age := ages["Rahul"]
// Check if key exists — always do this
age, ok := ages["Unknown"]
if !ok {
fmt.Println("key not found")
}
// Delete
delete(ages, "Amit")
// Iterate
for name, age := range ages {
fmt.Printf("%s is %d years old\n", name, age)
}
Note: Maps are NOT safe for concurrent access. Use sync.Map or a mutex when accessing from multiple goroutines.
Structs — Go's Version of Classes
type User struct {
ID int
Name string
Email string
Age int
IsAdmin bool
}
// Create
u := User{ID: 1, Name: "Rahul", Email: "rahul@example.com"}
u2 := User{1, "Priya", "priya@example.com", 21, false} // positional (avoid)
// Access fields
fmt.Println(u.Name)
u.Age = 22
// Pointer to struct
p := &User{Name: "Amit"}
p.Age = 25 // Go auto-dereferences — no need for p->Age like C
// Anonymous struct
config := struct {
Host string
Port int
}{"localhost", 8080}
// Struct embedding (Go's version of inheritance)
type Admin struct {
User // embed User — Admin gets all User fields and methods
Permissions []string
}
admin := Admin{
User: User{Name: "Super Admin"},
Permissions: []string{"read", "write", "delete"},
}
fmt.Println(admin.Name) // promoted field — access directly
Pointers
Go has pointers but no pointer arithmetic (unlike C). They're simpler and safer.
x := 42
p := &x // p is a pointer to x
fmt.Println(*p) // dereference: prints 42
*p = 100 // modify through pointer
fmt.Println(x) // x is now 100
// When to use pointers:
// 1. Mutate a value inside a function
// 2. Avoid copying large structs
// 3. Represent "no value" (nil pointer)
Phase 3 — Interfaces & OOP in Go (3–4 weeks)
Go doesn't have classes, inheritance, or traditional OOP. But it has something arguably better — interfaces and composition.
Methods
Functions with a receiver — Go's way of attaching behaviour to types.
type Rectangle struct {
Width, Height float64
}
// Value receiver — works on a copy
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
// Pointer receiver — can modify the struct
func (r *Rectangle) Scale(factor float64) {
r.Width *= factor
r.Height *= factor
}
rect := Rectangle{Width: 10, Height: 5}
fmt.Println(rect.Area()) // 50
rect.Scale(2)
fmt.Println(rect.Area()) // 200
Rule of thumb: Use pointer receivers when you need to modify the receiver, or when the struct is large (avoid copying). Be consistent — if some methods have pointer receivers, make them all pointer receivers.
Interfaces
An interface defines a set of methods. Any type that implements those methods automatically satisfies the interface — no explicit declaration needed. This is called implicit implementation and it's one of Go's best features.
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) }
// Both Circle and Rectangle implement Shape automatically
func printShapeInfo(s Shape) {
fmt.Printf("Area: %.2f, Perimeter: %.2f\n", s.Area(), s.Perimeter())
}
printShapeInfo(Circle{Radius: 5})
printShapeInfo(Rectangle{Width: 4, Height: 3})
The empty interface — interface{} (or any in Go 1.18+):
Satisfied by every type. Use when you need to store any value.
func printAnything(v any) {
fmt.Printf("Type: %T, Value: %v\n", v, v)
}
Type assertions:
var i interface{} = "hello"
s, ok := i.(string) // s = "hello", ok = true
n, ok := i.(int) // n = 0, ok = false (safe)
// Type switch
switch v := i.(type) {
case string:
fmt.Println("string:", v)
case int:
fmt.Println("int:", v)
default:
fmt.Println("unknown type")
}
Key interfaces from the standard library you'll use constantly:
io.Reader— anything you can read from (files, HTTP bodies, strings)io.Writer— anything you can write to (files, HTTP responses, buffers)error— the built-in error interface (Error() string)fmt.Stringer— implementString() stringto control how your type printsjson.Marshaler/json.Unmarshaler— custom JSON serialisationhttp.Handler— anything that can handle HTTP requests
Phase 4 — Error Handling (2–3 weeks)
Go's approach to errors is fundamentally different from most languages. There are no try-catch-finally blocks. Errors are just values — and you check them explicitly.
This is not a limitation. It's a philosophy: errors are normal, expected outcomes of functions. Treat them like the data they are.
The Error Interface
type error interface {
Error() string
}
Any type with an Error() string method is an error.
Handling Errors — The Idiomatic Way
result, err := strconv.Atoi("123")
if err != nil {
// handle error
log.Printf("conversion failed: %v", err)
return
}
// use result safely
fmt.Println(result + 1)
Yes, you write if err != nil a lot. That's the point — errors are explicit. You can't ignore them accidentally.
Creating Errors
// Simple error
import "errors"
err := errors.New("something went wrong")
// Formatted error
import "fmt"
err := fmt.Errorf("user %d not found", userID)
// Custom error type — more information
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message)
}
Wrapping and Unwrapping Errors (Go 1.13+)
// Wrap — add context while preserving the original
err = fmt.Errorf("database query failed: %w", originalErr)
// Unwrap
errors.Is(err, ErrNotFound) // check if any wrapped error matches
errors.As(err, &myErrType) // extract a specific error type
Sentinel errors — predefined errors to check against:
var ErrNotFound = errors.New("not found")
var ErrUnauthorized = errors.New("unauthorized")
func GetUser(id int) (*User, error) {
if id == 0 {
return nil, ErrNotFound
}
// ...
}
user, err := GetUser(0)
if errors.Is(err, ErrNotFound) {
// respond with 404
}
Panic and Recover
Panic is Go's version of an exception — but it's reserved for truly unrecoverable situations (index out of bounds, nil pointer dereference, programmer errors).
panic("something catastrophically wrong")
// recover() catches panics
func safeDiv(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered from panic: %v", r)
}
}()
return a / b, nil
}
Rule: Don't use panic for normal error handling. Return errors. Use panic only when the program genuinely cannot continue.
Free Resources:
Phase 5 — Concurrency (4–6 weeks)
This is where Go truly shines. Concurrency is one of the top reasons developers choose Go. Take your time here — understand it deeply.
Goroutines
A goroutine is a lightweight thread managed by the Go runtime. Starting one is as simple as adding go before a function call.
func sayHello(name string) {
fmt.Printf("Hello, %s!\n", name)
}
func main() {
go sayHello("Rahul") // runs concurrently
go sayHello("Priya") // runs concurrently
time.Sleep(100 * time.Millisecond) // wait for goroutines to finish (not ideal)
}
Goroutines start with only 2KB of stack (vs ~1MB for OS threads) and can grow as needed. You can run thousands of goroutines without breaking a sweat.
Channels — Communication Between Goroutines
Don't communicate by sharing memory. Share memory by communicating.
// Unbuffered channel — send blocks until someone receives
ch := make(chan int)
go func() {
ch <- 42 // send
}()
value := <-ch // receive (blocks until value available)
fmt.Println(value) // 42
// Buffered channel — send doesn't block until buffer is full
ch := make(chan string, 3)
ch <- "first"
ch <- "second"
ch <- "third"
// ch <- "fourth" // this would block — buffer full
// Closing a channel
close(ch)
for msg := range ch { // range on channel reads until closed
fmt.Println(msg)
}
// Directional channels — restrict to send-only or receive-only
func producer(out chan<- int) { out <- 42 } // send-only
func consumer(in <-chan int) { fmt.Println(<-in) } // receive-only
Select Statement
Like a switch for channels — waits for whichever channel is ready first.
select {
case msg := <-ch1:
fmt.Println("from ch1:", msg)
case msg := <-ch2:
fmt.Println("from ch2:", msg)
case <-time.After(5 * time.Second):
fmt.Println("timeout!")
default:
fmt.Println("no message ready") // non-blocking
}
sync Package — Traditional Synchronisation
Sometimes channels are overkill. Use sync for simpler cases.
// Mutex — protect shared data
var mu sync.Mutex
var counter int
func increment() {
mu.Lock()
defer mu.Unlock()
counter++
}
// RWMutex — multiple readers, one writer
var rw sync.RWMutex
rw.RLock() // multiple goroutines can read at once
rw.RUnlock()
rw.Lock() // exclusive write lock
rw.Unlock()
// WaitGroup — wait for goroutines to finish
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
fmt.Println("worker", n)
}(i)
}
wg.Wait() // blocks until all Done() calls received
// Once — run something exactly once
var once sync.Once
once.Do(func() {
// initialise expensive resource — runs only once
})
Context — Cancellation and Deadlines
Context is how you cancel goroutines and set timeouts. Critical for production code.
// With timeout — cancel after 5 seconds
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() // always defer cancel to release resources
// With cancellation
ctx, cancel := context.WithCancel(context.Background())
go func() {
time.Sleep(2 * time.Second)
cancel() // cancel work from another goroutine
}()
// In your function — check if context is done
select {
case <-ctx.Done():
return ctx.Err() // context.DeadlineExceeded or context.Canceled
default:
// continue working
}
// Pass context through your call chain
func GetUser(ctx context.Context, id int) (*User, error) {
return db.QueryRowContext(ctx, "SELECT * FROM users WHERE id = ?", id)
}
Common Concurrency Patterns
Worker Pool — process tasks with N workers:
func workerPool(jobs <-chan int, results chan<- int, numWorkers int) {
var wg sync.WaitGroup
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := range jobs {
results <- j * j // process job
}
}()
}
wg.Wait()
close(results)
}
Pipeline — chain stages of processing:
func generate(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}
Free Resources:
- Go Concurrency Patterns — Rob Pike (YouTube) — legendary talk, watch this
- Advanced Go Concurrency Patterns — YouTube
- Go Blog — Pipelines and Cancellation
- Go Blog — Share Memory by Communicating
Phase 6 — Go Standard Library (3–4 weeks)
The Go standard library is one of the richest in any language. Learn these packages — they'll reduce your external dependency count dramatically.
fmt — Formatted I/O
fmt.Println("Hello") // print with newline
fmt.Printf("Hello, %s!\n", name) // formatted print
fmt.Sprintf("Hello, %s!", name) // return formatted string
fmt.Fprintf(w, "Hello, %s!", name) // write to any io.Writer
// Format verbs:
// %v — default format
// %+v — struct with field names
// %#v — Go syntax representation
// %T — type
// %d — integer
// %f — float
// %s — string
// %q — quoted string
// %x — hexadecimal
// %b — binary
// %p — pointer
strings and strconv
import "strings"
strings.Contains("hello world", "world") // true
strings.HasPrefix("golang", "go") // true
strings.ToUpper("hello") // "HELLO"
strings.Split("a,b,c", ",") // ["a", "b", "c"]
strings.Join([]string{"a","b","c"}, "-") // "a-b-c"
strings.TrimSpace(" hello ") // "hello"
strings.Replace("aababc", "ab", "X", 2) // "aXXc"
strings.Builder{} // efficient string building
import "strconv"
strconv.Itoa(42) // "42"
strconv.Atoi("42") // 42, nil
strconv.ParseFloat("3.14", 64) // 3.14, nil
strconv.FormatBool(true) // "true"
encoding/json — JSON in Go
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email,omitempty"` // omit if empty
pass string // unexported — never included in JSON
}
// Marshal (Go struct → JSON)
user := User{ID: 1, Name: "Rahul"}
data, err := json.Marshal(user)
// data = []byte(`{"id":1,"name":"Rahul"}`)
// Unmarshal (JSON → Go struct)
jsonStr := `{"id":1,"name":"Rahul","email":"rahul@example.com"}`
var u User
err = json.Unmarshal([]byte(jsonStr), &u)
// Streaming — for large JSON
dec := json.NewDecoder(r.Body)
err = dec.Decode(&u)
enc := json.NewEncoder(w)
enc.Encode(user)
net/http — HTTP Client and Server
// HTTP Server — no framework needed for simple cases
http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %s!", r.URL.Query().Get("name"))
})
http.ListenAndServe(":8080", nil)
// HTTP Client
resp, err := http.Get("https://api.github.com/users/golang")
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
// POST with body
payload := strings.NewReader(`{"key":"value"}`)
resp, err := http.Post(url, "application/json", payload)
// Custom client with timeout
client := &http.Client{Timeout: 10 * time.Second}
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
req.Header.Set("Authorization", "Bearer "+token)
resp, err := client.Do(req)
os and io — Files and System
// Read entire file
data, err := os.ReadFile("file.txt")
// Write entire file
err = os.WriteFile("output.txt", []byte("content"), 0644)
// Open for streaming
f, err := os.Open("large.txt")
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := scanner.Text()
}
// Environment variables
os.Getenv("DATABASE_URL")
os.Setenv("KEY", "value")
os.Args // command line arguments
// Paths
filepath.Join("dir", "subdir", "file.txt")
filepath.Ext("main.go") // ".go"
filepath.Base("/path/to/file.txt") // "file.txt"
time
now := time.Now()
fmt.Println(now.Format("2006-01-02 15:04:05")) // Go's weird but unique reference time
t := time.Date(2024, time.January, 15, 0, 0, 0, 0, time.UTC)
duration := time.Since(t)
time.Sleep(2 * time.Second)
ticker := time.NewTicker(1 * time.Second) // tick every second
timer := time.NewTimer(5 * time.Second) // fire once after 5s
Free Resources:
- Go Standard Library Documentation — official, comprehensive, free
- Go by Example — Standard Library — code examples for each package
Phase 7 — Building REST APIs with Go (5–6 weeks)
This is where most Go developers spend most of their time. Go is exceptional for building fast, reliable HTTP APIs.
Option 1: net/http (No Framework)
For simple services, the standard library is enough. Many companies use it exclusively.
type Server struct {
db *Database
}
func main() {
s := &Server{db: NewDatabase()}
mux := http.NewServeMux()
mux.HandleFunc("GET /users", s.handleGetUsers)
mux.HandleFunc("POST /users", s.handleCreateUser)
mux.HandleFunc("GET /users/{id}", s.handleGetUser)
log.Fatal(http.ListenAndServe(":8080", mux))
}
func (s *Server) handleGetUsers(w http.ResponseWriter, r *http.Request) {
users, err := s.db.GetAllUsers(r.Context())
if err != nil {
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(users)
}
Option 2: Gin (Most Popular Framework)
Gin is fast, has excellent routing, and is used by thousands of production services.
go get -u github.com/gin-gonic/gin
func main() {
r := gin.Default() // includes Logger and Recovery middleware
r.GET("/users", getUsers)
r.POST("/users", createUser)
r.GET("/users/:id", getUserByID)
r.PUT("/users/:id", updateUser)
r.DELETE("/users/:id", deleteUser)
// Groups
api := r.Group("/api/v1")
api.Use(AuthMiddleware())
{
api.GET("/profile", getProfile)
api.POST("/posts", createPost)
}
r.Run(":8080") // starts on port 8080
}
func getUserByID(c *gin.Context) {
id := c.Param("id")
query := c.Query("include") // query parameter
var body CreateUserRequest
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"id": id, "user": user})
}
Option 3: Fiber (Express-Like, Very Fast)
If you're coming from Node.js Express, Fiber will feel familiar.
go get github.com/gofiber/fiber/v2
app := fiber.New()
app.Get("/users", getUsers)
app.Post("/users", createUser)
app.Listen(":8080")
Middleware — Cross-Cutting Concerns
// Custom middleware in net/http
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if !isValidToken(token) {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
// Common middleware to use:
// - Logging (log every request)
// - Authentication (verify JWT token)
// - Rate limiting (limit requests per IP)
// - CORS (allow cross-origin requests)
// - Recovery (catch panics, return 500 instead of crashing)
// - Request ID (add unique ID to each request for tracing)
Request Validation
type CreateUserRequest struct {
Name string `json:"name" binding:"required,min=2,max=50"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=8"`
Age int `json:"age" binding:"required,gte=18,lte=120"`
}
Free Resources:
- Gin Documentation — free, comprehensive
- TutorialEdge — REST API with Go — free
- freeCodeCamp — Go REST API — free YouTube course
Phase 8 — Databases (4–5 weeks)
database/sql — Standard Library
Go's built-in database package. Works with any SQL database via drivers.
import (
"database/sql"
_ "github.com/lib/pq" // PostgreSQL driver
_ "github.com/go-sql-driver/mysql" // MySQL driver
)
db, err := sql.Open("postgres", "user=postgres dbname=mydb sslmode=disable")
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(25)
db.SetConnMaxLifetime(5 * time.Minute)
// Query single row
var user User
err = db.QueryRowContext(ctx, "SELECT id, name, email FROM users WHERE id = $1", id).
Scan(&user.ID, &user.Name, &user.Email)
// Query multiple rows
rows, err := db.QueryContext(ctx, "SELECT id, name FROM users WHERE active = $1", true)
defer rows.Close()
for rows.Next() {
var u User
rows.Scan(&u.ID, &u.Name)
users = append(users, u)
}
// Execute (INSERT, UPDATE, DELETE)
result, err := db.ExecContext(ctx,
"INSERT INTO users (name, email) VALUES ($1, $2)",
"Rahul", "rahul@example.com")
lastID, _ := result.LastInsertId()
// Transactions
tx, err := db.BeginTx(ctx, nil)
defer tx.Rollback() // rollback if not committed
tx.ExecContext(ctx, "UPDATE accounts SET balance = balance - $1 WHERE id = $2", amount, from)
tx.ExecContext(ctx, "UPDATE accounts SET balance = balance + $1 WHERE id = $2", amount, to)
tx.Commit()
GORM — Go ORM
GORM is the most popular ORM in Go. Great for rapid development.
import "gorm.io/gorm"
type Product struct {
gorm.Model // adds ID, CreatedAt, UpdatedAt, DeletedAt
Name string `gorm:"not null"`
Price float64
Stock int
}
// Auto-create table
db.AutoMigrate(&Product{})
// Create
db.Create(&Product{Name: "Laptop", Price: 49999.00, Stock: 10})
// Read
var product Product
db.First(&product, 1) // find by primary key
db.Where("name = ?", "Laptop").First(&product) // find with condition
var products []Product
db.Find(&products) // all products
db.Where("price < ?", 10000).Find(&products) // filtered
// Update
db.Model(&product).Update("Price", 45999.00)
db.Model(&product).Updates(map[string]interface{}{"Price": 45999, "Stock": 8})
// Delete (soft delete with gorm.Model)
db.Delete(&product, 1)
// Associations
type User struct {
gorm.Model
Name string
Orders []Order
}
type Order struct {
gorm.Model
UserID uint
Total float64
}
db.Preload("Orders").Find(&users) // load users with their orders
PostgreSQL — The Recommended Database
Use PostgreSQL for production Go apps. It's powerful, reliable, and Go's ecosystem supports it best.
Key concepts:
- Connection pooling — use
pgxordatabase/sqlwith pool settings - Prepared statements — precompile queries for performance
- Transactions and ACID
- JSON columns — store flexible data in PostgreSQL's native JSON/JSONB
- Migrations — use golang-migrate (free) to manage schema changes
Redis — Caching and Sessions
import "github.com/go-redis/redis/v8"
rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
// Set with expiry
rdb.Set(ctx, "user:1", userJSON, 30*time.Minute)
// Get
val, err := rdb.Get(ctx, "user:1").Result()
// Common use cases:
// - Cache database query results
// - Rate limiting (INCR + EXPIRE)
// - Session storage
// - Pub/Sub messaging
// - Distributed locks
Free Resources:
- GORM Documentation — excellent free docs
- golang-migrate — free database migration tool
- TutorialEdge — Go with PostgreSQL — free
Phase 9 — Authentication & Security (2–3 weeks)
JWT Authentication
import "github.com/golang-jwt/jwt/v5"
// Generate token
claims := jwt.MapClaims{
"user_id": userID,
"email": email,
"exp": time.Now().Add(24 * time.Hour).Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString([]byte(os.Getenv("JWT_SECRET")))
// Validate token
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method")
}
return []byte(os.Getenv("JWT_SECRET")), nil
})
Password Hashing
Never store plain text passwords. Use bcrypt.
import "golang.org/x/crypto/bcrypt"
// Hash
hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
// Verify
err = bcrypt.CompareHashAndPassword(hashed, []byte(inputPassword))
if err == nil {
// password matches
}
Security Checklist for Go APIs
- Always validate and sanitise input
- Use parameterised queries — never string concatenation in SQL
- Set proper HTTP headers (X-Content-Type-Options, X-Frame-Options, CSP)
- Use HTTPS in production — never serve sensitive data over HTTP
- Rate limit your endpoints — prevent brute force attacks
- Log security events — failed logins, permission denials
- Never log secrets, passwords, or tokens
Phase 10 — Testing in Go (3–4 weeks)
Go has testing built into the standard library. You don't need a testing framework.
Unit Tests
// add.go
func Add(a, b int) int { return a + b }
// add_test.go
import "testing"
func TestAdd(t *testing.T) {
result := Add(2, 3)
if result != 5 {
t.Errorf("Add(2, 3) = %d, want 5", result)
}
}
// Table-driven tests — the Go way
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
want int
}{
{"positive numbers", 2, 3, 5},
{"negative numbers", -1, -2, -3},
{"zeros", 0, 0, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Add(tt.a, tt.b); got != tt.want {
t.Errorf("Add(%d, %d) = %d, want %d", tt.a, tt.b, got, tt.want)
}
})
}
}
Run tests: go test ./...
Run with coverage: go test -cover ./...
Run specific test: go test -run TestAdd
Benchmarks
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(2, 3)
}
}
// go test -bench=. -benchmem
Mocking with Interfaces
Go interfaces make mocking dead simple:
type UserRepository interface {
GetUser(ctx context.Context, id int) (*User, error)
CreateUser(ctx context.Context, user *User) error
}
// In tests, use a mock
type MockUserRepo struct {
users map[int]*User
}
func (m *MockUserRepo) GetUser(ctx context.Context, id int) (*User, error) {
if u, ok := m.users[id]; ok {
return u, nil
}
return nil, ErrNotFound
}
Testify — most popular testing helpers library (free):
import "github.com/stretchr/testify/assert"
assert.Equal(t, 5, Add(2, 3))
assert.NoError(t, err)
assert.Nil(t, result)
Free Resources:
Phase 11 — Docker & Deployment (2–3 weeks)
One of Go's best features is that it compiles to a single binary. This makes Docker images tiny.
Dockerfile for Go
# Multi-stage build — final image has only the binary
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o main ./cmd/api
FROM alpine:3.19
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/main .
EXPOSE 8080
CMD ["./main"]
Result: a Docker image under 20MB vs 500MB+ for Node.js or Python apps.
Environment Configuration
// Use environment variables for all configuration
type Config struct {
Port string
DatabaseURL string
JWTSecret string
RedisAddr string
}
func LoadConfig() Config {
return Config{
Port: getEnv("PORT", "8080"),
DatabaseURL: mustGetEnv("DATABASE_URL"),
JWTSecret: mustGetEnv("JWT_SECRET"),
RedisAddr: getEnv("REDIS_ADDR", "localhost:6379"),
}
}
func mustGetEnv(key string) string {
v := os.Getenv(key)
if v == "" {
log.Fatalf("required env var %s not set", key)
}
return v
}
Use godotenv for local development to load from a .env file.
Free Resources:
Phase 12 — Microservices & gRPC (4–5 weeks)
Go was basically designed for microservices. It starts fast, uses minimal memory, and handles high concurrency naturally.
gRPC
gRPC is Google's high-performance RPC framework. Faster than REST for service-to-service communication.
// user.proto
syntax = "proto3";
package user;
service UserService {
rpc GetUser (GetUserRequest) returns (User);
rpc CreateUser (CreateUserRequest) returns (User);
rpc ListUsers (ListUsersRequest) returns (stream User); // server streaming
}
message User {
int32 id = 1;
string name = 2;
string email = 3;
}
// Server implementation
type UserServer struct {
pb.UnimplementedUserServiceServer
repo UserRepository
}
func (s *UserServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
user, err := s.repo.GetUser(ctx, int(req.Id))
if err != nil {
return nil, status.Errorf(codes.NotFound, "user not found: %v", err)
}
return &pb.User{Id: int32(user.ID), Name: user.Name}, nil
}
Service-to-Service Communication
- gRPC — fastest, type-safe, supports streaming
- REST — simpler, more universal
- Message queues (RabbitMQ, Kafka, NATS) — async, decoupled
Distributed Tracing
Use OpenTelemetry (free) to trace requests across multiple services. Helps you find where latency is coming from in a microservices system.
Free Resources:
- gRPC Go Tutorial — official free guide
- TutorialEdge — Go Microservices
Projects to Build
Beginner
- CLI tool — a command-line app that does something useful (file renamer, password generator, todo list)
- URL shortener API — POST a URL, get back a short code; GET the short code, redirect
- Weather CLI — call a free weather API, display formatted output
Intermediate
- REST API with authentication — user registration, login, JWT, protected routes, PostgreSQL
- URL shortener with Redis — add caching, rate limiting, analytics
- Blog API — CRUD for posts, comments, tags, user roles
Advanced
- Real-time chat server — WebSockets, multiple rooms, history stored in DB
- Job queue system — submit background jobs, workers process them, status tracking
- Microservices project — 3 services (users, orders, notifications) communicating via gRPC + NATS
Free YouTube Channels & Resources
| Resource | Best For |
|---|---|
| TechWorld with Nana — Go Crash Course | Absolute beginners |
| freeCodeCamp — Full Go Course | 7-hour comprehensive free course |
| Melkey — Go Backend | Real-world backend Go projects |
| Anthony GG | Go system design, advanced patterns |
| Dreams of Code | Go projects, Docker, microservices |
| Matt KØDVB | Deep Go internals |
Suggested Timeline
| Phase | Topic | Duration |
|---|---|---|
| 1 | Go Fundamentals — syntax, functions, control flow | 5–6 weeks |
| 2 | Data Structures — slices, maps, structs, pointers | 3–4 weeks |
| 3 | Interfaces, Methods, OOP in Go | 3–4 weeks |
| 4 | Error Handling | 2–3 weeks |
| 5 | Concurrency — goroutines, channels, sync | 4–6 weeks |
| 6 | Standard Library | 3–4 weeks |
| 7 | REST APIs — net/http, Gin, middleware | 5–6 weeks |
| 8 | Databases — database/sql, GORM, PostgreSQL, Redis | 4–5 weeks |
| 9 | Authentication & Security | 2–3 weeks |
| 10 | Testing | 3–4 weeks |
| 11 | Docker & Deployment | 2–3 weeks |
| 12 | Microservices & gRPC | 4–5 weeks |
Total: 10–14 months of consistent part-time learning (2–3 hours/day). Full-time? Cut it in half.
Go Interview Prep
Most Asked Questions
Fundamentals:
- What is the difference between
vardeclaration and short:=declaration? - What are zero values in Go?
- What is a goroutine? How is it different from a thread?
- Explain how channels work. What's the difference between buffered and unbuffered?
- What is a defer statement? When does it execute?
- What is the blank identifier
_used for?
Data Structures:
- What is the difference between an array and a slice?
- What happens when you append to a slice beyond its capacity?
- Are maps safe for concurrent access?
- How do you copy a slice vs create a new one?
Interfaces & OOP:
- How does Go implement polymorphism?
- What is a nil interface? When does an interface equal nil?
- What is the empty interface? When would you use it?
Concurrency:
- What is a race condition? How do you detect one? (
go test -race) - What is a deadlock? Give an example.
- When would you use a Mutex vs a channel?
- What is the select statement?
- What is context used for in Go?
Error Handling:
- How does Go handle errors? Compare with exceptions.
- What is panic? When should you use it?
- How do you wrap and unwrap errors?
Free Prep:
- Go Interview Questions — GitHub
- InterviewBit — Go Questions
- Exercism Go Track — free coding exercises reviewed by mentors
Career Paths
Backend Go Developer Build APIs, services, and backend systems. High demand. Companies using Go heavily pay well above market rate.
DevOps / Platform Engineer Go is the language of DevOps tooling. Kubernetes, Docker, Terraform, Prometheus — if you understand Go, you can contribute to these projects and build custom tooling.
Site Reliability Engineer (SRE) Write tools to keep large systems running. Go is the language of choice for custom monitoring, alerting, and automation scripts.
Distributed Systems Engineer Build the infrastructure that runs at scale — messaging systems, databases, service meshes. Go's concurrency model makes it perfect for this.
Open Source Contributor Some of the most impactful open source projects are in Go. Contributing to Kubernetes, Go itself, or popular Go libraries is extremely good for your career.
One Last Thing
Go has a reputation for being "simple." Some developers interpret that as "limited" and move on. That's a mistake.
Go's simplicity is intentional. The language designers made deliberate choices to keep it small, readable, and explicit. There's no magic. When you read Go code, you know exactly what it does. That predictability is what makes Go maintainable at scale, across large teams, over years.
The developers who fall in love with Go are usually the ones who've been burned by complexity in other languages. Once you build something real in Go — a fast API, a concurrent data processor, a CLI tool — and you see how little can go wrong when the language is on your side, you'll understand why.
Write your first HTTP server. Make it accept and return JSON. Add a database. Add authentication. Deploy it in a Docker container. That journey will teach you more about Go than any amount of reading.
Start at A Tour of Go right now. It's free, it runs in your browser, and you can finish the basics in a weekend.
Join our Telegram group to connect with other Go developers and get help!