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
mainpackages 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 ./...andgo test -race ./...in CI.- A pre-commit
gofmt -w(or use your editor's format-on-save). - Pin tool versions with a
tools.gofile or withgo runinvocations.
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.