re-write with std usage of net/http
This commit is contained in:
parent
c107d94b11
commit
81bc046237
10 changed files with 419 additions and 97 deletions
80
README.md
80
README.md
|
@ -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
30
handlers/routes.go
Normal 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
28
internal/config/config.go
Normal 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
41
logger/logger.go
Normal 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
52
main.go
|
@ -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
56
middleware/logging.go
Normal 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),
|
||||
)
|
||||
})
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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
56
static/index.html
Normal 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>© 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
110
static/styles/index.css
Normal 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;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue