JB logo

Command Palette

Search for a command to run...

yOUTUBE
Blog
PreviousNext

Complete Go Programming Guide: From Basics to Building Production-Ready REST APIs

Master Go (Golang) programming with this comprehensive guide covering fundamentals, essential concepts, and building REST APIs with Gin & GORM. Perfect for JavaScript developers transitioning to Go - includes side-by-side comparisons, real-world examples, and a complete CRUD API project with authentication, relationships, and best practices.

Complete Go Guide: From Basics to API Development

Table of Contents

  1. Part 0: About Go
  2. Part 1: Go Fundamentals
  3. Part 2: Essential Go Concepts Cheatsheet
  4. Part 3: Building REST API with Gin & GORM
  5. Part 4: GORM CRUD Cheatsheet

About Go

Go, also known as Golang, is a programming language designed at Google by Robert Griesemer, Rob Pike, and Ken Thompson. It was officially announced in November 2009, with development starting in 2007.

Who created Golang

The three engineers who designed Go while working at Google are:

  • Robert Griesemer: A software engineer who has worked on Java's HotSpot compiler and the Google Chrome V8 JavaScript engine.
  • Rob Pike: A long-time figure in computer science, known for his work on the Unix operating system and the Plan 9 operating system at Bell Labs.
  • Ken Thompson: Another influential computer scientist and colleague of Pike from Bell Labs. He is one of the original creators of Unix and the B programming language.

The Motivation

The primary motivation behind the creation of Go was to address challenges faced with existing programming languages, particularly in the context of large-scale software development at Google. Specifically, the creators aimed to:

  • Improve Developer Productivity: Existing languages like C++ and Java were perceived as having complexities that hindered development speed and efficiency, especially in large codebases with many developers. Go aimed for a simpler, cleaner syntax and a faster compilation process.
  • Enhance Concurrency Support: With the rise of multicore processors and distributed systems, handling concurrency efficiently became crucial. Go introduced built-in, lightweight concurrency primitives like goroutines and channels, making it easier to write concurrent and parallel programs.
  • Combine Performance with Simplicity: The goal was to create a language that offered the performance and efficiency of compiled languages (like C/C++) while providing the readability and ease of use found in interpreted languages (like Python).
  • Address System-Level Programming Needs: Go was designed to be suitable for systems programming, including building network services, web servers, and other infrastructure components, where performance and reliability are critical.

In essence, Go was created to offer a modern, efficient, and scalable language solution that could meet the evolving demands of software development in a cloud-native, highly concurrent environment.

Companies Using Golang - Performance Metrics

Uber

  • Scale: Approximately 50 million lines of Go code with around 2,100 unique Go services
  • Throughput: Peak loads of 170,000 queries per second with 99.99% uptime
  • Optimization: CPU usage reduced by 50% after migrating geofence service from Node.js to Go. Profile-Guided Optimization saved the equivalent of 24,000 CPU cores

Twitch

  • Scale: 15 million daily active users, 2 million concurrent video streams at peak, over 10 billion chat messages daily
  • Throughput: Web APIs process an average of 50,000 requests per second
  • Concurrency: Able to serve over 500,000 concurrent users from each physical host with 1,500,000 goroutines per process
  • GC Improvements: Between Go 1.4 and Go 1.6, achieved a 20x improvement in garbage collection pause times. Typical pause times with Go 1.7 are close to 1ms

SendGrid

  • Scale: Processes more than 500 million messages daily
  • Performance: High throughput with consistently low latency in email delivery system

Similarweb

  • Infrastructure reduction: Reduced from 700 Node.js containers (requiring 179.2 GHz CPU and 89.6 GB memory) to 25 Go instances
  • Throughput increase: Increased from 500 to 700 requests per second

MercadoLibre

  • Latency improvement: Reduced request processing time from one minute to 10 milliseconds

Capital One

  • Cost savings: Achieved 90% cost savings

Twirp at Twitch

  • Reliability: No outages related to Twirp in over a year of operation powering dozens of backend systems

InfluxDB (Time-series database)

  • Throughput: Production deployments show ingestion rates exceeding millions of data points per second while maintaining microsecond query latencies
  • Performance advantage: Consistently outperforms traditional relational databases for time-series data by factors of 10-100x

Prometheus (Monitoring system)

  • Scale: Collects and stores millions of metrics per second using Go's efficient concurrency model, maintaining query response times under 100 milliseconds for most operations

Kubernetes

  • Scale: Production deployments demonstrate Kubernetes managing clusters with thousands of nodes and hundreds of thousands of containers while maintaining sub-second response times for API operations

Netflix

  • Infrastructure: Employs Go in edge computing infrastructure handling massive request volumes while maintaining microsecond response times for content routing decisions

Twitch Video Streaming

  • CPU reduction: 40-50% reduction in CPU usage for equivalent throughput after migrating to Go, with more predictable memory consumption

Monzo (Digital Bank)

  • Scale: Built entire banking infrastructure on Go from day one, currently serving over 7.5 million customers
  • Performance: Handles thousands of banking transactions per second

Anti-malware Service (Case Study)

  • Throughput: Cluster of only 4 machines handling POST requests writing to Amazon S3 bucket 1 million times every minute

Generic Go HTTP Server Benchmarks

  • Throughput: Individual Go HTTP servers can handle 90,000-120,000+ requests per second on commodity 8-16 core machines

Key Performance Patterns

The data reveals consistent patterns across these companies:

  • 10-100x performance improvements over previous solutions
  • 40-50% CPU reduction for equivalent workloads
  • Sub-millisecond to microsecond latencies at scale
  • Millions of concurrent operations handled efficiently
  • Dramatic infrastructure cost reductions (50-90%)

These metrics demonstrate why Go has become the language of choice for high-performance, scalable backend systems.

Case Studies of Companies who Migrated from Node.js to Go

There are quite a few documented case studies. Here are some notable examples:

Uber (Major migration) Uber migrated their geofence lookup microservice from Node.js to Go in 2016, achieving a 50% reduction in CPU usage. This service handles over 170,000 queries per second at peak load with 99.99% uptime. Their recent collaboration with Google on Profile-Guided Optimization saved them the equivalent of 24,000 CPU cores across their top services.

Firebase (Complete backend migration) After being acquired by Google, Firebase completely migrated their backend servers from Node.js to Go for easier concurrency support and more efficient execution.

Digg Digg rewrote their "Octo" service from Node.js to Go after experiencing daily traffic spikes that caused the Node service to seize up every couple of weeks. The service needed to handle spikes from 50 to 200+ requests per minute, with each request potentially making 10-100 S3 fetches. The Go version has been running successfully for months.

Similarweb Similarweb migrated a data processing service from Node.js to Go, reducing infrastructure from 700 containers to just 25 instances while increasing throughput from 500 to 700 requests per second. The original Node.js deployment required 179.2 GHz of CPU and 89.6 GB of memory.

Serverless migration example One team documented migrating their AWS Lambda-based serverless application from TypeScript to Go, reporting significantly lower execution durations and reduced standard deviations in processing times.

Why you Should learn Golang

Based on the case studies and practical considerations, here are compelling reasons to learn Golang or migrate from Node.js/JavaScript: Based on the case studies and practical considerations, here are compelling reasons to learn Golang or migrate from Node.js/JavaScript:

Performance and Cost Savings

Dramatic resource efficiency - Real-world migrations show infrastructure reductions from 700 containers down to 25 instances while actually increasing throughput, which translates to massive cloud cost savings. Go's compiled nature and efficient memory management mean you can do more with less hardware.

Handling high-scale workloads - Go services have achieved 99.99% uptime while handling peak loads of 170,000 queries per second. If you're building services that need to scale significantly, Go gives you that headroom without proportionally increasing infrastructure.

