A grab bag of the topics you need most often in real services: HTTP, JSON, logging, configuration, structured project layout, and a few production-ready patterns.

A simple HTTP server with net/http

package main

import (
    "encoding/json"
    "log"
    "net/http"
)

type pingResp struct {
    Status string `json:"status"`
    Msg    string `json:"msg,omitempty"`
}

func ping(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(pingResp{Status: "ok"})
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/ping", ping)

    addr := ":8080"
    log.Println("listening on", addr)
    log.Fatal(http.ListenAndServe(addr, mux))
}

Since Go 1.22 the default mux supports method+pattern routing:

mux.HandleFunc("GET /users/{id}", getUser)

For larger services people use chi or gin, but the standard library is production-quality.

Middleware as func(http.Handler) http.Handler

func logger(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r)
        log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start))
    })
}

handler := logger(mux)
http.ListenAndServe(":8080", handler)

Compose middlewares by wrapping; recovery, auth, CORS, request IDs all fit this shape.

JSON encoding/decoding

type User struct {
    ID    int       `json:"id"`
    Name  string    `json:"name"`
    Email string    `json:"email,omitempty"`
    Born  time.Time `json:"born"`
    Hidden string   `json:"-"`
}

// Encode
b, _ := json.Marshal(u)              // []byte
_ = json.NewEncoder(w).Encode(u)     // straight to a writer

// Decode
var u User
_ = json.Unmarshal(data, &u)
_ = json.NewDecoder(r.Body).Decode(&u)

Useful struct tag flags: json:"name,omitempty", json:"-" (skip), json:",string" (encode numbers as strings).

For dynamic JSON: map[string]any, []any, or json.RawMessage.

Structured logging with log/slog

Available since Go 1.21:

import "log/slog"

logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
logger.Info("user logged in",
    slog.String("user", "alice"),
    slog.Int("attempts", 3),
)

Output:

{"time":"...","level":"INFO","msg":"user logged in","user":"alice","attempts":3}

Set as default with slog.SetDefault(logger).

Configuration: flags + environment

addr := flag.String("addr", ":8080", "listen address")
flag.Parse()

if v := os.Getenv("ADDR"); v != "" {
    *addr = v
}

The standard library's flag package handles CLI flags. For richer config people use Viper or envconfig.

Project layout

There is no official layout, but a common one for a service is:

my-service/
├── cmd/
│   └── server/
│       └── main.go            # tiny: parse flags, build deps, start
├── internal/
│   ├── http/
│   │   ├── handlers.go
│   │   ├── middleware.go
│   │   └── routes.go
│   ├── store/
│   │   ├── postgres.go
│   │   └── store.go
│   └── domain/
│       └── user.go
├── pkg/                        # OPTIONAL: code we expose to others
├── api/                        # OpenAPI / proto definitions
├── go.mod
└── go.sum

Notes:

  • Keep main packages tiny — they wire dependencies and start things.
  • Put real code under internal/ so other modules can't import it.
  • Avoid mega-packages. Many small packages around domain concepts (user, order, payment) are easier to test.

Graceful shutdown

srv := &http.Server{Addr: ":8080", Handler: handler}
go func() {
    if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
        log.Fatal(err)
    }
}()

stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
<-stop

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = srv.Shutdown(ctx)

This pattern shows up in nearly every Go service.

Database access (briefly)

The standard database/sql API is generic over drivers. Pick the driver:

import (
    "database/sql"
    _ "github.com/jackc/pgx/v5/stdlib"   // postgres
)

db, err := sql.Open("pgx", "postgres://user:pass@host/db?sslmode=disable")
if err != nil { ... }
defer db.Close()

ctx := context.Background()
rows, err := db.QueryContext(ctx, "SELECT id, name FROM users WHERE id = $1", 42)

For higher-level SQL many teams use sqlc (codegen) or pgx directly.

Don't forget

  • go vet ./... and go test -race ./... in CI.
  • A pre-commit gofmt -w (or use your editor's format-on-save).
  • Pin tool versions with a tools.go file or with go run invocations.

Run the example

go run ./part1-language/examples/13_practical
# in another terminal:
curl http://localhost:8080/ping
curl 'http://localhost:8080/echo?msg=hello'

Press Ctrl+C to trigger the graceful shutdown path.