Go
Backend

Go Programming Language: Getting Started

Mayur Dabhi
Mayur Dabhi
May 29, 2026
14 min read

Go, often called Golang, is an open-source programming language created at Google in 2009 by Robert Griesemer, Rob Pike, and Ken Thompson. Designed to address the scalability and developer-experience shortcomings of C++ and Java for large distributed systems, Go has rapidly become one of the most sought-after languages in backend development. Its combination of static typing, garbage collection, lightning-fast compilation, and first-class concurrency primitives makes it uniquely suited for building cloud-native services, APIs, and system tools — all while remaining refreshingly simple to learn.

Who Uses Go?

Go powers some of the most critical infrastructure in the world. Docker and Kubernetes are written entirely in Go. Terraform, Hugo, Caddy, Prometheus, and CockroachDB are all Go projects. Cloudflare, Dropbox, Uber, and Netflix rely on Go for performance-critical services.

Why Learn Go?

In a world full of programming languages, Go occupies a unique sweet spot. It compiles to a statically-linked native binary with no runtime dependencies — meaning you can deploy a Go program as a single file to any Linux server and it just works. No Node.js installation required, no JVM version mismatches, no Python virtual environments.

Here's what sets Go apart from the crowd:

Feature Go Python Node.js
Execution Compiled (native binary) Interpreted JIT (V8 engine)
Performance Near-C speed Moderate Good
Concurrency Model Goroutines + Channels GIL limits true threading Single-threaded event loop
Memory Usage Very low High Moderate
Deployment Single static binary Requires Python runtime Requires Node.js
Type System Static, inferred Dynamic Dynamic (TypeScript opt-in)
Learning Curve Low-Moderate Very Low Low

Installation and Your First Program

Installing Go is straightforward. The official distribution provides prebuilt binaries for all major platforms.

1

Download and Install Go

Visit go.dev/dl and download the installer for your OS. On macOS/Linux you can also use a package manager.

Terminal — package manager install
# macOS (Homebrew)
brew install go

# Ubuntu/Debian
sudo apt install golang-go

# Arch Linux
sudo pacman -S go

# Verify installation
go version
# go version go1.22.3 linux/amd64
2

Create a New Module

Go uses modules (introduced in Go 1.11) for dependency management. Every project starts with go mod init.

Terminal
mkdir hello-go && cd hello-go
go mod init github.com/yourname/hello-go
3

Write and Run Your First Program

Every Go program lives in a package. The executable entry point is always package main with a main() function.

main.go
package main

import "fmt"

func main() {
    fmt.Println("Hello, Go!")
    fmt.Printf("Version: %s\n", "1.22")
}
Terminal
# Run directly (compile + execute)
go run main.go

# Build a binary
go build -o hello-go .
./hello-go

# Output:
# Hello, Go!
# Version: 1.22
Go Modules vs GOPATH

Modern Go (1.11+) uses Go Modules — you can create projects anywhere on your filesystem, not just under a special GOPATH directory. The go.mod file tracks your module name and dependencies, similar to package.json in Node.js or composer.json in PHP.

Core Language Features

Variables, Types, and Constants

Go is statically typed — every variable has a type known at compile time. The compiler infers types from assigned values using the := short declaration operator, so you rarely need to write explicit types.

variables.go
package main

import "fmt"

func main() {
    // Explicit type declaration
    var name string = "Gopher"
    var age  int    = 5

    // Short declaration (type inferred)
    language := "Go"
    version  := 1.22

    // Multiple assignment
    x, y := 10, 20
    x, y = y, x   // swap — no temp variable needed

    // Constants — evaluated at compile time
    const Pi      = 3.14159
    const MaxConn = 100

    // Zero values — Go initializes all variables
    var count   int     // 0
    var enabled bool    // false
    var message string  // ""

    fmt.Printf("%s %s %.2f — age %d\n", language, name, version, age)
    fmt.Printf("x=%d y=%d count=%d enabled=%v msg=%q\n",
        x, y, count, enabled, message)
}

Functions and Multiple Return Values

One of Go's most practical features is that functions can return multiple values. This is the idiomatic way to return both a result and an error — no exceptions, no try/catch blocks, just explicit error handling that forces you to deal with failure cases.

