{}const=>[]async()letfn</>var
РазработкаОсновыBackend

Как написать первый API на Go

Как создать REST API на Go с нуля: поднимаем HTTP-сервер, создаём GET и POST endpoints, работаем с JSON, статус-кодами и тестируем API.

К

Кодик

Автор

6 мин чтения

Вы выучили переменные, циклы, структуры и функции в Go. Посмотрели на горутины. Кивнули с умным видом. А потом приходит главный вопрос:

«Окей. А как на этом сделать что-нибудь настоящее?»

Например, API. Такое, чтобы можно было открыть:

http://localhost:8080/users

и получить настоящий JSON. Не:

Hello, World!

А:

[
  {
    "id": 1,
    "name": "Alex"
  }
]

Вот в этот момент начинается backend.

Хорошая новость: для первого API на Go нам даже не понадобится огромный фреймворк. У Go уже есть пакет net/http.

Go: «Фреймворк? А зачем?»

Что такое API простыми словами?

API позволяет одной программе обращаться к другой.

Например, мобильное приложение отправляет серверу:

GET /users

Сервер отвечает:

[
  {
    "id": 1,
    "name": "Max"
  },
  {
    "id": 2,
    "name": "Kate"
  }
]

Или приложение хочет создать пользователя:

POST /users

и отправляет:

{
  "name": "Alex"
}

Сервер принимает данные, что-то с ними делает и отвечает. Всё. REST API звучит намного страшнее, чем выглядит в коде.

🔥 100 000+ учеников уже с нами

Устал читать теорию?
Пора кодить!

Кодик — приложение, где ты учишься программировать через практику. AI-наставник, интерактивные уроки, реальные проекты.

🤖 AI 24/7
🎓 Сертификаты
💰 Бесплатно
🚀 Начать учиться
Присоединились сегодня

Шаг 1. Создаём проект на Go

Создадим папку:

mkdir first-go-api
cd first-go-api

Инициализируем Go-модуль:

go mod init first-go-api

После этого создаём файл main.go.

first-go-api/
└── main.go

Архитекторы уже нервничают. Но для первого API этого более чем достаточно.

Шаг 2. Поднимаем HTTP-сервер

Начнём с самого простого сервера:

package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/", homeHandler)
    fmt.Println("Server started on http://localhost:8080")
    http.ListenAndServe(":8080", nil)
}

func homeHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "Hello from Go API")
}

Запускаем:

go run main.go

Открываем:

http://localhost:8080

Получаем:

Hello from Go API

Поздравляем. У вас уже HTTP-сервер на Go. Да. Вот настолько быстро.

Что здесь вообще произошло?

http.HandleFunc

http.HandleFunc("/", homeHandler)

Мы говорим: если кто-нибудь обращается по /, вызови функцию homeHandler.

Handler

func homeHandler(w http.ResponseWriter, r *http.Request)

Практически каждый HTTP-handler будет получать две важные штуки.

r *http.Request содержит информацию от клиента: HTTP-метод, URL, query-параметры, headers, body и cookies.

Например:

r.Method

может содержать GET или POST.

Через w http.ResponseWriter мы формируем ответ сервера:

fmt.Fprintln(w, "Hello")

Шаг 3. Возвращаем JSON

API без JSON — это как программист без вкладок Stack Overflow. Технически возможно. Но подозрительно.

Создадим структуру пользователя:

type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

Теперь сделаем endpoint:

GET /users

Полный пример:

package main

import (
    "encoding/json"
    "fmt"
    "net/http"
)

type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

func main() {
    http.HandleFunc("/users", usersHandler)
    fmt.Println("Server started on http://localhost:8080")
    http.ListenAndServe(":8080", nil)
}

func usersHandler(w http.ResponseWriter, r *http.Request) {
    users := []User{
        {ID: 1, Name: "Alex"},
        {ID: 2, Name: "Kate"},
    }
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(users)
}