Concurrency Made Simple

Built for modern distributed systems - Unlike Node.js where you need to carefully architect async code, Go's goroutines make concurrent programming more intuitive. When Uber needed to refresh information from multiple sources simultaneously, Go's goroutines enabled parallel execution with an easier learning curve than Node.js's single-threaded approach.

Real concurrent processing - While Node.js excels at I/O-bound tasks, Go handles CPU-intensive work alongside I/O operations without blocking. This makes it ideal for services doing data processing, transformations, or complex calculations.

Deployment Simplicity

Single binary deployment - No more node_modules folders, no runtime dependencies, no version conflicts. You compile once and deploy a single executable that runs anywhere. This dramatically simplifies Docker images, reduces deployment failures, and makes rollbacks trivial.

Predictable production behavior - Static typing catches errors at compile time, and what works in development will work in production. No more runtime type errors from typos or undefined properties.

Career and Market Positioning

High-demand skill - Go has become a preferred choice for building scalable web applications and cloud-native applications, with notable companies like Google, Uber, BBC, and SoundCloud using it. The infrastructure tooling space (Kubernetes, Docker, Terraform) is dominated by Go.

Better compensation - Go is now one of the top paying programming languages, and Go developers are in high demand as more companies adopt it for backend services.

When It Makes Sense to Learn/Migrate

You should consider Go if you're:

  • Building microservices or APIs that need to handle high throughput
  • Working with real-time systems or streaming data
  • Dealing with CPU-intensive processing alongside I/O operations
  • Building infrastructure tools, CLI applications, or DevOps tooling
  • Experiencing performance bottlenecks or high cloud costs with Node.js
  • Working in a company investing in cloud-native architecture

You might stick with Node.js if:

  • You need rapid prototyping and faster time-to-market
  • Your application is primarily I/O-bound with simple logic
  • You have a small team and hiring is a constraint (though this is changing as Go adoption grows)
  • You're building frontend-heavy applications where sharing code with the browser is valuable
  • Your workload doesn't have significant performance or scaling requirements

Learning Curve Advantage

Go is surprisingly approachable for JavaScript developers. The syntax is clean and minimalist, there's essentially one way to do things (reducing decision fatigue), and the standard library is excellent. Teams report that Go's simplicity makes it easy for newcomers to learn and reduces friction in onboarding and training.

The key insight from the case studies: companies don't migrate because Go is "better" - they migrate when they hit specific scaling, performance, or operational challenges where Go's strengths align perfectly with their needs.

Part 1: Go Fundamentals

1. Installation & Setup

Installing Go

What you're installing:

  • Go compiler (like Node.js V8 engine)
  • Go runtime (executes your Go code)
  • Go tools (go build, go run - similar to npm commands)
  • Go standard library (like Node.js built-in modules)

Steps:

  1. Visit https://golang.org
  2. Download installer for your OS
  3. Run installer
  4. Verify installation:
go version

Setting Up VSCode

Required Extensions:

  1. Go Extension (by Go Team at Google)

    • Syntax highlighting
    • IntelliSense (auto-complete)
    • Error detection
    • Debugging support
  2. Code Runner (Optional)

    • Run code with Ctrl+Alt+N
    • Quick testing without terminal

Installing Go Tools:

  1. Open VSCode Command Palette (Ctrl+Shift+P)
  2. Type "Go: Install/Update Tools"
  3. Select all tools and click OK

These tools include:

  • gopls - Go Language Server (like TypeScript Language Server)
  • dlv - Debugger
  • staticcheck - Linter (like ESLint)

2. Your First Go Program

Understanding the Structure

package main
 
import "fmt"
 
func main() {
    fmt.Println("Hello world")
}

Breaking it down:

package main

  • Every Go file must start with a package declaration
  • main package = entry point of your program
  • Like your main index.js or app.js in JavaScript

import "fmt"

  • Import packages you need (like import express from 'express')
  • fmt = "Format" - for printing and formatting text
  • Similar to console in JavaScript

func main()

  • Entry point function - Go automatically executes this
  • No need to call it manually (unlike JavaScript)

JavaScript equivalent:

// JavaScript needs you to call main()
function main() {
  console.log("Hello world");
}
main();
 
// Go automatically runs main()

fmt.Println()

  • Print to console (like console.log())
  • Println = Print Line (adds newline automatically)
  • No semicolons required in Go!

Running Your Code

Option 1: Direct Run (Development)

go run main.go

Option 2: Build then Execute (Production)

# Build executable
go build main.go
 
# Run it
./main        # Mac/Linux
main.exe      # Windows

Option 3: VSCode Code Runner

  • Right-click → "Run Code"
  • Or press Ctrl+Alt+N

3. Go Modules (Project Setup)

What are Modules?

JavaScript comparison:

JavaScript Project          Go Project
├── package.json      →    ├── go.mod
├── node_modules/     →    ├── (packages cached globally)
├── index.js               ├── main.go
└── utils/                 └── utils/

Creating a Module

Initialize your project:

go mod init github.com/yourusername/project-name

This creates go.mod file (like package.json):

module github.com/yourusername/project-name
 
go 1.21
 
require (
    github.com/gin-gonic/gin v1.9.1
)

Packages vs Modules

Simple explanation:

  • Package = Single room (one folder with .go files)
  • Module = Entire building (whole project with go.mod)

Example structure:

my-go-project/              ← MODULE
├── go.mod                 ← Module definition
├── main.go                ← main package
├── utils/                 ← utils package
│   └── helper.go
└── models/                ← models package
    └── user.go

4. Basic Data Types

Variables

package main
 
import "fmt"
 
func main() {
    // Explicit type declaration
    var name string = "John"
    var age int = 25
    var price float64 = 99.99
    var isActive bool = true
 
    // Type inference (Go guesses the type)
    var city = "New York"
 
    // Short declaration (most common)
    country := "USA"
 
    fmt.Println(name, age, price, isActive, city, country)
}

JavaScript equivalent:

// JavaScript (loosely typed)
let name = "John";
let age = 25;
let price = 99.99;
let isActive = true;

Constants

const birthYear = 1993
const pi = 3.14159
 
// Cannot be changed later
// birthYear = 1995  // ERROR!

Basic Types

Go TypeJavaScript EquivalentExample
stringstring"Hello"
intnumber42
float64number3.14
boolbooleantrue
[]stringstring[]["a", "b"]

5. Arrays and Slices

Arrays (Fixed Size)

// Fixed size - cannot change
var numbers [5]int = [5]int{1, 2, 3, 4, 5}

Slices (Dynamic Size - More Common)

// Dynamic size - can grow/shrink
names := []string{"JB", "Jane", "Gerald"}
 
// Add items
names = append(names, "Alice")
 
// Access items
fmt.Println(names[0])  // "JB"
 
// Length
fmt.Println(len(names))  // 4

JavaScript comparison:

// JavaScript arrays are always dynamic
const names = ["JB", "Jane", "Gerald"];
names.push("Alice");
console.log(names[0]);
console.log(names.length);

6. Functions

Basic Function

func greet(name string) string {
    return "Hello, " + name
}
 
// Usage
message := greet("John")
fmt.Println(message)

Multiple Return Values

func getUser() (string, int) {
    return "John", 25
}
 
// Usage
name, age := getUser()
fmt.Println(name, age)  // John 25

JavaScript equivalent:

// JavaScript needs array or object
function getUser() {
  return ["John", 25];
}
const [name, age] = getUser();

Function Example from Your Code

func calcAge() {
    const birthYear = 1993
    var currentYear = 2025
    var result = currentYear - birthYear
    var names = []string{"JB", "Jane", "Gerald"}
 
    printItems(names)
    fmt.Println(result, names)
}
 
