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
- Part 0: About Go
- Part 1: Go Fundamentals
- Part 2: Essential Go Concepts Cheatsheet
- Part 3: Building REST API with Gin & GORM
- 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:
- Visit https://golang.org
- Download installer for your OS
- Run installer
- Verify installation:
go versionSetting Up VSCode
Required Extensions:
-
Go Extension (by Go Team at Google)
- Syntax highlighting
- IntelliSense (auto-complete)
- Error detection
- Debugging support
-
Code Runner (Optional)
- Run code with Ctrl+Alt+N
- Quick testing without terminal
Installing Go Tools:
- Open VSCode Command Palette (Ctrl+Shift+P)
- Type "Go: Install/Update Tools"
- Select all tools and click OK
These tools include:
gopls- Go Language Server (like TypeScript Language Server)dlv- Debuggerstaticcheck- 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
mainpackage = entry point of your program- Like your main
index.jsorapp.jsin JavaScript
import "fmt"
- Import packages you need (like
import express from 'express') fmt= "Format" - for printing and formatting text- Similar to
consolein 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.goOption 2: Build then Execute (Production)
# Build executable
go build main.go
# Run it
./main # Mac/Linux
main.exe # WindowsOption 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-nameThis 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
.gofiles) - 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 Type | JavaScript Equivalent | Example |
|---|---|---|
string | string | "Hello" |
int | number | 42 |
float64 | number | 3.14 |
bool | boolean | true |
[]string | string[] | ["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)) // 4JavaScript 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 25JavaScript 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 = 26JavaScript 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 21Why 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:
agehas value25- When you call
tryToModify(age), Go copies the value - Inside the function,
xis a separate variable with its own copy of25 - Changing
xto100only affects that copy - Original
ageremains 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:
agehas value25stored at some memory address (let's say0x123)&agegets the address whereagelives (0x123)- The function receives that address in
x(a pointer) *xmeans "go to that address and change the value there"- Original
ageis 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: 100Visual 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?
- To modify the original value (like we just saw)
- For large structs - Avoid copying large amounts of data
- When you need
nil- Pointers can benil, regular values can't - 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 configWhen 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) { } // CorrectKey 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) // 26Why 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) // 26Summary
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: 25Format Specifiers
| Specifier | Use | Example |
|---|---|---|
%s | String | fmt.Printf("%s", "text") |
%d | Integer | fmt.Printf("%d", 42) |
%f | Float | fmt.Printf("%.2f", 3.14159) |
%t | Boolean | fmt.Printf("%t", true) |
%v | Any value | fmt.Printf("%v", anything) |
%+v | Struct with field names | fmt.Printf("%+v", person) |
%T | Type of value | fmt.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
| Skill | Weather App | URL Shortener | Expense 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:
- Add support for multiple cities at once
- Show sunrise/sunset times
- Add air quality index
- Create a 7-day forecast
URL Shortener:
- Add custom domains support
- Implement QR code generation
- Add expiration dates for URLs
- 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-api2. 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/CompileDaemonProject 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=8080Create 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)CreatedAtUpdatedAtDeletedAt(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.goThis 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.goBuild and Run
go build
./go-crud-apiTesting 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/categoriesGet Single Category:
curl http://localhost:8080/api/v1/categories/1Update 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/1Part 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 = 03. 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! 🎉