Переходим на http://localhost:8080/users и получаем:

[
  {
    "id": 1,
    "name": "Alex"
  },
  {
    "id": 2,
    "name": "Kate"
  }
]

Вот теперь это уже действительно похоже на API.

Зачем нужны json-теги?

type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

Теги вроде json:"id" говорят Go, как поле должно называться в JSON. Поэтому ID превращается в id, а не остаётся ID.

Шаг 4. Разбираемся с GET и POST

Пока endpoint /users отвечает вообще на любой HTTP-метод. Можно отправить GET, POST или DELETE — сервер всё равно вернёт пользователей.

Это немного похоже на охранника:

— У вас пропуск есть?

— Нет.

— Ну ладно, проходите.

Исправляем:

func usersHandler(w http.ResponseWriter, r *http.Request) {
    if r.Method == http.MethodGet {
        getUsers(w)
        return
    }
    if r.Method == http.MethodPost {
        createUser(w, r)
        return
    }
    http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}

Теперь GET /users получает пользователей, а POST /users создаёт пользователя.

Шаг 5. Создаём пользователя через POST

Для начала заведём массив пользователей:

var users = []User{
    {ID: 1, Name: "Alex"},
    {ID: 2, Name: "Kate"},
}

Функция получения пользователей:

func getUsers(w http.ResponseWriter) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(users)
}

Теперь создание пользователя:

func createUser(w http.ResponseWriter, r *http.Request) {
    var user User
    err := json.NewDecoder(r.Body).Decode(&user)
    if err != nil {
        http.Error(w, "Invalid JSON", http.StatusBadRequest)
        return
    }
    user.ID = len(users) + 1
    users = append(users, user)
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(user)
}

Теперь можно отправить:

POST /users

с body:

{
  "name": "John"
}

Сервер ответит:

{
  "id": 3,
  "name": "John"
}

Пользователь создан. Мы только что сделали первый настоящий API endpoint.

А вот и знаменитое if err != nil

err := json.NewDecoder(r.Body).Decode(&user)
if err != nil {
    http.Error(w, "Invalid JSON", http.StatusBadRequest)
    return
}

Добро пожаловать в Go.

Здесь начинается if err != nil. Потом ещё if err != nil. Потом вы открываете чужой Go-проект — и там снова if err != nil.

Через неделю вам снится:

if err != nil

Но идея на самом деле хорошая: Go заставляет ошибки обрабатывать явно.

Если клиент отправил кривой JSON, Decode() вернёт ошибку. Мы ловим её и отвечаем:

400 Bad Request

Шаг 6. HTTP status codes — не игнорируем их

API должен возвращать не только данные, но и нормальные HTTP-коды.

Код

Значение

200

OK

201

Created

400

Bad Request

404

Not Found

405

Method Not Allowed

500

Internal Server Error

Например:

w.WriteHeader(http.StatusCreated)

отправляет 201 Created.

Использовать http.StatusCreated лучше, чем просто писать 201. Через полгода вы хотя бы будете понимать, что хотели сказать.

Собираем наш первый REST API целиком

package main

import (
    "encoding/json"
    "fmt"
    "net/http"
)

type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

var users = []User{
    {ID: 1, Name: "Alex"},
    {ID: 2, Name: "Kate"},
}

func main() {
    http.HandleFunc("/users", usersHandler)
    fmt.Println("Server started on http://localhost:8080")
    err := http.ListenAndServe(":8080", nil)
    if err != nil {
        fmt.Println("Server error:", err)
    }
}

func usersHandler(w http.ResponseWriter, r *http.Request) {
    switch r.Method {
    case http.MethodGet:
        getUsers(w)
    case http.MethodPost:
        createUser(w, r)
    default:
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
    }
}

func getUsers(w http.ResponseWriter) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(users)
}