func printItems(items []string) {
    for index, item := range items {
        fmt.Printf("%d. %s\n", index+1, item)
    }
}

7. Loops

For Loop (The Only Loop in Go!)

// Standard for loop
for i := 0; i < 5; i++ {
    fmt.Println(i)
}
 
// While-style loop
i := 0
for i < 5 {
    fmt.Println(i)
    i++
}
 
// Infinite loop
for {
    // runs forever
    break  // use break to exit
}
 
// Range loop (like forEach)
names := []string{"JB", "Jane", "Gerald"}
for index, name := range names {
    fmt.Printf("%d. %s\n", index+1, name)
}
 
// Ignore index with underscore
for _, name := range names {
    fmt.Println(name)
}

JavaScript comparison:

// For loop
for (let i = 0; i < 5; i++) {
  console.log(i);
}
 
// While loop
let i = 0;
while (i < 5) {
  console.log(i);
  i++;
}
 
// forEach
names.forEach((name, index) => {
  console.log(`${index + 1}. ${name}`);
});

8. Conditionals

If/Else

age := 25
 
if age >= 18 {
    fmt.Println("Adult")
} else if age >= 13 {
    fmt.Println("Teenager")
} else {
    fmt.Println("Child")
}
 
// Short statement before condition
if age := getAge(); age >= 18 {
    fmt.Println("Adult")
}

Switch

day := "Monday"
 
switch day {
case "Monday":
    fmt.Println("Start of week")
case "Friday":
    fmt.Println("Almost weekend!")
case "Saturday", "Sunday":
    fmt.Println("Weekend!")
default:
    fmt.Println("Midweek")
}

9. Structs (Like Objects/Classes)

// Define a struct (like a class or type)
type Person struct {
    Name string
    Age  int
    City string
}
 
// Create instance
john := Person{
    Name: "John",
    Age:  25,
    City: "New York",
}
 
// Access fields
fmt.Println(john.Name)  // "John"
 
// Modify fields
john.Age = 26

JavaScript equivalent:

// JavaScript object
const john = {
  name: "John",
  age: 25,
  city: "New York",
};
 
console.log(john.name);
john.age = 26;

10. Pointers (Important Concept!)

Go Pointers Explained

What are pointers?

A pointer is a variable that stores the memory address of another variable. In Go, you declare a pointer using * and get an address using &:

x := 42
p := &x  // p is a pointer to x
fmt.Println(*p)  // prints 42 (dereference to get the value)
*p = 21  // changes x to 21

Why use pointers?

1. Modify values in functions

Without pointers, Go passes copies of values:

Great! Let's break down pointers step by step.

What's Happening: Copy vs Reference

Without Pointers (Pass by Value)

func tryToModify(x int) {
    x = 100  // Changes only the LOCAL copy
}
 
func main() {
    age := 25
    tryToModify(age)
    fmt.Println(age)  // Still 25! ❌
}

What happens:

  1. age has value 25
  2. When you call tryToModify(age), Go copies the value
  3. Inside the function, x is a separate variable with its own copy of 25
  4. Changing x to 100 only affects that copy
  5. Original age remains unchanged

Think of it like photocopying a document - you can write on the copy, but the original stays the same.

With Pointers (Pass by Reference)

func modifyWithPointer(x *int) {
    *x = 100  // Changes the ORIGINAL value
}
 
func main() {
    age := 25
    modifyWithPointer(&age)
    fmt.Println(age)  // Now 100! ✅
}