functions.go
package main

import (
    "errors"
    "fmt"
    "math"
)

// Multiple return values: result + error
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("cannot divide by zero")
    }
    return a / b, nil
}

// Named return values (useful for documentation)
func circleMetrics(r float64) (area, circumference float64) {
    area = math.Pi * r * r
    circumference = 2 * math.Pi * r
    return   // "naked" return uses named values
}

// Variadic function — accepts any number of int arguments
func sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}

func main() {
    result, err := divide(10, 3)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Printf("10 / 3 = %.4f\n", result)  // 3.3333

    area, circ := circleMetrics(5)
    fmt.Printf("Circle r=5: area=%.2f circ=%.2f\n", area, circ)

    fmt.Println("Sum:", sum(1, 2, 3, 4, 5))  // 15
}

Structs and Interfaces

Go achieves object-oriented programming through structs (data) and interfaces (behavior) — without inheritance or class hierarchies. Interface satisfaction is implicit: if a type has all the methods an interface requires, it satisfies that interface automatically. No implements keyword needed.

structs.go
package main

import "fmt"

// Struct definition
type User struct {
    ID       int
    Name     string
    Email    string
    IsActive bool
}

// Method with pointer receiver (can modify the struct)
func (u *User) Deactivate() {
    u.IsActive = false
}

// Method with value receiver (read-only)
func (u User) String() string {
    status := "active"
    if !u.IsActive {
        status = "inactive"
    }
    return fmt.Sprintf("User{id=%d name=%q status=%s}", u.ID, u.Name, status)
}

// Constructor function pattern
func NewUser(id int, name, email string) *User {
    return &User{
        ID:       id,
        Name:     name,
        Email:    email,
        IsActive: true,
    }
}

// Interface — satisfied implicitly by any type with these methods
type Notifier interface {
    Notify(message string) error
}

type EmailNotifier struct {
    SMTPHost string
}

// EmailNotifier satisfies Notifier without declaring "implements"
func (e EmailNotifier) Notify(message string) error {
    fmt.Printf("[Email via %s] %s\n", e.SMTPHost, message)
    return nil
}

func sendAlert(n Notifier, msg string) {
    if err := n.Notify(msg); err != nil {
        fmt.Println("Notification failed:", err)
    }
}

func main() {
    u := NewUser(1, "Alice", "alice@example.com")
    fmt.Println(u)  // User{id=1 name="Alice" status=active}

    u.Deactivate()
    fmt.Println(u)  // User{id=1 name="Alice" status=inactive}

    notifier := EmailNotifier{SMTPHost: "smtp.example.com"}
    sendAlert(notifier, "Server memory above 90%")
}

Concurrency: Go's Superpower

Concurrency is where Go truly shines. A goroutine is a function that runs concurrently with other goroutines in the same address space. Unlike OS threads (which cost ~1MB of stack memory each), goroutines start with just ~2KB of stack and are multiplexed onto real OS threads by Go's runtime scheduler. This means you can comfortably spawn hundreds of thousands of goroutines in a single program.

Channels are Go's mechanism for goroutines to communicate with each other safely — following the principle: "Do not communicate by sharing memory; share memory by communicating."

main() Main Goroutine goroutine 1 go fetchData(url1, ch) goroutine 2 go fetchData(url2, ch) goroutine 3 go fetchData(url3, ch) channel ch (buffered, cap 3) go go go ch <- result ch <- result ch <- result

Goroutines send results to a shared channel; main() reads them in order

goroutines.go — concurrent API calls
package main

import (
    "fmt"
    "time"
)

func fetchData(url string, ch chan<- string) {
    // Simulates an HTTP call
    time.Sleep(100 * time.Millisecond)
    ch <- fmt.Sprintf("response from %s", url)
}

func main() {
    urls := []string{
        "https://api.service-a.com/users",
        "https://api.service-b.com/orders",
        "https://api.service-c.com/products",
    }

    // Buffered channel — holds up to 3 messages without blocking
    ch := make(chan string, len(urls))

    // Launch all requests concurrently
    for _, url := range urls {
        go fetchData(url, ch)
    }

    // Collect all results (~100ms total, not ~300ms sequential)
    for range urls {
        fmt.Println(<-ch)
    }
}

