WHY GO?
THE CASE.
The Problem Go Solves
By 2007, Google had millions of lines of C++, Java, and Python. C++ compile times were measured in hours. Java's runtime overhead was significant. Python couldn't handle the concurrency they needed. Rob Pike, Ken Thompson, and Robert Griesemer designed Go to fix all three.
Speed Numbers That Matter
- Compile: a 100k line Go program compiles in ~2 seconds. Same size Java: 30–90 seconds.
- Startup: a Go binary starts in milliseconds. JVM: 500ms–2s warmup.
- Memory: a Go HTTP server idle uses ~5MB. Java Spring Boot: 200MB+.
- Concurrency: 1 million goroutines in ~2GB RAM. 1 million threads: impossible (OS limit).
When to Use Go
- APIs and microservices — standard library has everything, performance is excellent
- Infrastructure tooling — Docker, Kubernetes, Terraform are all Go
- Data pipelines — goroutines make concurrent processing trivial
- CLI tools — single static binary, ships anywhere
- Trading systems — low latency, high throughput, precise memory control
When NOT to Use Go
- Systems programming requiring manual memory — use Rust
- ML / data science notebooks — use Python
- Browser frontends — use TypeScript (or Go + WASM for heavy compute)
- Rapid scripting and glue code — Python or Bash is faster to write
Hello, Production
package main
import (
"fmt"
"net/http"
"log"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "alive")
})
log.Println("listening on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
// Single binary. No Dockerfile needed to test. Compiles in <1s.
}
This is a production-ready HTTP server. No framework, no dependencies beyond stdlib. Build it: go build -o server . && ./server. That's Go's design philosophy — the standard library is enough for most things.
TYPES,
STRUCTS &
INTERFACES
Structs Are Your Data
type Order struct {
ID string
Symbol string
Qty int
Price float64
Side OrderSide // custom type
}
type OrderSide string
const (
Buy OrderSide = "BUY"
Sell OrderSide = "SELL"
)
// Methods attach behaviour to types
func (o Order) Total() float64 {
return float64(o.Qty) * o.Price
}
Interfaces: Implicit Satisfaction
Go interfaces are the most important concept in the language. A type satisfies an interface simply by having the required methods — no declaration needed.
type Executor interface {
Execute(o Order) error
Cancel(id string) error
}
// DhanBroker satisfies Executor without saying so
type DhanBroker struct{ token string }
func (d *DhanBroker) Execute(o Order) error { /* ... */ return nil }
func (d *DhanBroker) Cancel(id string) error { /* ... */ return nil }
// ZerodhaBroker also satisfies Executor — swap without changing calling code
type ZerodhaBroker struct{ apiKey string }
func (z *ZerodhaBroker) Execute(o Order) error { /* ... */ return nil }
func (z *ZerodhaBroker) Cancel(id string) error { /* ... */ return nil }
// This function works with ANY broker — decoupled from implementation
func PlaceOrder(broker Executor, order Order) error {
return broker.Execute(order)
}
Embedding: Composition Over Inheritance
type BaseRepo struct{ db *sql.DB }
func (b *BaseRepo) Begin() (*sql.Tx, error) { return b.db.Begin() }
type OrderRepo struct {
BaseRepo // embedded — gets Begin() for free
}
// o.Begin() works — promoted from BaseRepo
ERROR
HANDLING
The Basic Pattern
func ReadConfig(path string) (Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return Config{}, fmt.Errorf("ReadConfig: %w", err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return Config{}, fmt.Errorf("ReadConfig: parse: %w", err)
}
return cfg, nil
}
Wrapping Errors: %w
Use fmt.Errorf("context: %w", err) to wrap errors with context. Callers can use errors.Is() and errors.As() to inspect the chain.
Sentinel Errors and Custom Types
var ErrNotFound = errors.New("not found")
var ErrUnauthorized = errors.New("unauthorized")
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation: %s: %s", e.Field, e.Message)
}
// Caller can detect specific types:
var ve *ValidationError
if errors.As(err, &ve) {
// handle validation error specifically
}
GOROUTINES
& CHANNELS
Goroutines: Start Cheap
func fetchPrices(symbols []string) []float64 {
results := make([]float64, len(symbols))
var wg sync.WaitGroup
for i, sym := range symbols {
wg.Add(1)
go func(i int, sym string) { // capture i and sym by value
defer wg.Done()
results[i] = getPrice(sym) // safe: each i is unique
}(i, sym)
}
wg.Wait()
return results
}
Channels: Typed Pipes
func pipeline() {
jobs := make(chan string, 100)
done := make(chan struct{})
// producer
go func() {
for _, sym := range symbols { jobs <- sym }
close(jobs)
}()
// 5 workers
for i := 0; i < 5; i++ {
go func() {
for job := range jobs {
process(job)
}
}()
}
}
SYNC,
CONTEXT &
PATTERNS
Context: Cancellation Tree
func handleRequest(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel() // always defer cancel to avoid leak
result, err := fetchData(ctx)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
http.Error(w, "timeout", 504)
}
return
}
json.NewEncoder(w).Encode(result)
}
sync.Once: Singleton Init
var (
instance *DB
once sync.Once
)
func GetDB() *DB {
once.Do(func() {
instance = connect() // runs exactly once
})
return instance
}
BUILD:
HTTP SERVER
FROM SCRATCH
Start Here: The Listener
package main
import "net"
func main() {
l, _ := net.Listen("tcp", ":4221")
defer l.Close()
for {
conn, _ := l.Accept()
go handle(conn) // each connection gets its own goroutine
}
}
func handle(conn net.Conn) {
defer conn.Close()
buf := make([]byte, 4096)
n, _ := conn.Read(buf)
req := parseRequest(buf[:n])
respond(conn, req)
}
BUILD:
TCP ECHO
SERVER
type Hub struct {
clients map[net.Conn]struct{}
broadcast chan string
join chan net.Conn
leave chan net.Conn
}
func (h *Hub) Run() {
for {
select {
case c := <-h.join:
h.clients[c] = struct{}{}
case c := <-h.leave:
delete(h.clients, c)
case msg := <-h.broadcast:
for c := range h.clients {
fmt.Fprintln(c, msg)
}
}
}
}
BUILD:
REDIS CLONE
type Store struct {
mu sync.RWMutex
strings map[string]string
expiry map[string]time.Time
}
func (s *Store) Get(key string) (string, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
if exp, ok := s.expiry[key]; ok && time.Now().After(exp) {
return "", false // expired
}
v, ok := s.strings[key]
return v, ok
}
BUILD:
gRPC SERVICE
// signal.proto
service SignalService {
rpc StreamSignals(SignalRequest) returns (stream Signal);
}
// Go implementation
func (s *Server) StreamSignals(req *pb.SignalRequest, stream pb.SignalService_StreamSignalsServer) error {
for {
select {
case <-stream.Context().Done():
return nil
case sig := <-s.signals:
stream.Send(sig)
}
}
}
BUILD:
SQL QUERY
ENGINE
PERFORMANCE
& PROFILING
Benchmarks First
func BenchmarkJSON(b *testing.B) {
order := Order{ID: "123", Symbol: "NIFTY", Qty: 50}
b.ResetTimer()
for i := 0; i < b.N; i++ {
json.Marshal(order)
}
}
// go test -bench=. -benchmem -cpuprofile=cpu.out
// go tool pprof cpu.out
Common Optimizations
- Pre-allocate slices with known capacity: make([]T, 0, n) avoids repeated doubling
- Reuse buffers with sync.Pool — avoid GC pressure in hot paths
- Use sync.RWMutex for read-heavy data — multiple concurrent readers
- Avoid string concatenation in loops — use strings.Builder
- Profile before optimizing — bottlenecks are never where you expect
DEPLOY:
DOCKER
& KUBERNETES
Multi-Stage Dockerfile
# Stage 1: build
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o server .
# Stage 2: minimal runtime image (~5MB total)
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]
Kubernetes Deployment
apiVersion: apps/v1
kind: Deployment
spec:
replicas: 3
template:
spec:
containers:
- name: api
image: ghcr.io/servloci/api:latest
resources:
requests: { cpu: "100m", memory: "32Mi" }
limits: { cpu: "500m", memory: "128Mi" }
livenessProbe:
httpGet: { path: /health, port: 8080 }