What happens:

  1. age has value 25 stored at some memory address (let's say 0x123)
  2. &age gets the address where age lives (0x123)
  3. The function receives that address in x (a pointer)
  4. *x means "go to that address and change the value there"
  5. Original age is modified

Think of it like giving someone your home address - they can go to your actual house and change things, not just a photo of it.

Pointer Syntax

The Two Key Operators

& (address-of operator) - Gets the memory address:

age := 25
ptr := &age  // ptr now holds the address of age
fmt.Println(ptr)   // Prints something like: 0xc0000120a0

* (dereference operator) - Accesses the value at an address:

age := 25
ptr := &age       // ptr points to age
 
fmt.Println(*ptr) // Prints: 25 (value at that address)
*ptr = 100        // Changes the value at that address
fmt.Println(age)  // Prints: 100

Visual Example

func main() {
    // Create a variable
    score := 50
 
    // Get its address
    scorePtr := &score
 
    fmt.Println("Value:", score)        // 50
    fmt.Println("Address:", scorePtr)   // 0xc000012080 (example)
    fmt.Println("Value via pointer:", *scorePtr)  // 50
 
    // Modify through pointer
    *scorePtr = 75
 
    fmt.Println("New value:", score)    // 75
}

Practical Example: Updating a Struct

type Person struct {
    Name string
    Age  int
}
 
// Won't work - receives a copy
func birthdayCopy(p Person) {
    p.Age++
}
 
// Works - receives address, modifies original
func birthdayPointer(p *Person) {
    p.Age++  // Note: Go auto-dereferences, same as (*p).Age++
}
 
func main() {
    person := Person{Name: "Alice", Age: 25}
 
    birthdayCopy(person)
    fmt.Println(person.Age)  // Still 25
 
    birthdayPointer(&person)
    fmt.Println(person.Age)  // Now 26
}

When to Use Pointers?

  1. To modify the original value (like we just saw)
  2. For large structs - Avoid copying large amounts of data
  3. When you need nil - Pointers can be nil, regular values can't
  4. For consistency - Some types (maps, slices, channels) are already reference types

Common Pattern in Error Handling

func getUser(id int) (*User, error) {
    if id < 0 {
        return nil, errors.New("invalid ID")  // Can return nil pointer
    }
    user := User{ID: id, Name: "John"}
    return &user, nil  // Return pointer to user
}

Quick Mental Model:

  • & = "Give me the address of..."
  • * in type (*int) = "This is a pointer to..."
  • * before variable (*ptr) = "Go to that address and get/set the value"

2. Avoid copying large structs

Copying a large struct can be expensive. Passing a pointer is always 8 bytes (on 64-bit systems):

type LargeStruct struct {
    data [1000]int
}
 
// Inefficient - copies 8000 bytes
func processCopy(s LargeStruct) { }
 
// Efficient - passes 8 bytes
func processPointer(s *LargeStruct) { }

3. Share state

Multiple parts of your program can reference the same data:

type Config struct {
    timeout int
}
 
cfg := &Config{timeout: 30}
server := NewServer(cfg)
client := NewClient(cfg)
// Both share the same config

When to use pointers

Use pointers when you need to:

  • Modify the receiver in a method (receivers should be pointer or value consistently)
  • Avoid copying large structs (generally > 100 bytes or so)
  • Represent optional/nullable values (a nil pointer means "no value")
  • Share mutable state between functions
  • Implement interfaces that require pointer receivers
type User struct {
    name string
    age  int
}
 
// Pointer receiver - can modify the struct
func (u *User) HaveBirthday() {
    u.age++
}
 
// Value receiver - read-only operations
func (u User) IsAdult() bool {
    return u.age >= 18
}

When NOT to use pointers

Avoid pointers when:

  • Working with small values (ints, bools, small structs) - copying is cheap
  • You don't need to modify the value
  • Working with slices, maps, or channels (these already contain pointers internally)
  • You want immutability and safety from concurrent modification
// Don't do this - int is small, copy is fine
func add(a, b *int) int {
    return *a + *b
}
 
// Do this instead
func add(a, b int) int {
    return a + b
}
 
// Don't pass pointer to slice - slice header already references underlying array
func processSlice(items *[]string) { }  // Wrong
 
func processSlice(items []string) { }   // Correct

Key considerations

Performance: Pointers add indirection (extra memory lookup), so for tiny values they can actually be slower than copies.

Nil checks: Pointer receivers can be nil, which can cause panics if not handled:

func (u *User) GetName() string {
    if u == nil {
        return "unknown"
    }
    return u.name
}

Concurrency: Shared pointers require synchronization (mutexes) if accessed from multiple goroutines.

Semantics: Use value receivers when methods shouldn't modify state, pointer receivers when they should. This communicates intent.

Rule of thumb

Start with values. Use pointers only when you have a specific reason: mutation, large size, or sharing state. Go's compiler often optimizes value passing anyway, so premature optimization with pointers can make code more complex without real benefits.

Pointers hold memory addresses, not values.

// Regular variable
age := 25
 
// Pointer to age
agePointer := &age
 
// Get value from pointer
fmt.Println(*agePointer)  // 25
 
// Modify via pointer
*agePointer = 26
fmt.Println(age)  // 26

Why use pointers?

  • Modify original values in functions
  • Memory efficiency for large structs
  • Required by many Go libraries
func incrementAge(age *int) {
    *age = *age + 1
}
 
age := 25
incrementAge(&age)
fmt.Println(age)  // 26

Summary

You now understand:

  • ✅ Go installation and setup
  • ✅ Basic syntax and structure
  • ✅ Variables, constants, and types
  • ✅ Arrays and slices
  • ✅ Functions and multiple returns
  • ✅ Loops (for, range)
  • ✅ Conditionals (if, switch)
  • ✅ Structs (objects)
  • ✅ Pointers (memory references)

Next: Essential Go concepts for API development!


Part 2: Essential Go Concepts Cheatsheet

1. String Formatting (fmt package)

Basic Printing

import "fmt"
 
// Print without newline
fmt.Print("Hello")
fmt.Print("World")  // HelloWorld
 
// Print with newline
fmt.Println("Hello")
fmt.Println("World")
// Hello
// World
 
// Formatted print (like template literals)
name := "John"
age := 25
fmt.Printf("Name: %s, Age: %d\n", name, age)
// Name: John, Age: 25

Format Specifiers

SpecifierUseExample
%sStringfmt.Printf("%s", "text")
%dIntegerfmt.Printf("%d", 42)
%fFloatfmt.Printf("%.2f", 3.14159)
%tBooleanfmt.Printf("%t", true)
%vAny valuefmt.Printf("%v", anything)
%+vStruct with field namesfmt.Printf("%+v", person)
%TType of valuefmt.Printf("%T", variable)

JavaScript equivalent:

// JavaScript template literals
const name = "John";
const age = 25;
console.log(`Name: ${name}, Age: ${age}`);

2. String Manipulation

import "strings"
 
text := "Hello, World!"
 
// Convert to lowercase/uppercase
lower := strings.ToLower(text)      // "hello, world!"
upper := strings.ToUpper(text)      // "HELLO, WORLD!"
 
// Check if contains
contains := strings.Contains(text, "World")  // true
 
// Split string
parts := strings.Split("a,b,c", ",")  // ["a", "b", "c"]
 
// Join strings
joined := strings.Join([]string{"a", "b"}, "-")  // "a-b"
 
// Replace
replaced := strings.Replace(text, "World", "Go", -1)  // "Hello, Go!"
 
// Trim whitespace
trimmed := strings.TrimSpace("  hello  ")  // "hello"

JavaScript equivalent:

const text = "Hello, World!";
const lower = text.toLowerCase();
const upper = text.toUpperCase();
const contains = text.includes("World");
const parts = "a,b,c".split(",");
const joined = ["a", "b"].join("-");
const replaced = text.replace("World", "Go");
const trimmed = "  hello  ".trim();

3. Working with Maps (Objects/Dictionaries)

// Create a map
ages := make(map[string]int)
ages["John"] = 25
ages["Jane"] = 30
 
// Or create with initial values
scores := map[string]int{
    "John": 95,
    "Jane": 87,
}
 
// Access value
johnScore := scores["John"]  // 95
 
// Check if key exists
score, exists := scores["Alice"]
if exists {
    fmt.Println("Alice scored:", score)
} else {
    fmt.Println("Alice not found")
}
 
// Delete key
delete(scores, "John")
 
// Iterate over map
for name, score := range scores {
    fmt.Printf("%s: %d\n", name, score)
}

JavaScript equivalent:

// JavaScript object
const scores = {
  John: 95,
  Jane: 87,
};
 
const johnScore = scores["John"];
 
// Check if exists
if ("Alice" in scores) {
  console.log("Alice scored:", scores.Alice);
}
 
delete scores.John;
 
// Iterate
for (const [name, score] of Object.entries(scores)) {
  console.log(`${name}: ${score}`);
}

4. Error Handling

Go doesn't have try/catch. Instead, functions return errors.

import (
    "errors"
    "fmt"
)
 
// Function that returns error
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("cannot divide by zero")
    }
    return a / b, nil
}
 
// Usage - always check errors!
result, err := divide(10, 0)
if err != nil {
    fmt.Println("Error:", err)
    return
}
fmt.Println("Result:", result)

JavaScript equivalent:

// JavaScript uses try/catch
function divide(a, b) {
  if (b === 0) {
    throw new Error("cannot divide by zero");
  }
  return a / b;
}
 
try {
  const result = divide(10, 0);
  console.log("Result:", result);
} catch (err) {
  console.log("Error:", err.message);
}

5. JSON Handling

Struct to JSON (Marshal)

import (
    "encoding/json"
    "fmt"
)
 
type Person struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
    City string `json:"city"`
}
 
func main() {
    person := Person{
        Name: "John",
        Age:  25,
        City: "New York",
    }
 
    // Convert to JSON
    jsonData, err := json.Marshal(person)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
 
    fmt.Println(string(jsonData))
    // {"name":"John","age":25,"city":"New York"}
}

JSON to Struct (Unmarshal)

func main() {
    jsonStr := `{"name":"John","age":25,"city":"New York"}`
 
    var person Person
    err := json.Unmarshal([]byte(jsonStr), &person)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
 
    fmt.Printf("%+v\n", person)
    // {Name:John Age:25 City:New York}
}

JavaScript equivalent:

// JavaScript
const person = { name: "John", age: 25, city: "New York" };
 
// To JSON
const jsonStr = JSON.stringify(person);
 
// From JSON
const parsed = JSON.parse(jsonStr);

6. HTTP Requests (Fetching External APIs)

GET Request

import (
    "encoding/json"
    "fmt"
    "io"
    "net/http"
)
 
type Post struct {
    UserID int    `json:"userId"`
    ID     int    `json:"id"`
    Title  string `json:"title"`
    Body   string `json:"body"`
}
 
func fetchPost() {
    url := "https://jsonplaceholder.typicode.com/posts/1"
 
    // Make GET request
    resp, err := http.Get(url)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    defer resp.Body.Close()
 
    // Check status code
    if resp.StatusCode != 200 {
        fmt.Println("Error: Status", resp.StatusCode)
        return
    }
 
    // Read response body
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        fmt.Println("Error reading body:", err)
        return
    }
 
    // Parse JSON
    var post Post
    err = json.Unmarshal(body, &post)
    if err != nil {
        fmt.Println("Error parsing JSON:", err)
        return
    }
 
    fmt.Printf("%+v\n", post)
}

POST Request

import (
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
)
 
