golang-engineering-lab is a compact net/http service. Rather than pretending it is a concurrency showcase, this post looks at its package boundaries, context usage, middleware, and database access.

A small Go backend lab: the useful parts are the boring ones


golang-engineering-lab is a small backend service, not a giant catalogue of concurrency tricks. That makes it useful in a different way. The repository shows the basic pieces you need for a Go HTTP service: config loading, database connection, handlers, middleware, services, and a main.go that wires them together.

Small repos are good for reading because there is nowhere for structure to hide.

main.go owns wiring

The entry point loads config, connects to Postgres, builds services, builds handlers, registers routes, and starts the server:

cfg := config.Load()

database, err := db.Connect(cfg.DBConn)
if err != nil {
	log.Fatal(err)
}
defer database.Close()

userSvc := service.NewUserService(database)
authSvc := service.NewAuthService(cfg.JWTSecret)

mux := http.NewServeMux()
mux.HandleFunc("POST /login", authHandler.Login)
mux.Handle("GET /users", middleware.Auth(authSvc)(http.HandlerFunc(userHandler.List)))

That is the right amount of ceremony for a learning service. main composes packages but does not contain SQL, token validation, or JSON response logic.

Configuration stays simple

The config package reads environment variables with fallbacks:

type Config struct {
	Port      string
	DBConn    string
	JWTSecret string
}

func Load() *Config {
	return &Config{
		Port:      getEnv("PORT", "8080"),
		DBConn:    getEnv("DB_CONN", "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable"),
		JWTSecret: getEnv("JWT_SECRET", "dev-secret"),
	}
}

For production you would want stricter validation and no insecure default secret. For a lab, this keeps the moving parts visible.

Database connection uses a timeout

The Postgres package opens a database/sql connection and pings it with a bounded context:

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

if err := db.PingContext(ctx); err != nil {
	return nil, err
}

This is a small habit worth keeping. Startup should fail clearly when the database is unavailable. A hung Ping during deploy is not useful.

Services take context from handlers

The user service accepts a caller-provided context, then derives its own timeout:

func (s *UserService) List(ctx context.Context) ([]User, error) {
	ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
	defer cancel()
	rows, err := s.db.QueryContext(ctx, `SELECT id, email FROM users`)
	// ...
}

That preserves request cancellation. If the client disconnects, the handler’s context can still propagate into the database call. The service adds a deadline so a slow query does not sit forever.

Middleware is just function composition

The auth middleware returns a function that wraps an http.Handler:

func Auth(auth *service.AuthService) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			token := r.Header.Get("Authorization")
			if err := auth.ValidateToken(token); err != nil {
				http.Error(w, "unauthorized", http.StatusUnauthorized)
				return
			}
			next.ServeHTTP(w, r)
		})
	}
}

This is one of the nicest parts of Go’s standard HTTP package. You can build authentication, logging, request IDs, and rate limits without framework-specific middleware types.

What I would improve before production

The lab is intentionally small, but a few changes would make it sturdier:

  • parse Authorization: Bearer <token> instead of treating the whole header as the token
  • validate config and fail if JWT_SECRET is still the dev value
  • return structured JSON errors instead of plain http.Error
  • add request IDs to the logging middleware
  • make SQL migrations part of startup or deployment

Those are useful exercises because they build directly on the existing structure.

What to take from it

The useful lesson is package shape. main wires dependencies. Handlers translate HTTP to service calls. Services own business/database operations. Middleware wraps handlers. Config and database setup sit behind small packages.

That structure is enough for a lot of Go services. Learn it before reaching for a heavier framework.