func createUser(w http.ResponseWriter, r *http.Request) {
    var user User
    err := json.NewDecoder(r.Body).Decode(&user)
    if err != nil {
        http.Error(w, "Invalid JSON", http.StatusBadRequest)
        return
    }
    if user.Name == "" {
        http.Error(w, "Name is required", http.StatusBadRequest)
        return
    }
    user.ID = len(users) + 1
    users = append(users, user)
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(user)
}

Итого API умеет:

GET /users
POST /users

Для первого проекта — уже отлично.

Как проверить API?

Через браузер удобно тестировать GET. Но POST так просто не отправить.

curl

Получить пользователей:

curl http://localhost:8080/users

Создать пользователя:

curl -X POST http://localhost:8080/users \
-H "Content-Type: application/json" \
-d '{"name":"Mike"}'

Postman

Можно использовать Postman. Создаёте запрос:

POST http://localhost:8080/users

Body → JSON:

{
  "name": "Mike"
}

Нажимаете Send и чувствуете себя backend-разработчиком примерно на +12%.

Но где база данных?

Пока здесь:

var users = []User{}

Да, наша база данных называется массив в оперативной памяти.

Выключили сервер — все добавленные данные исчезли.

Для обучения это нормально. Следующим этапом можно подключить PostgreSQL, MySQL или SQLite.

Client
   ↓
HTTP API
   ↓
Handler
   ↓
Service
   ↓
Repository
   ↓
PostgreSQL

Но не пытайтесь впихнуть всё это в первый вечер. Сначала поймите HTTP. Потом архитектура станет гораздо понятнее.

Нужен ли Gin, Fiber или Echo?

Когда человек гуглит «как написать REST API на Go», через пять минут он обычно уже выбирает между Gin, Fiber, Echo и Chi. Хотя ещё толком не понял net/http.

Фреймворки действительно удобны. Например, в Gin маршрут выглядит так:

router.GET("/users", getUsers)

Но первый API полезно написать именно на стандартном net/http. Тогда становится понятно, что такое handler, request, response, headers, JSON, HTTP methods и status codes.

И уже потом Gin не выглядит как шаманская библиотека, которая каким-то образом заставляет интернет работать.

Что добавить в API дальше?

Теперь можно прокачать проект.

Получение одного пользователя:

GET /users/1

Обновление:

PUT /users/1

Удаление:

DELETE /users/1

Тогда получится полноценный CRUD:

CREATE → POST
READ   → GET
UPDATE → PUT/PATCH
DELETE → DELETE

Именно такой набор операций встречается буквально везде.

Небольшая практика

Не просто копируйте код. Добавьте структуру:

type Task struct {
    ID        int    `json:"id"`
    Title     string `json:"title"`
    Completed bool   `json:"completed"`
}

И реализуйте:

GET /tasks
POST /tasks

А затем попробуйте самостоятельно добавить:

DELETE /tasks/{id}

Если получилось — у вас уже начинает складываться понимание backend-разработки.

А если хочется изучать Go не через бесконечное чтение

Самая частая ловушка при изучении программирования выглядит так:

курс → видео → статья → ещё видео → ещё статья

А код? Когда-нибудь потом.

Именно поэтому в Кодике обучение построено вокруг практики: изучаете тему и сразу решаете задачи, пишете код и закрепляете материал руками.

Так намного проще пройти путь от:

fmt.Println("Hello")

до собственного API, приложения или полноценного проекта.

А если хочется повторять программирование небольшими порциями, заглядывайте в наше Telegram-сообщество Кодика. Там выходят полезные разборы, короткие посты, задачи и материалы для разработчиков — удобно открыть ленту и за несколько минут освежить какую-нибудь тему.

🎯Хватит откладывать

Понравилась статья?
Пора применять на практике!

В Кодик ты не просто читаешь — ты сразу пишешь код. Теория + практика = реальный скилл.

Мгновенная практика
🧠AI объяснит код
🏆Сертификат

Без регистрации • Без карты