func createPost() {
    url := "https://jsonplaceholder.typicode.com/posts"
 
    // Create request data
    newPost := Post{
        UserID: 1,
        Title:  "My Post",
        Body:   "This is the content",
    }
 
    // Convert to JSON
    jsonData, err := json.Marshal(newPost)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
 
    // Make POST request
    resp, err := http.Post(
        url,
        "application/json",
        bytes.NewBuffer(jsonData),
    )
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    defer resp.Body.Close()
 
    fmt.Println("Status:", resp.Status)
}

JavaScript equivalent:

// JavaScript Fetch API
async function fetchPost() {
  const response = await fetch("https://jsonplaceholder.typicode.com/posts/1");
  const post = await response.json();
  console.log(post);
}
 
async function createPost() {
  const response = await fetch("https://jsonplaceholder.typicode.com/posts", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      userId: 1,
      title: "My Post",
      body: "This is the content",
    }),
  });
  const data = await response.json();
  console.log(data);
}

7. Working with Files

Reading Files

import (
    "fmt"
    "os"
)
 
func readFile() {
    // Read entire file
    data, err := os.ReadFile("config.txt")
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
 
    fmt.Println(string(data))
}

Writing Files

func writeFile() {
    content := []byte("Hello, World!")
 
    err := os.WriteFile("output.txt", content, 0644)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
 
    fmt.Println("File written successfully")
}

8. Environment Variables

import (
    "fmt"
    "os"
)
 
func main() {
    // Get environment variable
    dbURL := os.Getenv("DATABASE_URL")
 
    if dbURL == "" {
        fmt.Println("DATABASE_URL not set")
        return
    }
 
    fmt.Println("Database URL:", dbURL)
 
    // Set environment variable (for current process)
    os.Setenv("APP_ENV", "development")
}

9. Goroutines (Concurrency - Like Async/Await)

import (
    "fmt"
    "time"
)
 
func fetchUser() {
    time.Sleep(2 * time.Second)
    fmt.Println("User fetched")
}
 
func fetchPosts() {
    time.Sleep(1 * time.Second)
    fmt.Println("Posts fetched")
}
 
func main() {
    // Run concurrently (like Promise.all)
    go fetchUser()
    go fetchPosts()
 
    // Wait for goroutines to finish
    time.Sleep(3 * time.Second)
    fmt.Println("Done")
}

JavaScript equivalent:

async function fetchUser() {
  await new Promise((resolve) => setTimeout(resolve, 2000));
  console.log("User fetched");
}
 
async function fetchPosts() {
  await new Promise((resolve) => setTimeout(resolve, 1000));
  console.log("Posts fetched");
}
 
// Run concurrently
Promise.all([fetchUser(), fetchPosts()]).then(() => {
  console.log("Done");
});

10. Interfaces (Like TypeScript Interfaces)

// Define interface
type Animal interface {
    Speak() string
}
 
// Implement interface (implicit)
type Dog struct {
    Name string
}
 
func (d Dog) Speak() string {
    return "Woof!"
}
 
type Cat struct {
    Name string
}
 
func (c Cat) Speak() string {
    return "Meow!"
}
 
// Use interface
func makeAnimalSpeak(a Animal) {
    fmt.Println(a.Speak())
}
 
func main() {
    dog := Dog{Name: "Buddy"}
    cat := Cat{Name: "Whiskers"}
 
    makeAnimalSpeak(dog)  // Woof!
    makeAnimalSpeak(cat)  // Meow!
}

TypeScript equivalent:

interface Animal {
  speak(): string;
}
 
class Dog implements Animal {
  name: string;
 
  speak(): string {
    return "Woof!";
  }
}
 
function makeAnimalSpeak(animal: Animal) {
  console.log(animal.speak());
}

Project Series Summary


Project 1: Weather CLI + Web Dashboard

Video 1: "Build a Real-Time Weather CLI App in Go | Beginner Go Tutorial"

Description: Learn Go by building a practical command-line weather application that fetches real-time weather data from the OpenWeatherMap API. Perfect for beginners who want to understand HTTP requests, JSON parsing, and error handling while creating something useful you'll actually use!

Features:

  • Fetch current weather for any city worldwide
  • Display temperature (Celsius/Fahrenheit), conditions, and humidity
  • Show 5-day weather forecast
  • Colored terminal output for better readability
  • Proper error handling for invalid cities and API failures
  • Save favorite cities for quick access
  • Command-line arguments parsing (e.g., weather Kampala --forecast)

Tech Stack: Go standard library, OpenWeatherMap API, net/http, encoding/json


Video 2: "Turn Your CLI into a Web App | Go HTTP Server Tutorial"

Description: Take your weather CLI to the next level by building a beautiful web dashboard! Learn how to create HTTP servers in Go, use HTML templates, and build a responsive web interface. This tutorial shows you how to transform any CLI tool into a web application.

Features:

  • Simple HTTP server with multiple routes
  • HTML templates for dynamic content rendering
  • Search weather by city through web form
  • Responsive design that works on mobile
  • Display current weather with icons
  • 5-day forecast cards
  • Error handling with user-friendly messages
  • Cache API responses to reduce API calls (bonus)
  • Auto-refresh every 30 minutes

Tech Stack: net/http, html/template, CSS, OpenWeatherMap API


Project 2: URL Shortener with Analytics

"Build Your Own Bit.ly Clone in Go | REST API Tutorial"

Description: Create a professional URL shortener service like bit.ly from scratch! This project teaches you how to build REST APIs, handle redirects, track analytics, and persist data. Perfect for intermediate Go developers looking to build portfolio-worthy projects and understand backend development patterns.

Features:

  • Shorten long URLs into 6-character codes
  • Custom short codes (optional)
  • Redirect short URLs to original destinations
  • Click tracking and analytics
  • View stats: total clicks, creation date, last accessed
  • List all shortened URLs
  • Delete shortened URLs
  • JSON file persistence (or optional database)
  • Thread-safe concurrent access with mutexes
  • Rate limiting (bonus)
  • API authentication (bonus)

API Endpoints:

POST   /api/shorten          - Create short URL
GET    /:code                - Redirect to original URL
GET    /api/stats/:code      - Get analytics
GET    /api/urls             - List all URLs
DELETE /api/urls/:code       - Delete URL
GET    /                     - Web interface (optional)

Tech Stack: net/http, encoding/json, sync.RWMutex, JSON file storage


Project 3: Expense Tracker API + CLI

Video 1: "Build a Complete Expense Tracker in Go | CLI Application"

Description: Master Go by building a real-world expense tracking application! Learn how to work with dates, perform data aggregations, implement business logic, and create a polished command-line interface. This project covers essential concepts every Go developer needs to know.

Features:

  • Add expenses with amount, description, category, and date
  • List all expenses with filtering options
  • Filter by month, year, or category
  • Calculate totals by period or category
  • Set monthly budgets per category
  • Budget status warnings (over budget alerts)
  • Monthly/yearly expense reports
  • Export data to CSV
  • Import expenses from CSV
  • Data persistence with JSON
  • Colored output for budget status

CLI Commands:

expense add 50.00 "Lunch at cafe" --category food
expense list --month 10 --year 2025
expense list --category transport
expense total --month 10
expense budget set food 500
expense budget set transport 200
expense budget status
expense report --month 10
expense export expenses.csv

Tech Stack: Go standard library, time package, encoding/json, encoding/csv, flag package


Video 2: "Add a REST API to Your Expense Tracker | Go Backend Tutorial"

Description: Transform your CLI expense tracker into a full-featured REST API! Learn how to structure Go applications, handle HTTP requests/responses, validate user input, and implement proper error handling. Build the backend foundation for a mobile or web frontend.

Features:

  • RESTful API endpoints for all expense operations
  • JSON request/response handling
  • Input validation and error responses
  • CORS support for web frontends
  • Query parameters for filtering
  • Pagination for large datasets
  • Summary endpoints for analytics
  • Budget management endpoints
  • Authentication (optional with JWT)
  • Rate limiting per user
  • API documentation

API Endpoints:

POST   /api/expenses              - Create expense
GET    /api/expenses              - List expenses (with filters)
GET    /api/expenses/:id          - Get single expense
PUT    /api/expenses/:id          - Update expense
DELETE /api/expenses/:id          - Delete expense
GET    /api/expenses/summary      - Get totals and stats
GET    /api/expenses/by-category  - Group by category
POST   /api/budgets               - Set budget
GET    /api/budgets               - List budgets
GET    /api/budgets/status        - Check budget status
GET    /api/reports/monthly       - Monthly report

Tech Stack: net/http, encoding/json, gorilla/mux (optional), middleware patterns


Video 3: "Advanced Features | Data Visualization & Export" (Optional Bonus)

Description: Take your expense tracker to the next level with advanced features! Add data visualization, recurring expenses, categories management, and multi-currency support. Learn advanced Go patterns and best practices.

Features:

  • Recurring expenses (weekly, monthly, yearly)
  • Category management (add, edit, delete categories)
  • Multi-currency support with exchange rates
  • Data visualization endpoint (chart data)
  • PDF report generation
  • Email notifications for budget alerts
  • Backup and restore functionality
  • Data migration tools
  • Search expenses by description
  • Tags for expenses
  • Attachments (receipt images)

Tech Stack: Additional packages for PDF generation, charting libraries


📊 Skills Progression Matrix

SkillWeather AppURL ShortenerExpense Tracker
HTTP Requests✅ GET✅ GET/POST/DELETE✅ All methods
JSON Parsing✅ Basic✅ Intermediate✅ Advanced
Error Handling✅ Basic✅ Intermediate✅ Advanced
File I/O⚪ Optional✅ JSON✅ JSON + CSV
Structs & Methods✅ Basic✅ Intermediate✅ Advanced
Time/Date⚪ Display only✅ Timestamps✅ Full manipulation
Concurrency⚪ None✅ Mutexes⚪ Optional
Testing⚪ Optional✅ Unit tests✅ Full coverage
Web Templates✅ HTML⚪ Optional⚪ Optional
CLI Arguments✅ Basic⚪ None✅ Advanced
Business Logic⚪ Light✅ Moderate✅ Complex

💡 Viewer Challenges (End of Each Video)

Weather App:

  1. Add support for multiple cities at once
  2. Show sunrise/sunset times
  3. Add air quality index
  4. Create a 7-day forecast

URL Shortener:

  1. Add custom domains support
  2. Implement QR code generation
  3. Add expiration dates for URLs
  4. Create a browser extension

Part 3: Building REST API with Gin & GORM

Project Setup

1. Initialize Project

# Create project directory
mkdir go-crud-api
cd go-crud-api
 
# Initialize Go module
go mod init github.com/yourusername/go-crud-api

2. Install Dependencies

# Install Gin (Web framework)
go get -u github.com/gin-gonic/gin
 
# Install GORM (ORM)
go get -u gorm.io/gorm
 
# Install Database Driver (PostgreSQL)
go get -u gorm.io/driver/postgres
 
# Install for Environment Variables
go get github.com/joho/godotenv
 
# Install Hot Reload (Development)
go get github.com/githubnemo/CompileDaemon
go install github.com/githubnemo/CompileDaemon

Project Structure

go-crud-api/
├── controllers/
│   └── category_controller.go
├── models/
│   └── category.go
├── dtos/
│   └── category_dto.go
├── initializers/
│   ├── database.go
│   └── loadEnvVariables.go
├── migrate/
│   └── migrate.go
├── .env
├── go.mod
├── go.sum
└── main.go

Step 1: Environment Variables

Create .env file

DATABASE_URL="host=your-host user=your-user password=your-password dbname=your-db port=5432 sslmode=require"
PORT=8080

Create initializers/loadEnvVariables.go

package initializers
 
import (
    "log"
    "github.com/joho/godotenv"
)
 
func LoadEnvVariables() {
    err := godotenv.Load()
    if err != nil {
        log.Fatal("Error loading .env file")
    }
}

Step 2: Database Connection

Create initializers/database.go

package initializers
 
import (
    "fmt"
    "log"
    "os"
 
    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)
 
var DB *gorm.DB
 
func ConnectToDB() {
    var err error
    dsn := os.Getenv("DATABASE_URL")
 
    DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
    if err != nil {
        log.Fatal("Failed to connect to database:", err)
    }
 
    fmt.Println("✅ Successfully connected to database!")
}

Step 3: Create Model

Create models/category.go

package models
 
import "gorm.io/gorm"
 
type Category struct {
    gorm.Model
    Title       string `gorm:"uniqueIndex;not null"`
    Slug        string `gorm:"uniqueIndex;not null"`
    Description string
}

What is gorm.Model? It automatically adds these fields:

  • ID (primary key)
  • CreatedAt
  • UpdatedAt
  • DeletedAt (for soft deletes)

Step 4: Create Migration

Create migrate/migrate.go

package main
 
import (
    "github.com/yourusername/go-crud-api/initializers"
    "github.com/yourusername/go-crud-api/models"
)
 
func init() {
    initializers.LoadEnvVariables()
    initializers.ConnectToDB()
}
 
func main() {
    initializers.DB.AutoMigrate(&models.Category{})
}

Run Migration

go run migrate/migrate.go

This creates the categories table in your database!


Step 5: Create DTOs (Data Transfer Objects)

Create dtos/category_dto.go

package dtos
 
// Request DTOs
type CreateCategoryRequest struct {
    Title       string `json:"title" binding:"required,min=3,max=100"`
    Description string `json:"description" binding:"max=500"`
    Slug        string `json:"slug" binding:"required,min=3,max=100"`
}
 
type UpdateCategoryRequest struct {
    Title       string `json:"title" binding:"omitempty,min=3,max=100"`
    Description string `json:"description" binding:"omitempty,max=500"`
    Slug        string `json:"slug" binding:"omitempty,min=3,max=100"`
}
 
// Response DTOs
type SuccessResponse struct {
    Success bool        `json:"success"`
    Data    interface{} `json:"data,omitempty"`
    Message string      `json:"message,omitempty"`
}
 
type ErrorResponse struct {
    Success bool   `json:"success"`
    Error   string `json:"error"`
}

Step 6: Create Controllers

Create controllers/category_controller.go

package controllers
 
import (
    "errors"
    "strings"
 
    "github.com/yourusername/go-crud-api/dtos"
    "github.com/yourusername/go-crud-api/initializers"
    "github.com/yourusername/go-crud-api/models"
    "github.com/gin-gonic/gin"
    "gorm.io/gorm"
)
 
// CREATE
func CreateCategory(c *gin.Context) {
    var req dtos.CreateCategoryRequest
 
    // Validate request
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(200, dtos.SuccessResponse{
        Success: true,
        Data:    categories,
    })
}
 
// READ ONE
func GetCategory(c *gin.Context) {
    id := c.Param("id")
    var category models.Category
 
    result := initializers.DB.First(&category, id)
 
    if result.Error != nil {
        if errors.Is(result.Error, gorm.ErrRecordNotFound) {
            c.JSON(404, dtos.ErrorResponse{
                Success: false,
                Error:   "Category not found",
            })
            return
        }
 
        c.JSON(500, dtos.ErrorResponse{
            Success: false,
            Error:   "Database error",
        })
        return
    }
 
    c.JSON(200, dtos.SuccessResponse{
        Success: true,
        Data:    category,
    })
}
 