Synchronizing with WaitGroups

When you need to wait for a group of goroutines to finish without collecting return values through a channel, sync.WaitGroup is the idiomatic choice.

waitgroup.go
package main

import (
    "fmt"
    "sync"
    "time"
)

func processItem(id int, wg *sync.WaitGroup) {
    defer wg.Done()  // signal completion even if function panics
    time.Sleep(50 * time.Millisecond)
    fmt.Printf("Processed item %d\n", id)
}

func main() {
    var wg sync.WaitGroup
    items := []int{1, 2, 3, 4, 5}

    for _, id := range items {
        wg.Add(1)          // increment counter before launching
        go processItem(id, &wg)
    }

    wg.Wait()  // block until all Done() calls bring counter to 0
    fmt.Println("All items processed")
}

The select Statement

The select statement lets a goroutine wait on multiple channel operations simultaneously — whichever is ready first gets executed. It's the Go equivalent of a non-blocking I/O multiplexer.

select.go — timeout pattern
package main

import (
    "fmt"
    "time"
)

func slowOperation(ch chan string) {
    time.Sleep(2 * time.Second)
    ch <- "operation complete"
}

func main() {
    ch := make(chan string)
    go slowOperation(ch)

    // Wait for result OR timeout after 1 second
    select {
    case result := <-ch:
        fmt.Println("Got:", result)
    case <-time.After(1 * time.Second):
        fmt.Println("Operation timed out!")
    }
}
Goroutine Leaks

A goroutine that is blocked forever waiting on a channel is a goroutine leak — it consumes memory and never gets garbage collected. Always ensure every goroutine you launch has a clear exit path. Use context.WithCancel or context.WithTimeout to propagate cancellation signals to goroutines that perform long-running or I/O-bound work.

Building a REST API

Go's standard library includes a production-ready HTTP server in net/http. For many microservices and internal APIs, you don't need any third-party framework at all. When you do want routing, middleware chains, or parameter binding, lightweight frameworks like Gin or Chi add minimal overhead.

JSON Handling and Struct Tags

Go uses struct tags to control JSON (de)serialization. The encoding/json package uses reflection to read these tags at runtime.

models.go
package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    ID        int    `json:"id"`
    Name      string `json:"name"`
    Email     string `json:"email"`
    Password  string `json:"-"`          // omit from JSON output
    CreatedAt string `json:"created_at,omitempty"` // omit if empty
}

func main() {
    u := User{ID: 1, Name: "Alice", Email: "alice@example.com", Password: "secret"}

    // Marshal (struct -> JSON)
    data, _ := json.Marshal(u)
    fmt.Println(string(data))
    // {"id":1,"name":"Alice","email":"alice@example.com"}
    // Note: Password is omitted, CreatedAt is omitted (empty)

    // Unmarshal (JSON -> struct)
    payload := `{"id":2,"name":"Bob","email":"bob@example.com"}`
    var newUser User
    json.Unmarshal([]byte(payload), &newUser)
    fmt.Printf("Decoded: %+v\n", newUser)
}

Complete REST API Example

Here's a fully working in-memory REST API for a users resource using only the standard library. It handles GET /users, POST /users, and GET /users/{id}.

main.go — REST API with net/http
package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "strconv"
    "strings"
    "sync"
)

type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

// Thread-safe in-memory store
var (
    mu     sync.RWMutex
    users  = []User{
        {ID: 1, Name: "Alice Johnson", Email: "alice@example.com"},
        {ID: 2, Name: "Bob Smith",     Email: "bob@example.com"},
    }
    nextID = 3
)

func respondJSON(w http.ResponseWriter, status int, v any) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(v)
}

// GET /users        — list all users
// POST /users       — create user
func usersHandler(w http.ResponseWriter, r *http.Request) {
    switch r.Method {
    case http.MethodGet:
        mu.RLock()
        defer mu.RUnlock()
        respondJSON(w, http.StatusOK, users)

    case http.MethodPost:
        var u User
        if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
            respondJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
            return
        }
        mu.Lock()
        u.ID = nextID
        nextID++
        users = append(users, u)
        mu.Unlock()
        respondJSON(w, http.StatusCreated, u)

    default:
        respondJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"})
    }
}

