Software Engineering
Go for backend beginners: a practical path from zero to a working API
A learning path for Go aimed at backend work: setup, the language features you need, a small HTTP API with PostgreSQL, tests, and the mistakes beginners make.
By Raktim Ranjit · Published · 3 min read
Short answer: install Go, do the official Tour of Go in an evening, then build one small HTTP API that stores data in PostgreSQL, using only the standard library plus a database driver. Write tests as you go. That path covers the language features you will use daily and teaches the habits Go expects.
Why learn Go for backend work?
- It compiles to a single static binary, which makes deployment simple.
- The standard library includes a production-grade HTTP server and JSON handling.
- Concurrency is built in.
- The language is small, and codebases look alike, so reading other people's code is easier.
- Startup time and memory use are low.
How do you set up?
# download from go.dev/dl, then
go version
mkdir hello && cd hello
go mod init example.com/hellogo mod init creates go.mod, which tracks your module and dependencies. There is no separate package manager to learn.
Which language features matter first?
- Types and structs, instead of classes. Methods attach to types.
- Interfaces, satisfied implicitly. If a type has the methods, it implements the interface. Keep them small.
- Errors as values. Functions return
(result, error), and you checkif err != nil. It is repetitive, and it makes failure paths explicit. - Slices and maps.
- `defer`, to run cleanup when a function returns.
- Packages and exported names: capitalised means public.
- Goroutines and channels, later. See concurrency patterns.
What does a small API look like?
package main
import (
"encoding/json"
"log"
"net/http"
)
type Health struct {
Status string `json:"status"`
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(Health{Status: "ok"})
})
mux.HandleFunc("GET /items/{id}", func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
w.Write([]byte("item " + id))
})
log.Println("listening on :8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}Since Go 1.22 the standard router supports methods and path parameters, so you can build a real API without a framework.
How do you talk to PostgreSQL?
Use pgx, the most widely used PostgreSQL driver for Go, with its connection pool.
pool, err := pgxpool.New(ctx, os.Getenv("DATABASE_URL"))
if err != nil { log.Fatal(err) }
defer pool.Close()
var name string
err = pool.QueryRow(ctx, "SELECT name FROM customers WHERE id = $1", id).Scan(&name)
if errors.Is(err, pgx.ErrNoRows) {
http.NotFound(w, r)
return
}Always use placeholders for values. See how to prevent SQL injection. Keep an eye on pool sizing too, covered in connection pooling.
How do you test?
func TestHealth(t *testing.T) {
req := httptest.NewRequest("GET", "/healthz", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != 200 {
t.Fatalf("got %d", rec.Code)
}
}Run go test ./.... Table-driven tests, where a slice of cases is looped over, are the usual style.
Which tools come with Go?
go fmtformats code in one standard way. No style arguments.go vetfinds suspicious constructs.go test -racefinds data races.go buildproduces the binary, with cross-compilation throughGOOSandGOARCH.
What mistakes do beginners make?
- Ignoring returned errors, or using
_for them. - Using a global database handle and global state everywhere.
- Overusing channels where a mutex or plain function call would be clearer.
- Creating interfaces before there are two implementations.
- Writing Java-style layers of abstraction. Prefer small packages and plain functions.
- Forgetting that a nil map panics on write.
When your API works, package it: deploying a Go app with Docker.
References
Author
Raktim Ranjit is a software engineer and the founder of NodeDR Infotech. He builds and maintains the software described here.