// UPDATE
func UpdateCategory(c *gin.Context) {
    id := c.Param("id")
    var req dtos.UpdateCategoryRequest
 
    // Validate request
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(400, dtos.ErrorResponse{
            Success: false,
            Error:   "Invalid input: " + err.Error(),
        })
        return
    }
 
    // Find existing category
    var category models.Category
    result := initializers.DB.First(&category, id)
 
    if result.Error != nil {
        if errors.Is(result.Error, gorm.ErrRecordNotFound) {
            c.JSON(404, dtos.ErrorResponse{
                Success: false,
                Error:   "Category not found",
            })
            return
        }
 
        c.JSON(500, dtos.ErrorResponse{
            Success: false,
            Error:   "Database error",
        })
        return
    }
 
    // Update fields
    updates := models.Category{
        Title:       req.Title,
        Slug:        req.Slug,
        Description: req.Description,
    }
 
    if err := initializers.DB.Model(&category).Updates(updates).Error; err != nil {
        if strings.Contains(err.Error(), "duplicate") {
            c.JSON(409, dtos.ErrorResponse{
                Success: false,
                Error:   "Category with this slug already exists",
            })
            return
        }
 
        c.JSON(500, dtos.ErrorResponse{
            Success: false,
            Error:   "Failed to update category",
        })
        return
    }
 
    // Fetch updated data
    initializers.DB.First(&category, id)
 
    c.JSON(200, dtos.SuccessResponse{
        Success: true,
        Data:    category,
    })
}
 
// DELETE
func DeleteCategory(c *gin.Context) {
    id := c.Param("id")
 
    // Check if exists
    var category models.Category
    result := initializers.DB.First(&category, id)
 
    if result.Error != nil {
        if errors.Is(result.Error, gorm.ErrRecordNotFound) {
            c.JSON(404, dtos.ErrorResponse{
                Success: false,
                Error:   "Category not found",
            })
            return
        }
 
        c.JSON(500, dtos.ErrorResponse{
            Success: false,
            Error:   "Database error",
        })
        return
    }
 
    // Delete
    if err := initializers.DB.Delete(&category).Error; err != nil {
        c.JSON(500, dtos.ErrorResponse{
            Success: false,
            Error:   "Failed to delete category",
        })
        return
    }
 
    c.JSON(200, dtos.SuccessResponse{
        Success: true,
        Message: "Category deleted successfully",
    })
}

Step 7: Create Routes (main.go)

Create main.go

package main
 
import (
    "github.com/yourusername/go-crud-api/controllers"
    "github.com/yourusername/go-crud-api/initializers"
    "github.com/gin-gonic/gin"
)
 
func init() {
    initializers.LoadEnvVariables()
    initializers.ConnectToDB()
}
 
func main() {
    // Initialize router
    router := gin.Default()
 
    // API v1 group (versioning)
    v1 := router.Group("/api/v1")
    {
        // Category routes
        categories := v1.Group("/categories")
        {
            categories.POST("", controllers.CreateCategory)
            categories.GET("", controllers.ListCategories)
            categories.GET("/:id", controllers.GetCategory)
            categories.PUT("/:id", controllers.UpdateCategory)
            categories.DELETE("/:id", controllers.DeleteCategory)
        }
    }
 
    // Health check
    router.GET("/health", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "status":  "ok",
            "message": "Server is running",
        })
    })
 
    // Start server
    router.Run() // Default :8080
}

Step 8: Run Your API

Development Mode (with hot reload)

CompileDaemon -command="./go-crud-api"

Normal Mode

go run main.go

Build and Run

go build
./go-crud-api

Testing Your API

Using cURL

Create Category:

curl -X POST http://localhost:8080/api/v1/categories \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Electronics",
    "slug": "electronics",
    "description": "Electronic products and gadgets"
  }'

Get All Categories:

curl http://localhost:8080/api/v1/categories

Get Single Category:

curl http://localhost:8080/api/v1/categories/1

Update Category:

curl -X PUT http://localhost:8080/api/v1/categories/1 \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Consumer Electronics",
    "description": "Updated description"
  }'

Delete Category:

curl -X DELETE http://localhost:8080/api/v1/categories/1

Part 4: GORM CRUD Cheatsheet

1. Basic CRUD Operations

Create

// Single record
user := models.User{Name: "John", Age: 25}
result := db.Create(&user)
 
// Check error
if result.Error != nil {
    // Handle error
}
 
// Get ID of created record
fmt.Println(user.ID)
 
// Create multiple records
users := []models.User{
    {Name: "John", Age: 25},
    {Name: "Jane", Age: 30},
}
db.Create(&users)

Read

// Get single record by primary key
var user models.User
db.First(&user, 1) // WHERE id = 1
 
// Get single record with condition
db.First(&user, "name = ?", "John")
 
// Get all records
var users []models.User
db.Find(&users)
 
// Get first record (ordered by primary key)
db.First(&user)
 
// Get last record
db.Last(&user)

Update

// Update single field
db.Model(&user).Update("name", "John Doe")
 
// Update multiple fields (struct)
db.Model(&user).Updates(models.User{
    Name: "John",
    Age:  26,
})
 
// Update multiple fields (map)
db.Model(&user).Updates(map[string]interface{}{
    "name": "John",
    "age":  26,
})
 
// Update all records
db.Model(&models.User{}).Where("age < ?", 18).Update("status", "minor")

Delete

// Delete single record
db.Delete(&user, 1)
 
// Delete with condition
db.Where("age < ?", 18).Delete(&models.User{})
 
// Soft delete (if using gorm.Model)
db.Delete(&user) // Sets DeletedAt
 
// Permanent delete
db.Unscoped().Delete(&user)

2. Query Conditions

Where Clauses

// Simple where
db.Where("name = ?", "John").Find(&users)
 
// Multiple conditions
db.Where("name = ? AND age >= ?", "John", 18).Find(&users)
 
// OR condition
db.Where("name = ?", "John").Or("name = ?", "Jane").Find(&users)
 
// NOT condition
db.Not("name = ?", "John").Find(&users)
 
// IN clause
db.Where("name IN ?", []string{"John", "Jane", "Bob"}).Find(&users)
 
// LIKE
db.Where("name LIKE ?", "%John%").Find(&users)
 
// Between
db.Where("age BETWEEN ? AND ?", 18, 30).Find(&users)

Struct & Map Conditions

// Struct conditions (only non-zero fields)
db.Where(&models.User{Name: "John", Age: 0}).Find(&users)
// WHERE name = 'John' (Age ignored because it's 0)
 
// Map conditions (all fields)
db.Where(map[string]interface{}{
    "name": "John",
    "age":  0,
}).Find(&users)
// WHERE name = 'John' AND age = 0

3. Selecting Specific Columns

// Select specific columns
var users []models.User
db.Select("name", "age").Find(&users)
 
// Select with custom query
db.Select("name, age, DATE(created_at) as date").Find(&users)
 
// Omit columns
db.Omit("password").Find(&users)
 
// Select into custom struct
type Result struct {
    Name string
    Age  int
}
var results []Result
db.Model(&models.User{}).Select("name", "age").Find(&results)

4. Ordering & Limiting

// Order by
db.Order("age desc").Find(&users)
db.Order("age desc, name").Find(&users)
 
// Limit
db.Limit(10).Find(&users)
 
// Offset
db.Offset(5).Limit(10).Find(&users)
 
// Pagination
page := 2
pageSize := 10
db.Offset((page - 1) * pageSize).Limit(pageSize).Find(&users)

5. Aggregation

// Count
var count int64
db.Model(&models.User{}).Where("age > ?", 18).Count(&count)
 
// Sum
var total int
db.Model(&models.Order{}).Select("sum(amount)").Scan(&total)
 
// Average
var avgAge float64
db.Model(&models.User{}).Select("avg(age)").Scan(&avgAge)
 
// Min/Max
var minAge, maxAge int
db.Model(&models.User{}).Select("min(age)").Scan(&minAge)
db.Model(&models.User{}).Select("max(age)").Scan(&maxAge)

6. Relationships

One-to-Many

// Models
type User struct {
    gorm.Model
    Name  string
    Posts []Post // Has many posts
}
 
type Post struct {
    gorm.Model
    Title  string
    UserID uint
    User   User // Belongs to user
}
 
// Create with association
user := User{
    Name: "John",
    Posts: []Post{
        {Title: "Post 1"},
        {Title: "Post 2"},
    },
}
db.Create(&user)
 
// Preload associations
var users []User
db.Preload("Posts").Find(&users)
 
// Find user with posts
var user User
db.Preload("Posts").First(&user, 1)

Many-to-Many

// Models
type User struct {
    gorm.Model
    Name  string
    Roles []Role `gorm:"many2many:user_roles;"`
}
 
type Role struct {
    gorm.Model
    Name  string
    Users []User `gorm:"many2many:user_roles;"`
}
 
// Create with association
user := User{
    Name: "John",
    Roles: []Role{
        {Name: "Admin"},
        {Name: "User"},
    },
}
db.Create(&user)
 
// Preload many-to-many
var users []User
db.Preload("Roles").Find(&users)
 
// Append association
var user User
var role Role
db.First(&user, 1)
db.First(&role, 1)
db.Model(&user).Association("Roles").Append(&role)
 
// Remove association
db.Model(&user).Association("Roles").Delete(&role)
 
// Clear all associations
db.Model(&user).Association("Roles").Clear()

One-to-One

type User struct {
    gorm.Model
    Name    string
    Profile Profile
}
 
type Profile struct {
    gorm.Model
    UserID uint
    Bio    string
}
 
// Create with association
user := User{
    Name: "John",
    Profile: Profile{Bio: "Software Developer"},
}
db.Create(&user)
 
// Preload
var user User
db.Preload("Profile").First(&user, 1)

7. Advanced Model Features

Custom Table Name

type User struct {
    gorm.Model
    Name string
}
 
func (User) TableName() string {
    return "custom_users"
}

Indexes

type User struct {
    gorm.Model
    Name  string `gorm:"index"`                    // Regular index
    Email string `gorm:"uniqueIndex"`              // Unique index
    Code  string `gorm:"index:idx_code,unique"`   // Named unique index
}

Default Values

type User struct {
    gorm.Model
    Name   string
    Status string `gorm:"default:active"`
    Age    int    `gorm:"default:18"`
}

Field Validation

type User struct {
    gorm.Model
    Name  string `gorm:"not null"`
    Email string `gorm:"uniqueIndex;not null"`
    Age   int    `gorm:"check:age >= 18"`
}

JSON Fields

type User struct {
    gorm.Model
    Name     string
    Settings datatypes.JSON
}
 
// Usage
settings := map[string]interface{}{
    "theme": "dark",
    "notifications": true,
}
user := User{
    Name:     "John",
    Settings: datatypes.JSON(settings),
}
db.Create(&user)

8. Transactions

// Manual transaction
tx := db.Begin()
 
user := models.User{Name: "John"}
if err := tx.Create(&user).Error; err != nil {
    tx.Rollback()
    return err
}
 
post := models.Post{Title: "My Post", UserID: user.ID}
if err := tx.Create(&post).Error; err != nil {
    tx.Rollback()
    return err
}
 
tx.Commit()
 
// Transaction with function
err := db.Transaction(func(tx *gorm.DB) error {
    if err := tx.Create(&user).Error; err != nil {
        return err
    }
 
    if err := tx.Create(&post).Error; err != nil {
        return err
    }
 
    return nil
})

9. Raw SQL

// Raw query
type Result struct {
    Name string
    Age  int
}
 
var results []Result
db.Raw("SELECT name, age FROM users WHERE age > ?", 18).Scan(&results)
 
// Execute raw SQL
db.Exec("UPDATE users SET status = ? WHERE age < ?", "minor", 18)
 
// Named arguments
db.Raw(
    "SELECT * FROM users WHERE name = @name AND age > @age",
    sql.Named("name", "John"),
    sql.Named("age", 18),
).Scan(&users)

10. Hooks (Lifecycle Callbacks)

type User struct {
    gorm.Model
    Name string
}
 
// Before create
func (u *User) BeforeCreate(tx *gorm.DB) error {
    // Generate UUID, validate data, etc.
    return nil
}
 
// After create
func (u *User) AfterCreate(tx *gorm.DB) error {
    // Send welcome email, log event, etc.
    return nil
}
 
// Before update
func (u *User) BeforeUpdate(tx *gorm.DB) error {
    return nil
}
 
// After update
func (u *User) AfterUpdate(tx *gorm.DB) error {
    return nil
}
 
// Before delete
func (u *User) BeforeDelete(tx *gorm.DB) error {
    return nil
}
 
// After delete
func (u *User) AfterDelete(tx *gorm.DB) error {
    return nil
}

11. Scopes (Reusable Queries)

// Define scope
func ActiveUsers(db *gorm.DB) *gorm.DB {
    return db.Where("status = ?", "active")
}
 
func AdultUsers(db *gorm.DB) *gorm.DB {
    return db.Where("age >= ?", 18)
}
 
// Use scopes
var users []User
db.Scopes(ActiveUsers, AdultUsers).Find(&users)
 
// Scope with parameters
func AgeGreaterThan(age int) func(db *gorm.DB) *gorm.DB {
    return func(db *gorm.DB) *gorm.DB {
        return db.Where("age > ?", age)
    }
}
 
db.Scopes(AgeGreaterThan(21)).Find(&users)

12. Complex Example with Everything

package models
 
type User struct {
    gorm.Model
    Name     string    `gorm:"not null;index"`
    Email    string    `gorm:"uniqueIndex;not null"`
    Age      int       `gorm:"check:age >= 18"`
    Status   string    `gorm:"default:active"`
    Posts    []Post
    Profile  Profile
    Roles    []Role    `gorm:"many2many:user_roles;"`
}
 
type Post struct {
    gorm.Model
    Title    string    `gorm:"not null"`
    Content  string
    UserID   uint
    User     User
    Tags     []Tag     `gorm:"many2many:post_tags;"`
}
 
type Profile struct {
    gorm.Model
    UserID   uint      `gorm:"uniqueIndex"`
    Bio      string
    Avatar   string
}
 
type Role struct {
    gorm.Model
    Name     string    `gorm:"uniqueIndex;not null"`
    Users    []User    `gorm:"many2many:user_roles;"`
}
 
type Tag struct {
    gorm.Model
    Name     string    `gorm:"uniqueIndex;not null"`
    Posts    []Post    `gorm:"many2many:post_tags;"`
}
 
// Usage
func GetUserWithEverything(db *gorm.DB, id uint) (*User, error) {
    var user User
    result := db.
        Preload("Posts.Tags").
        Preload("Profile").
        Preload("Roles").
        First(&user, id)
 
    if result.Error != nil {
        return nil, result.Error
    }
 
    return &user, nil
}

Summary

You now know:

  • Go fundamentals and syntax
  • Essential Go concepts for API development
  • How to build REST APIs with Gin framework
  • Complete GORM CRUD operations
  • Database relationships and advanced queries
  • Project structure and best practices

🚀 Next Steps:

  • Add authentication (JWT)
  • Add middleware (logging, CORS)
  • Add pagination
  • Add file uploads
  • Deploy your API

**Happy coding! 🎉