// GET /users/{id}   — get single user by ID
func userHandler(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodGet {
        respondJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"})
        return
    }
    idStr := strings.TrimPrefix(r.URL.Path, "/users/")
    id, err := strconv.Atoi(idStr)
    if err != nil || id < 1 {
        respondJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid id"})
        return
    }
    mu.RLock()
    defer mu.RUnlock()
    for _, u := range users {
        if u.ID == id {
            respondJSON(w, http.StatusOK, u)
            return
        }
    }
    respondJSON(w, http.StatusNotFound, map[string]string{"error": "user not found"})
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/users", usersHandler)
    mux.HandleFunc("/users/", userHandler)

    fmt.Println("Server running at http://localhost:8080")
    log.Fatal(http.ListenAndServe(":8080", mux))
}
Terminal — testing the API with curl
# List all users
curl http://localhost:8080/users
# [{"id":1,"name":"Alice Johnson","email":"alice@example.com"},...]

# Get user by ID
curl http://localhost:8080/users/1
# {"id":1,"name":"Alice Johnson","email":"alice@example.com"}

# Create a new user
curl -X POST http://localhost:8080/users \
  -H "Content-Type: application/json" \
  -d '{"name":"Carol White","email":"carol@example.com"}'
# {"id":3,"name":"Carol White","email":"carol@example.com"}

Choosing a Web Framework

The standard library is powerful enough for most microservices, but popular frameworks add automatic parameter binding, middleware pipelines, and richer routing.

Framework GitHub Stars Style Best For
net/http (stdlib) Built-in Minimal, explicit Microservices, learning Go
Gin 78k+ Express-like, fast REST APIs, high-throughput services
Echo 29k+ Middleware-centric Complex middleware chains
Fiber 33k+ Express-inspired, very fast High-performance edge APIs
Chi 17k+ Idiomatic Go, stdlib-compatible Teams that value Go conventions

Essential Go CLI Commands

CommandPurpose
go run main.goCompile and run a program
go build -o app .Build a binary
go test ./...Run all tests recursively
go test -race ./...Run tests with race detector
go get github.com/gin-gonic/ginAdd a dependency
go mod tidyRemove unused dependencies
go fmt ./...Format all Go source files
go vet ./...Report suspicious code patterns
GOOS=linux go build .Cross-compile for Linux
go doc fmt.PrintlnView documentation offline

Next Steps

You now understand Go's core pillars: static typing with inference, multiple return values for explicit error handling, structs and interfaces for flexible composition, goroutines for cheap concurrency, and a standard library capable of powering production HTTP services. This foundation covers the majority of day-to-day Go development.

Continue Your Go Journey

  • Error Wrapping: Use fmt.Errorf("context: %w", err) and errors.Is / errors.As for structured error handling
  • Context Package: Learn context.Context for request-scoped cancellation and timeouts across goroutine boundaries
  • Database/SQL: Use database/sql with lib/pq (PostgreSQL) or go-sql-driver/mysql for type-safe database access
  • Testing: Go has built-in testing with go test — write table-driven tests using testing.T and benchmarks with testing.B
  • Generics: Go 1.18+ supports type parameters — learn them for writing reusable data structures and algorithms
  • gRPC: Build high-performance service-to-service communication with Protocol Buffers and Go's google.golang.org/grpc package
"Go is an open source programming language that makes it easy to build simple, reliable, and efficient software."
— golang.org

Go rewards simplicity. Resist the urge to over-engineer — idiomatic Go code is flat, explicit, and direct. As your programs grow, the language's static analysis tools (go vet, staticcheck), the race detector (go test -race), and the pprof profiler give you the confidence to scale without surprises. Start with a small CLI or API, and let Go's pragmatic design guide you toward writing fast, maintainable software.

Go Golang Backend Concurrency REST API Goroutines
Mayur Dabhi

Mayur Dabhi

Full Stack Developer with 5+ years of experience building scalable web applications with Laravel, React, and Node.js.