Why Go for a Video API
When DailyWatch needed an API that could handle thousands of concurrent requests with minimal latency, Go was a natural fit. Its standard library includes a production-grade HTTP server, goroutines handle concurrency without callback hell, and a single compiled binary simplifies deployment.
Basic Server Setup
Go's net/http is all you need — no framework required:
package main
import (
"database/sql"
"encoding/json"
"log"
"net/http"
"time"
_ "github.com/lib/pq"
)
type Video struct {
VideoID string `json:"video_id"`
Title string `json:"title"`
Channel string `json:"channel_title"`
Views int64 `json:"views"`
Likes int64 `json:"likes"`
ThumbnailURL string `json:"thumbnail_url"`
Regions []string `json:"regions"`
}
type Server struct {
db *sql.DB
router *http.ServeMux
}
func NewServer(db *sql.DB) *Server {
s := &Server{db: db, router: http.NewServeMux()}
s.routes()
return s
}
func (s *Server) routes() {
s.router.HandleFunc("GET /api/trending/{region}", s.handleTrending)
s.router.HandleFunc("GET /api/search", s.handleSearch)
s.router.HandleFunc("GET /api/video/{id}", s.handleVideoDetail)
s.router.HandleFunc("GET /health", s.handleHealth)
}
func main() {
db, err := sql.Open("postgres", "host=localhost dbname=dailywatch sslmode=disable")
if err != nil {
log.Fatal(err)
}
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(10)
db.SetConnMaxLifetime(5 * time.Minute)
defer db.Close()
srv := NewServer(db)
server := &http.Server{
Addr: ":8080",
Handler: srv.router,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
log.Println("Starting server on :8080")
log.Fatal(server.ListenAndServe())
}
Connection pooling is built into database/sql. Setting MaxOpenConns, MaxIdleConns, and ConnMaxLifetime prevents connection leaks and keeps the pool healthy under load.
Request Handlers
func (s *Server) handleTrending(w http.ResponseWriter, r *http.Request) {
region := r.PathValue("region")
if len(region) != 2 {
http.Error(w, "invalid region code", http.StatusBadRequest)
return
}
rows, err := s.db.QueryContext(r.Context(), `
SELECT v.video_id, v.title, v.channel_title, v.views, v.likes,
v.thumbnail_url, array_agg(vr.region) as regions
FROM videos v
JOIN video_regions vr ON v.video_id = vr.video_id
WHERE vr.region = $1
GROUP BY v.video_id
ORDER BY v.views DESC
LIMIT 50
`, region)
if err != nil {
http.Error(w, "database error", http.StatusInternalServerError)
log.Printf("trending query error: %v", err)
return
}
defer rows.Close()
var videos []Video
for rows.Next() {
var v Video
var regions []byte
if err := rows.Scan(&v.VideoID, &v.Title, &v.Channel,
&v.Views, &v.Likes, &v.ThumbnailURL, ®ions); err != nil {
continue
}
json.Unmarshal(regions, &v.Regions)
videos = append(videos, v)
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "public, max-age=3600")
json.NewEncoder(w).Encode(videos)
}
Using r.Context() in the database query means if the client disconnects, the query cancels automatically. This prevents wasted database work on abandoned requests.
Middleware Chain
Middleware in Go is just function composition:
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
wrapped := &statusWriter{ResponseWriter: w, status: 200}
next.ServeHTTP(wrapped, r)
log.Printf("%s %s %d %s", r.Method, r.URL.Path,
wrapped.status, time.Since(start))
})
}
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "https://dailywatch.video")
w.Header().Set("Access-Control-Allow-Methods", "GET")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
type statusWriter struct {
http.ResponseWriter
status int
}
func (w *statusWriter) WriteHeader(status int) {
w.status = status
w.ResponseWriter.WriteHeader(status)
}
Stack them in main():
handler := loggingMiddleware(corsMiddleware(srv.router))
Benchmarks
We benchmarked the Go API serving trending feeds for DailyWatch with wrk:
wrk -t4 -c100 -d30s http://localhost:8080/api/trending/US
Requests/sec: 12,450
Avg Latency: 3.2ms
P99 Latency: 12ms
Memory Usage: ~18MB
For comparison, our PHP API handling the same query averages 45ms per request. The Go version is 14x faster per request and uses a fraction of the memory. The compiled binary is 8MB — no runtime dependencies to install.
Graceful Shutdown
Production servers need clean shutdown:
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-quit
log.Println("Shutting down...")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
server.Shutdown(ctx)
}()
This lets in-flight requests complete before stopping, preventing dropped connections during deploys.
This article is part of the Building DailyWatch series. Check out DailyWatch to see these techniques in action.
Top comments (0)