- Home »

Как создать HTTP-сервер на Go
Сколько раз мы разработчики сталкиваемся с ситуацией, когда нужно быстро поднять HTTP-сервер? Может быть, для тестирования API, прототипирования или развёртывания микросервиса в production. Go — это один из самых удобных языков для создания веб-серверов, и сегодня мы разберём, как написать эффективный HTTP-сервер на Go с нуля. Я покажу не только базовые примеры, но и продвинутые техники, которые пригодятся в реальных проектах.
Почему именно Go для HTTP-серверов?
Go был создан в Google специально для системного программирования и сетевых приложений. Встроенная поддержка HTTP, горутины для конкурентности и минималистичный синтаксис делают его идеальным выбором для веб-разработки. По статистике Stack Overflow Developer Survey 2023, Go занимает 4-е место по популярности среди языков для backend-разработки.
Основные преимущества Go для HTTP-серверов:
- Встроенный пакет
net/http
без внешних зависимостей - Высокая производительность (сравнимая с C++)
- Простота развёртывания — один исполняемый файл
- Отличная поддержка конкурентности
- Быстрая компиляция
Базовый HTTP-сервер за 5 минут
Давайте начнём с самого простого примера. Создадим файл main.go
:
package main
import (
"fmt"
"net/http"
"log"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World! Method: %s, URL: %s", r.Method, r.URL.Path)
})
log.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Запускаем сервер:
go run main.go
Всё! Сервер работает на localhost:8080. Но это только начало.
Создание полноценного REST API
Теперь создадим более практичный пример — REST API для управления пользователями:
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"strings"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
var users = []User{
{ID: 1, Name: "John Doe", Email: "john@example.com"},
{ID: 2, Name: "Jane Smith", Email: "jane@example.com"},
}
func getUsersHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(users)
}
func getUserHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
// Извлекаем ID из URL
path := strings.TrimPrefix(r.URL.Path, "/users/")
id, err := strconv.Atoi(path)
if err != nil {
http.Error(w, "Invalid user ID", http.StatusBadRequest)
return
}
// Ищем пользователя
for _, user := range users {
if user.ID == id {
json.NewEncoder(w).Encode(user)
return
}
}
http.Error(w, "User not found", http.StatusNotFound)
}
func createUserHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var newUser User
if err := json.NewDecoder(r.Body).Decode(&newUser); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
// Генерируем новый ID
newUser.ID = len(users) + 1
users = append(users, newUser)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(newUser)
}
func usersHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
if r.URL.Path == "/users" {
getUsersHandler(w, r)
} else {
getUserHandler(w, r)
}
case http.MethodPost:
createUserHandler(w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func main() {
http.HandleFunc("/users", usersHandler)
http.HandleFunc("/users/", usersHandler)
log.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Тестируем API:
# Получить всех пользователей
curl http://localhost:8080/users
# Получить пользователя по ID
curl http://localhost:8080/users/1
# Создать нового пользователя
curl -X POST http://localhost:8080/users \
-H "Content-Type: application/json" \
-d '{"name":"Bob Wilson","email":"bob@example.com"}'
Продвинутые возможности: middleware и роутинг
Для production-приложений нужны middleware для логирования, аутентификации и обработки ошибок. Вот пример с кастомным роутером:
package main
import (
"encoding/json"
"log"
"net/http"
"time"
)
// Middleware для логирования
func loggingMiddleware(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 %v", r.Method, r.URL.Path, time.Since(start))
})
}
// Middleware для CORS
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
// Простой роутер
type Router struct {
routes map[string]map[string]http.HandlerFunc
}
func NewRouter() *Router {
return &Router{
routes: make(map[string]map[string]http.HandlerFunc),
}
}
func (r *Router) Handle(method, path string, handler http.HandlerFunc) {
if r.routes[path] == nil {
r.routes[path] = make(map[string]http.HandlerFunc)
}
r.routes[path][method] = handler
}
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if methods, exists := r.routes[req.URL.Path]; exists {
if handler, exists := methods[req.Method]; exists {
handler(w, req)
return
}
}
http.NotFound(w, req)
}
func healthHandler(w http.ResponseWriter, r *http.Request) {
response := map[string]string{
"status": "ok",
"time": time.Now().Format(time.RFC3339),
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
func main() {
router := NewRouter()
router.Handle("GET", "/health", healthHandler)
router.Handle("GET", "/users", getUsersHandler)
// Применяем middleware
handler := loggingMiddleware(corsMiddleware(router))
server := &http.Server{
Addr: ":8080",
Handler: handler,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
log.Println("Server starting on :8080")
log.Fatal(server.ListenAndServe())
}
Работа с базой данных
Реальные приложения требуют персистентности данных. Вот пример с PostgreSQL:
package main
import (
"database/sql"
"encoding/json"
"log"
"net/http"
"strconv"
_ "github.com/lib/pq"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
var db *sql.DB
func initDB() {
var err error
db, err = sql.Open("postgres", "postgres://user:password@localhost/dbname?sslmode=disable")
if err != nil {
log.Fatal(err)
}
// Создаём таблицу
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL
)
`)
if err != nil {
log.Fatal(err)
}
}
func getUsersFromDB(w http.ResponseWriter, r *http.Request) {
rows, err := db.Query("SELECT id, name, email FROM users")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
var users []User
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name, &u.Email); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
users = append(users, u)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(users)
}
func createUserInDB(w http.ResponseWriter, r *http.Request) {
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
err := db.QueryRow(
"INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id",
user.Name, user.Email,
).Scan(&user.ID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
}
func main() {
initDB()
defer db.Close()
http.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
getUsersFromDB(w, r)
case http.MethodPost:
createUserInDB(w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
})
log.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Для установки драйвера PostgreSQL:
go mod init myserver
go get github.com/lib/pq
Конфигурация для production
Для production-среды нужна правильная конфигурация. Создадим файл config.go
:
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
type Config struct {
Port string
ReadTimeout time.Duration
WriteTimeout time.Duration
IdleTimeout time.Duration
DBUrl string
}
func getConfig() *Config {
return &Config{
Port: getEnv("PORT", "8080"),
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
DBUrl: getEnv("DATABASE_URL", "postgres://localhost/mydb?sslmode=disable"),
}
}
func getEnv(key, defaultValue string) string {
if value, exists := os.LookupEnv(key); exists {
return value
}
return defaultValue
}
func main() {
config := getConfig()
// Инициализируем роутер
router := NewRouter()
router.Handle("GET", "/health", healthHandler)
server := &http.Server{
Addr: ":" + config.Port,
Handler: loggingMiddleware(corsMiddleware(router)),
ReadTimeout: config.ReadTimeout,
WriteTimeout: config.WriteTimeout,
IdleTimeout: config.IdleTimeout,
}
// Graceful shutdown
go func() {
log.Printf("Server starting on port %s", config.Port)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Server failed to start: %v", err)
}
}()
// Ожидаем сигнал завершения
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Server shutting down...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Fatal("Server forced to shutdown:", err)
}
log.Println("Server exited")
}
Сравнение с другими решениями
Решение | Производительность | Простота | Экосистема | Память |
---|---|---|---|---|
Go net/http | Очень высокая | Простая | Средняя | Низкое потребление |
Node.js Express | Высокая | Простая | Огромная | Среднее потребление |
Python Flask | Средняя | Очень простая | Большая | Высокое потребление |
Java Spring Boot | Высокая | Сложная | Огромная | Очень высокое потребление |
Популярные Go-фреймворки
Хотя стандартная библиотека Go мощная, иногда нужны дополнительные возможности:
- Gin — самый популярный, быстрый и минималистичный
- Echo — высокопроизводительный с отличной документацией
- Fiber — вдохновлён Express.js, очень быстрый
- Gorilla Mux — мощный роутер для стандартной библиотеки
Пример с Gin:
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
r.Run() // По умолчанию на :8080
}
Развёртывание на сервере
Для развёртывания в production понадобится VPS или выделенный сервер. Вот пример развёртывания с systemd:
Создаём systemd service файл /etc/systemd/system/myserver.service
:
[Unit]
Description=My Go HTTP Server
After=network.target
[Service]
Type=simple
User=www-data
WorkingDirectory=/opt/myserver
ExecStart=/opt/myserver/myserver
Restart=always
RestartSec=5
Environment=PORT=8080
Environment=DATABASE_URL=postgres://user:pass@localhost/dbname
[Install]
WantedBy=multi-user.target
Команды для управления:
# Компилируем приложение
go build -o myserver main.go
# Копируем на сервер
sudo cp myserver /opt/myserver/
# Запускаем сервис
sudo systemctl enable myserver
sudo systemctl start myserver
sudo systemctl status myserver
Nginx как reverse proxy
Конфигурация Nginx для проксирования запросов:
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Мониторинг и метрики
Добавим endpoint для метрик Prometheus:
package main
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"net/http"
"time"
)
var (
requestsTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total number of HTTP requests",
},
[]string{"method", "path", "status"},
)
requestDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request duration in seconds",
},
[]string{"method", "path"},
)
)
func init() {
prometheus.MustRegister(requestsTotal)
prometheus.MustRegister(requestDuration)
}
func metricsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Обёртка для захвата status code
ww := &responseWriter{ResponseWriter: w, statusCode: 200}
next.ServeHTTP(ww, r)
duration := time.Since(start).Seconds()
requestsTotal.WithLabelValues(r.Method, r.URL.Path, string(ww.statusCode)).Inc()
requestDuration.WithLabelValues(r.Method, r.URL.Path).Observe(duration)
})
}
type responseWriter struct {
http.ResponseWriter
statusCode int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
func main() {
router := NewRouter()
router.Handle("GET", "/health", healthHandler)
// Endpoint для метрик
http.Handle("/metrics", promhttp.Handler())
handler := metricsMiddleware(loggingMiddleware(router))
log.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", handler))
}
Тестирование HTTP-сервера
Пример unit-тестов:
package main
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)
func TestGetUsersHandler(t *testing.T) {
req, err := http.NewRequest("GET", "/users", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := http.HandlerFunc(getUsersHandler)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}
var users []User
if err := json.Unmarshal(rr.Body.Bytes(), &users); err != nil {
t.Errorf("could not parse response: %v", err)
}
if len(users) == 0 {
t.Error("expected users, got none")
}
}
func TestCreateUserHandler(t *testing.T) {
user := User{Name: "Test User", Email: "test@example.com"}
jsonUser, _ := json.Marshal(user)
req, err := http.NewRequest("POST", "/users", bytes.NewBuffer(jsonUser))
if err != nil {
t.Fatal(err)
}
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
handler := http.HandlerFunc(createUserHandler)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusCreated {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusCreated)
}
}
Запуск тестов:
go test -v
go test -cover
Интересные факты и нестандартные применения
Go HTTP-серверы можно использовать не только для веб-API:
- Файловый сервер —
http.FileServer(http.Dir("./static"))
- WebSocket сервер — для real-time приложений
- gRPC-HTTP gateway — мост между gRPC и REST
- Прокси-сервер — используя
httputil.ReverseProxy
- Webhook обработчик — для GitHub, GitLab и других сервисов
Пример WebSocket сервера:
package main
import (
"github.com/gorilla/websocket"
"net/http"
"log"
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
func websocketHandler(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println(err)
return
}
defer conn.Close()
for {
messageType, p, err := conn.ReadMessage()
if err != nil {
log.Println(err)
break
}
// Эхо-сервер
if err := conn.WriteMessage(messageType, p); err != nil {
log.Println(err)
break
}
}
}
func main() {
http.HandleFunc("/ws", websocketHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
Автоматизация и CI/CD
Пример GitHub Actions для автоматического деплоя:
name: Deploy Go Server
on:
push:
branches: [ main ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.21
- name: Build
run: go build -o myserver main.go
- name: Deploy to server
uses: appleboy/ssh-action@v0.1.5
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.SSH_KEY }}
script: |
sudo systemctl stop myserver
sudo cp myserver /opt/myserver/
sudo systemctl start myserver
Заключение и рекомендации
Go предоставляет мощные и простые инструменты для создания HTTP-серверов. Основные рекомендации:
- Для простых API — используйте стандартную библиотеку
net/http
- Для сложных приложений — рассмотрите Gin или Echo
- Всегда используйте middleware для логирования, CORS и аутентификации
- Настройте graceful shutdown для production
- Добавьте метрики для мониторинга
- Пишите тесты с самого начала
Go HTTP-серверы отлично подходят для микросервисов, API Gateway, и высоконагруженных приложений. Благодаря простоте развёртывания и высокой производительности, Go стал стандартом де-факто для backend-разработки во многих компаниях.
Полезные ссылки:
В этой статье собрана информация и материалы из различных интернет-источников. Мы признаем и ценим работу всех оригинальных авторов, издателей и веб-сайтов. Несмотря на то, что были приложены все усилия для надлежащего указания исходного материала, любая непреднамеренная оплошность или упущение не являются нарушением авторских прав. Все упомянутые товарные знаки, логотипы и изображения являются собственностью соответствующих владельцев. Если вы считаете, что какой-либо контент, использованный в этой статье, нарушает ваши авторские права, немедленно свяжитесь с нами для рассмотрения и принятия оперативных мер.
Данная статья предназначена исключительно для ознакомительных и образовательных целей и не ущемляет права правообладателей. Если какой-либо материал, защищенный авторским правом, был использован без должного упоминания или с нарушением законов об авторском праве, это непреднамеренно, и мы исправим это незамедлительно после уведомления. Обратите внимание, что переиздание, распространение или воспроизведение части или всего содержимого в любой форме запрещено без письменного разрешения автора и владельца веб-сайта. Для получения разрешений или дополнительных запросов, пожалуйста, свяжитесь с нами.