HTTP сервер из коробки
Go создавался в Google для веб-сервисов, поэтому HTTP сервер встроен в стандартную библиотеку. Никаких фреймворков не нужно для начала!
Простейший сервер
package main
import (
"fmt"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Привет, %s!", r.URL.Path[1:])
}
func main() {
http.HandleFunc("/", handler)
fmt.Println("Сервер запущен на :8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
panic(err)
}
}
Структура Handler
http.Handler интерфейс
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
// Собственный handler
type MyHandler struct {
greeting string
}
func (h *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "%s, мир!", h.greeting)
}
func main() {
handler := &MyHandler{greeting: "Привет"}
http.Handle("/", handler)
http.ListenAndServe(":8080", nil)
}
HandlerFunc - адаптер
// Превращаем функцию в Handler
func myHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello!"))
}
func main() {
// HandlerFunc это тип, реализующий Handler
http.Handle("/", http.HandlerFunc(myHandler))
// Или короче
http.HandleFunc("/", myHandler)
}
Работа с запросами
func handler(w http.ResponseWriter, r *http.Request) {
// Метод запроса
fmt.Printf("Method: %s\n", r.Method)
// URL и путь
fmt.Printf("URL: %s\n", r.URL)
fmt.Printf("Path: %s\n", r.URL.Path)
// Query параметры
name := r.URL.Query().Get("name")
// Headers
contentType := r.Header.Get("Content-Type")
// Cookies
cookie, err := r.Cookie("session")
if err == nil {
fmt.Printf("Cookie: %s\n", cookie.Value)
}
// Body (для POST/PUT)
body, err := io.ReadAll(r.Body)
if err == nil {
fmt.Printf("Body: %s\n", string(body))
}
}
Работа с ответами
func handler(w http.ResponseWriter, r *http.Request) {
// Установка заголовков (до записи тела!)
w.Header().Set("Content-Type", "application/json")
w.Header().Set("X-Custom-Header", "value")
// Установка кода ответа (до записи тела!)
w.WriteHeader(http.StatusOK) // 200
// Запись тела ответа
w.Write([]byte(`{"status": "ok"}`))
// Или используем fmt.Fprintf
fmt.Fprintf(w, `{"message": "%s"}`, "Hello")
}
JSON API
JSON в Go обычно описывается структурой с тегами полей json:"...".
Отправка JSON
type Response struct {
Status string `json:"status"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
func jsonHandler(w http.ResponseWriter, r *http.Request) {
response := Response{
Status: "success",
Message: "Данные получены",
Data: map[string]interface{}{
"users": []string{"Alice", "Bob"},
"count": 2,
},
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(response); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
Получение JSON
type CreateUserRequest struct {
Name string `json:"name" validate:"required"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"min=0,max=150"`
}
func createUser(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
// Валидация
if req.Name == "" || req.Email == "" {
http.Error(w, "Name and email are required", http.StatusBadRequest)
return
}
// Обработка...
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]interface{}{
"id": 123,
"message": "User created",
})
}
Роутинг
Простой роутер
func router(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/":
homeHandler(w, r)
case "/users":
usersHandler(w, r)
case "/about":
aboutHandler(w, r)
default:
http.NotFound(w, r)
}
}
func main() {
http.HandleFunc("/", router)
http.ListenAndServe(":8080", nil)
}
RESTful роутинг
func usersHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
getUsers(w, r)
case http.MethodPost:
createUser(w, r)
case http.MethodPut:
updateUser(w, r)
case http.MethodDelete:
deleteUser(w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
Middleware паттерн
// Middleware тип
type Middleware func(http.HandlerFunc) http.HandlerFunc
// Logging middleware
func withLogging(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next(w, r)
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
}
}
// Auth middleware
func withAuth(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// Проверка токена...
next(w, r)
}
}
// CORS middleware
func withCORS(next http.HandlerFunc) http.HandlerFunc {
return 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(w, r)
}
}
// Композиция
func main() {
handler := withCORS(withLogging(withAuth(apiHandler)))
http.HandleFunc("/api", handler)
http.ListenAndServe(":8080", nil)
}
Статические файлы
func main() {
// Обслуживание статических файлов
fs := http.FileServer(http.Dir("./static"))
http.Handle("/static/", http.StripPrefix("/static/", fs))
// API endpoints
http.HandleFunc("/api/", apiHandler)
// SPA fallback
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "./static/index.html")
})
http.ListenAndServe(":8080", nil)
}
Работа с контекстом
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// Добавляем значения в контекст
ctx = context.WithValue(ctx, "userID", 123)
// Используем контекст для отмены
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
// Передаем контекст дальше
result, err := slowOperation(ctx)
if err != nil {
// `errors.Is` - см. урок про [обработку ошибок](./10-errors.md)
if errors.Is(err, context.DeadlineExceeded) {
http.Error(w, "Request timeout", http.StatusRequestTimeout)
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Write([]byte(result))
}
func slowOperation(ctx context.Context) (string, error) {
select {
case <-time.After(10 * time.Second):
return "Done", nil
case <-ctx.Done():
return "", ctx.Err()
}
}
Практический пример: REST API
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
type UserService struct {
mu sync.RWMutex
users map[int]User
maxID int
}
func NewUserService() *UserService {
return &UserService{
users: make(map[int]User),
}
}
func (s *UserService) GetAll() []User {
s.mu.RLock()
defer s.mu.RUnlock()
users := make([]User, 0, len(s.users))
for _, user := range s.users {
users = append(users, user)
}
return users
}
func (s *UserService) Create(user User) User {
s.mu.Lock()
defer s.mu.Unlock()
s.maxID++
user.ID = s.maxID
s.users[user.ID] = user
return user
}
func (s *UserService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
users := s.GetAll()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(users)
case http.MethodPost:
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
created := s.Create(user)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(created)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func main() {
userService := NewUserService()
mux := http.NewServeMux()
mux.Handle("/users", userService)
server := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}
log.Println("Server starting on :8080")
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
Graceful shutdown
func main() {
server := &http.Server{
Addr: ":8080",
Handler: router(),
}
go func() {
if err := server.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err)
}
}()
// Ждем сигнала остановки
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
<-quit
log.Println("Shutting down server...")
// Graceful shutdown с таймаутом
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")
}
Итоги
- HTTP сервер встроен в стандартную библиотеку
- Handler интерфейс - основа всего
- Middleware для переиспользуемой логики
- Контекст для отмены и значений
- Graceful shutdown для production
В следующем уроке подключим базу данных!
Типичная ошибка
Игнорировать r.Body.Close() и ошибки парсинга - потом будет «почему память течёт».