If Python feels comfortable but the wrong tool for some of your backends, Go is the language to learn next. It compiles to a single static binary, has a runtime that handles concurrency really well, and is small enough to read the spec in an afternoon. The Go ecosystem powers Docker, Kubernetes, Terraform, Caddy, and most of the modern cloud-native stack.
This post is the introduction I wish I’d had when I started Go — written for someone who already knows backend development in some language. We’ll cover what makes Go different, the syntax that matters, and how to ship your first HTTP server.
Why Go for backends
The pitch in one sentence: Go gives you Python-level productivity with C-level performance and a runtime that takes concurrency seriously.
Concretely:
- Single static binary. No runtime to install on the server, no
pip install, no Docker base image gymnastics.scpit and run it. - Fast startup. A Go HTTP server starts in milliseconds. Great for serverless and quick CI tests.
- Goroutines. Lightweight concurrency primitives that make “fan out 1000 things” cheap. We’ll cover them in a dedicated post .
- Strong stdlib.
net/http,encoding/json,database/sql,crypto/*— you can ship a real web service without any third-party packages. - Boring syntax. That’s a feature. There’s basically one way to write a
forloop. Code from one Go shop reads like code from another.
It’s not perfect. Generics are still relatively new (Go 1.18+), error handling is verbose, and the language deliberately doesn’t have features Python developers might miss (no list comprehensions, no decorators, no metaclasses). The tradeoff is consistency and simplicity.
Install
# macOS
brew install go
# Or download from https://go.dev/dl/
Verify:
go version
# go version go1.23.x darwin/arm64
Hello, world
// hello.go
package main
import "fmt"
func main() {
fmt.Println("Hello, Go!")
}
Run:
go run hello.go
# → Hello, Go!
Or build a binary:
go build hello.go
./hello
A working HTTP server in 15 lines
Here’s the killer feature for backend devs. The standard library alone gives you a real web server:
package main
import (
"encoding/json"
"log"
"net/http"
)
func main() {
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
})
log.Println("listening on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Run it:
go run main.go
# In another terminal:
curl localhost:8080/health
# → {"status":"ok"}
That’s a complete, production-shaped HTTP server. No framework. No pip install. No virtualenv. We’ll go deeper on this in Building a REST API with net/http
.
Your first Go module
Real projects use modules:
mkdir tasks-api && cd tasks-api
go mod init github.com/AlzyWelzy/tasks-api
go mod init creates a go.mod file — Go’s equivalent of package.json or pyproject.toml. Add a third-party dependency and Go updates go.mod and go.sum (the lock file) automatically:
go get github.com/google/uuid
Use it:
import "github.com/google/uuid"
id := uuid.New().String()
Syntax basics for someone fluent in Python
Variables and types
// Explicit type
var name string = "Manvendra"
// Inferred type (much more common)
name := "Manvendra"
age := 30
isActive := true
// Multiple at once
x, y := 1, 2
:= is the “declare and assign” operator. Use it inside functions; use var at package level.
Functions
func add(a, b int) int {
return a + b
}
// Multiple return values — Go's signature feature
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("divide by zero")
}
return a / b, nil
}
result, err := divide(10, 2)
if err != nil {
log.Fatal(err)
}
The (value, error) pattern is everywhere in Go. Get used to it.
Structs (kind of like dataclasses)
type User struct {
ID int
Name string
Email string
}
u := User{ID: 1, Name: "Alzy", Email: "[email protected]"}
fmt.Println(u.Name) // Alzy
Structs don’t have inheritance — Go uses composition instead. If you want shared behavior, embed types:
type Timestamps struct {
CreatedAt time.Time
UpdatedAt time.Time
}
type Post struct {
Timestamps // embedded — Post now has CreatedAt and UpdatedAt
ID int
Title string
}
Methods
Methods are functions with a receiver:
func (u User) Greet() string {
return "Hello, " + u.Name
}
u.Greet() // "Hello, Alzy"
Pointer receivers (*User) let you mutate the struct:
func (u *User) SetEmail(email string) {
u.Email = email
}
Rule of thumb: if the method modifies state or the struct is large, use a pointer receiver.
Interfaces (duck typing, but typed)
This is where Go shines. An interface is a set of method signatures; any type that implements them satisfies the interface — implicitly:
type Greeter interface {
Greet() string
}
func WelcomeAll(greeters []Greeter) {
for _, g := range greeters {
fmt.Println(g.Greet())
}
}
// User has a Greet() method, so it implicitly satisfies Greeter.
WelcomeAll([]Greeter{User{Name: "Alzy"}, User{Name: "Manvendra"}})
No implements keyword. You don’t even have to know about the interface when you write the type. This is the heart of idiomatic Go.
Error handling
There’s no try/except. Errors are values:
data, err := os.ReadFile("config.json")
if err != nil {
return fmt.Errorf("read config: %w", err)
}
This is verbose, and that’s the point — it forces you to think about every error at the call site. The %w verb in fmt.Errorf wraps the original error so the caller can unwrap it later.
Slices and maps
// Slice (dynamic array)
nums := []int{1, 2, 3}
nums = append(nums, 4)
fmt.Println(len(nums)) // 4
// Map
ages := map[string]int{"alzy": 30, "rajesh": 28}
ages["sam"] = 25
delete(ages, "rajesh")
// Iterate
for name, age := range ages {
fmt.Printf("%s is %d\n", name, age)
}
defer
defer schedules a function call to run when the surrounding function returns. Perfect for cleanup:
func processFile(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // runs when processFile returns, regardless of how
// do stuff with f
return nil
}
This is Go’s answer to Python’s with statement.
Tooling worth knowing
go run main.go # compile & run
go build # compile to a binary
go test ./... # run all tests recursively
go fmt ./... # auto-format the code
go vet ./... # static analysis
go mod tidy # clean up go.mod and download missing deps
gofmt is non-negotiable. There is one correct way to format Go code, and gofmt does it. No bikeshedding about tabs vs spaces; the tool decided.
For real projects, also install:
golangci-lint— runs ~30 linters in one pass. Configure in.golangci.ymland add to CI.air— live reload for development (go install github.com/air-verse/air@latest).delve(dlv) — debugger.
A tiny but real project structure
tasks-api/
├── go.mod
├── go.sum
├── cmd/
│ └── server/
│ └── main.go # entry point
├── internal/
│ ├── api/ # HTTP handlers
│ ├── store/ # database layer
│ └── domain/ # business logic
└── pkg/ # exported reusable code (only if needed)
The cmd/<name>/ convention lets one repo build multiple binaries. internal/ is special — Go enforces that nothing outside this module can import it. Use it for everything not meant to be reused externally.
What to learn next
net/httpdeeply — read the docs, write a handler from scratch, understandhttp.Handlerand middleware. See Building a REST API with net/http .- Goroutines and channels — Go’s superpower, easy to get wrong if you treat them like threads. See Go Concurrency: Goroutines, Channels, and
sync. - A web framework or two — Gin, Echo, Chi. See Building APIs with Gin, Echo, and Chi .
database/sql+sqlcorsqlx— for typed database access without an ORM.- Effective Go — the official style guide. Read it once a year.
Common stumbling blocks for Python developers
- No exceptions. Returns
(value, error). Get used to it. - No list comprehensions, decorators, or metaclasses. Go is deliberately minimal.
- Tabs vs. spaces? Tabs.
gofmtdoes it. Stop arguing. - Public/private isn’t with
_. Capitalized names are exported (public); lowercase are package-private. - Generics exist but used sparingly. Most Go code is concrete types. Don’t over-genericize.
nilis everywhere.nilslices, maps, and pointers all behave slightly differently. Learn the differences.
Conclusion
Go is one of those languages that punishes the first week and rewards the next decade. The boring syntax, the strict formatting, the verbose error handling — they all feel like friction at first. Then you ship a service, deploy it as one binary, and watch it run forever on a $5 VPS while your laptop fan stays quiet.
If you write Python, learning Go isn’t replacing it — it’s adding a tool that’s right for different jobs. Both belong in a serious backend engineer’s toolkit in 2026.
Happy coding!
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .