re-write with std usage of net/http

This commit is contained in:
Sebastian Cabrera 2025-06-11 15:49:47 -04:00
parent c107d94b11
commit 81bc046237
Signed by: okseby
GPG key ID: DA858232740D0404
10 changed files with 419 additions and 97 deletions

View file

@ -1,24 +1,31 @@
# Playground GO
A simple web server implementation in Go that demonstrates basic HTTP server concepts.
A modern web server implementation in Go that demonstrates production-ready HTTP server concepts.
## Overview
This project implements a basic HTTP server that:
- Listens on a specified address and port
- Handles HTTP requests
This project implements a robust HTTP server that:
- Listens on a configurable port
- Handles HTTP requests with a custom router
- Processes requests concurrently using goroutines
- Implements middleware for logging and request processing
- Provides graceful shutdown capabilities
- Uses structured logging
- Follows clean architecture principles
## Project Structure
```
.
├── main.go # Application entry point
├── server/
│ ├── server.go # Server implementation
│ └── handler.go # HTTP request handlers
├── go.mod # Go module definition
└── README.md # This file
├── main.go # Application entry point
├── handlers/ # HTTP request handlers
├── middleware/ # HTTP middleware components
├── logger/ # Structured logging implementation
├── internal/ # Internal packages and configuration
├── static/ # Static file serving
├── go.mod # Go module definition
├── .gitignore # Git ignore rules
└── README.md # This file
```
## Prerequisites
@ -27,38 +34,49 @@ This project implements a basic HTTP server that:
## Usage
1. Import the server package in your Go code:
```go
import "your-module-name/server"
1. Clone the repository:
```bash
git clone https://github.com/yourusername/playground-go.git
cd playground-go
```
2. Create a new server instance:
```go
server := server.NewServer(":8080") // Listen on port 8080
2. Run the server:
```bash
go run main.go
```
3. Start the server:
```go
server.ListenAndServe()
```
The server will start on the configured port (default: 8080).
## Features
- Concurrent request handling using goroutines
- HTTP request processing
- Graceful error handling
- Simple and clean API design
- Graceful server shutdown
- Structured logging
- Middleware support for request processing
- Configuration management
- Clean architecture with separated concerns
- Static file serving
- Error handling and logging
## Configuration
The server can be configured through environment variables:
- `PORT`: The port number to listen on (default: 8080)
- Additional configuration options can be added in the `internal/config` package
## Error Handling
The server includes basic error handling for:
- Address binding errors
- Request processing errors
- HTTP response errors
The server includes comprehensive error handling for:
- Server startup and shutdown
- Request processing
- Configuration loading
- Graceful shutdown with timeout
- Structured error logging
## Contributing
Feel free to submit issues and enhancement requests!
Feel free to submit issues and enhancement requests! When contributing, please:
1. Fork the repository
2. Create a new branch for your feature
3. Submit a pull request with a clear description of your changes

30
handlers/routes.go Normal file
View file

@ -0,0 +1,30 @@
package handlers
import (
"fmt"
"net/http"
)
func RegisterRoutes() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/", serveIndex)
mux.HandleFunc("/hello", helloHandler)
mux.HandleFunc("/goodbye", goodbyeHandler)
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
return mux
}
func serveIndex(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "static/index.html")
}
func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello, world!")
}
func goodbyeHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Goodbye, world!")
}

28
internal/config/config.go Normal file
View file

@ -0,0 +1,28 @@
package config
import (
"os"
)
type Config struct {
Port string
StaticDir string
Environment string
}
var AppConfig Config
func Load() {
AppConfig = Config{
Port: getEnv("PORT", "7878"),
StaticDir: getEnv("STATIC_DIR", "static"),
Environment: getEnv("ENVIRONMENT", "development"),
}
}
func getEnv(key, fallback string) string {
if value, ok := os.LookupEnv(key); ok {
return value
}
return fallback
}

41
logger/logger.go Normal file
View file

@ -0,0 +1,41 @@
package logger
import (
"fmt"
"os"
"time"
)
// ANSI colors
const (
colorReset = "\033[0m"
colorRed = "\033[31m"
colorYellow = "\033[33m"
colorCyan = "\033[36m"
colorWhite = "\033[97m"
)
// Log levels
const (
LevelInfo = "INFO"
LevelWarn = "WARN"
LevelError = "ERROR"
LevelFatal = "FATAL"
)
func logMessage(level, color, msg string, args ...any) {
timestamp := time.Now().Format("2006-01-02 15:04:05")
formatted := fmt.Sprintf(msg, args...)
output := fmt.Sprintf("[%s] %s[%s]%s %s\n", timestamp, color, level, colorReset, formatted)
fmt.Print(output)
if level == LevelFatal {
os.Exit(1)
}
}
// Public logging functions
func Info(msg string, args ...any) { logMessage(LevelInfo, colorCyan, msg, args...) }
func Warn(msg string, args ...any) { logMessage(LevelWarn, colorYellow, msg, args...) }
func Error(msg string, args ...any) { logMessage(LevelError, colorRed, msg, args...) }
func Fatal(msg string, args ...any) { logMessage(LevelFatal, colorRed, msg, args...) }

52
main.go
View file

