Введение: почему Go — идеальный выбор для REST API
Go (или Golang) — это компилируемый язык программирования от Google, который сочетает производительность C++ с простотой Python. Если вы хотите создать быстрый, надёжный и легко поддерживаемый REST API, Go — один из лучших вариантов. В этой статье мы за 30 минут напишем полноценный REST API на Go, который умеет создавать, читать, обновлять и удалять записи (CRUD). Никаких сложных фреймворков — только стандартная библиотека net/http и немного кода.
К концу статьи вы получите готовый HTTP-сервер, который отвечает на запросы в формате JSON и работает без внешних зависимостей. Поехали!
Шаг 1: Настройка проекта и структура
Для начала убедитесь, что у вас установлен Go (версия 1.16+). Создайте новую директорию для проекта и инициализируйте модуль:
mkdir go-rest-api
cd go-rest-api
go mod init go-rest-api
Структура проекта будет простой — один файл main.go. Весь код мы разместим в нём, чтобы не усложнять пример. В реальных проектах вы, конечно, разобьёте код на пакеты, но для обучения это идеальный вариант.
Шаг 2: Определяем структуру данных и глобальное хранилище
Наше API будет управлять списком книг. Создадим структуру Book и простой срез для хранения данных в памяти (вместо базы данных).
package main
import (
"encoding/json"
"log"
"net/http"
"strconv"
"strings"
)
type Book struct {
ID int `json:"id"`
Title string `json:"title"`
Author string `json:"author"`
Year int `json:"year"`
}
var books []Book
var nextID int = 1
Здесь books — наш временный «слой данных», а nextID — счётчик для автоматического присвоения ID новым книгам.
Шаг 3: Реализуем обработчики HTTP-запросов (CRUD)
Теперь самое интересное — напишем функции-обработчики для каждого HTTP-метода. Мы будем использовать стандартный маршрутизатор http.NewServeMux.
3.1. Получение всех книг (GET /books)
func getBooks(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(books)
}
3.2. Создание новой книги (POST /books)
func createBook(w http.ResponseWriter, r *http.Request) {
var newBook Book
if err := json.NewDecoder(r.Body).Decode(&newBook); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
newBook.ID = nextID
nextID++
books = append(books, newBook)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(newBook)
}
3.3. Получение одной книги по ID (GET /books/{id})
В стандартном ServeMux нет поддержки динамических путей, поэтому мы извлечём ID из URL вручную.
func getBook(w http.ResponseWriter, r *http.Request) {
idStr := strings.TrimPrefix(r.URL.Path, "/books/")
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "Invalid ID", http.StatusBadRequest)
return
}
for _, book := range books {
if book.ID == id {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(book)
return
}
}
http.Error(w, "Book not found", http.StatusNotFound)
}
3.4. Обновление книги (PUT /books/{id})
func updateBook(w http.ResponseWriter, r *http.Request) {
idStr := strings.TrimPrefix(r.URL.Path, "/books/")
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "Invalid ID", http.StatusBadRequest)
return
}
var updatedBook Book
if err := json.NewDecoder(r.Body).Decode(&updatedBook); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
for i, book := range books {
if book.ID == id {
updatedBook.ID = id
books[i] = updatedBook
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(updatedBook)
return
}
}
http.Error(w, "Book not found", http.StatusNotFound)
}
3.5. Удаление книги (DELETE /books/{id})
func deleteBook(w http.ResponseWriter, r *http.Request) {
idStr := strings.TrimPrefix(r.URL.Path, "/books/")
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "Invalid ID", http.StatusBadRequest)
return
}
for i, book := range books {
if book.ID == id {
books = append(books[:i], books[i+1:]...)
w.WriteHeader(http.StatusNoContent)
return
}
}
http.Error(w, "Book not found", http.StatusNotFound)
}
Шаг 4: Собираем всё вместе — главная функция и маршрутизация
Теперь объединим обработчики в единый маршрутизатор. Обратите внимание на трюк: мы используем один обработчик для пути /books/, который сам распределяет запросы по методам.
func booksRouter(w http.ResponseWriter, r *http.Request) {
// Если путь ровно "/books" — работаем со списком
if r.URL.Path == "/books" {
switch r.Method {
case http.MethodGet:
getBooks(w, r)
case http.MethodPost:
createBook(w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
return
}
// Иначе — работаем с конкретной книгой (путь /books/{id})
switch r.Method {
case http.MethodGet:
getBook(w, r)
case http.MethodPut:
updateBook(w, r)
case http.MethodDelete:
deleteBook(w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func main() {
// Добавим пару тестовых книг
books = append(books, Book{ID: 1, Title: "Война и мир", Author: "Лев Толстой", Year: 1869})
books = append(books, Book{ID: 2, Title: "Преступление и наказание", Author: "Фёдор Достоевский", Year: 1866})
nextID