@ -1,8 +1,54 @@
package main
import "git.okseby.com/okseby/playground-go/server"
import (
"context"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"git.okseby.com/okseby/playground-go/handlers"
"git.okseby.com/okseby/playground-go/internal/config"
"git.okseby.com/okseby/playground-go/logger"
"git.okseby.com/okseby/playground-go/middleware"
)
func main() {
server := server.NewServer("0.0.0.0:7878")
server.ListenAndServe()
// Load config
config.Load()
// Register routes
mux := handlers.RegisterRoutes()
handler := middleware.Logger(mux)
server := &http.Server{
Addr: ":" + config.AppConfig.Port,
Handler: handler,
}
// Channel to listen for shutdown signals
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
// Run the server in a goroutine
go func() {
logger.Info("Starting server on port %s...", config.AppConfig.Port)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Fatal("Error starting server: %v", err)
}
}()
// Wait for shutdown signal
<-quit
logger.Info("Shutting down...")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
logger.Fatal("Server forced to shutdown: %v", err)
}
logger.Info("Server exited cleanly.")
}

56
middleware/logging.go Normal file
View file

@ -0,0 +1,56 @@
package middleware
import (
"net/http"
"time"
"git.okseby.com/okseby/playground-go/logger"
)
// ANSI color codes
const (
ColorReset = "\033[0m"
ColorRed = "\033[31m"
ColorGreen = "\033[32m"
ColorYellow = "\033[33m"
ColorCyan = "\033[36m"
ColorGrey = "\033[90m"
)
type statusRecorder struct {
http.ResponseWriter
status int
}
func (r *statusRecorder) WriteHeader(code int) {
r.status = code
r.ResponseWriter.WriteHeader(code)
}
func Logger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
recorder := &statusRecorder{ResponseWriter: w, status: 200}
next.ServeHTTP(recorder, r)
duration := time.Since(start)
statusColor := ColorGreen
switch {
case recorder.status >= 500:
statusColor = ColorRed
case recorder.status >= 400:
statusColor = ColorRed
case recorder.status >= 300:
statusColor = ColorYellow
}
logger.Info("%s%-7s%s %s%-30s%s %s%d%s [%s]",
ColorCyan, r.Method, ColorReset,
ColorGrey, r.URL.Path, ColorReset,
statusColor, recorder.status, ColorReset,
duration.Round(time.Millisecond),
)
})
}

View file

@ -1,30 +0,0 @@
package server
import (
"bufio"
"fmt"
"io"
"net"
)
func handleConnection(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
request, err := reader.ReadString('\n')
if err != nil {
if err != io.EOF {
fmt.Println("read request error:", err)
}
return
}
fmt.Println("request:", request)
response := "HTTP/1.1 200 OK\r\n" +
"Content-Length: 13\r\n" +
"Content-Type: text/plain\r\n\r\n" +
"Hello, world!"
conn.Write([]byte(response))
}

View file

@ -1,33 +0,0 @@
package server
import (
"fmt"
"net"
)
type Server struct {
addr string
}
func NewServer(addr string) *Server {
return &Server{addr: addr}
}
func (s *Server) ListenAndServe() {
listener, err := net.Listen("tcp", s.addr)
if err != nil {
fmt.Println("Error binding to address:", err)
}
defer listener.Close()
fmt.Printf("Server listening on %s\n", s.addr)
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("Error accepting connection:", err)
continue
}
go handleConnection(conn)
}
}

56
static/index.html Normal file
View file

@ -0,0 +1,56 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cool Web Server</title>
<link rel="stylesheet" href="/static/styles/index.css">
</head>
<body>
<header>
<div class="container">
<h1>Welcome to My Web Server</h1>
<p class="subtitle">A modern and efficient web server implementation</p>
</div>
</header>
<main class="container">
<section class="features">
<div class="feature-card">
<h3>Fast & Efficient</h3>
<p>Built with performance in mind, ensuring quick response times and optimal resource usage.</p>
</div>
<div class="feature-card">
<h3>Modern Stack</h3>
<p>Utilizing the latest web technologies and best practices for a robust server implementation.</p>
</div>
<div class="feature-card">
<h3>Easy to Use</h3>
<p>Simple configuration and straightforward setup process for hassle-free deployment.</p>
</div>
</section>
<div style="text-align: center;">
<a href="#" class="cta-button">Get Started</a>
</div>
</main>
<footer>
<div class="container">
<p>&copy; 2024 My Web Server. All rights reserved.</p>
</div>
</footer>
<script>
// Add some interactivity
document.querySelectorAll('.feature-card').forEach(card => {
card.addEventListener('click', () => {
card.style.transform = 'scale(1.02)';
setTimeout(() => {
card.style.transform = 'translateY(-5px)';
}, 200);
});
});
</script>
</body>
</html>

110
static/styles/index.css Normal file
View file

@ -0,0 +1,110 @@
:root {
--primary-color: #2563eb;
--secondary-color: #1e40af;
--background-color: #f8fafc;
--text-color: #1e293b;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
line-height: 1.6;
color: var(--text-color);
background-color: var(--background-color);
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
header {
text-align: center;
padding: 4rem 0;
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
color: white;
margin-bottom: 3rem;
}
h1 {
font-size: 3rem;
margin-bottom: 1rem;
animation: fadeIn 1s ease-in;
}
.subtitle {
font-size: 1.2rem;
opacity: 0.9;
}
.features {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
margin-bottom: 3rem;
}
.feature-card {
background: white;
padding: 2rem;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease;
}
.feature-card:hover {
transform: translateY(-5px);
}
.feature-card h3 {
color: var(--primary-color);
margin-bottom: 1rem;
}
.cta-button {
display: inline-block;
background-color: var(--primary-color);
color: white;
padding: 1rem 2rem;
border-radius: 5px;
text-decoration: none;
transition: background-color 0.3s ease;
}
.cta-button:hover {
background-color: var(--secondary-color);
}
footer {
text-align: center;
padding: 2rem;
margin-top: 3rem;
border-top: 1px solid #e2e8f0;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 768px) {
.container {
padding: 1rem;
}
h1 {
font-size: 2rem;
}
}