Вы выучили переменные, циклы, структуры и функции в 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 звучит намного страшнее, чем выглядит в коде.
Шаг 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/usersBody → 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-сообщество Кодика. Там выходят полезные разборы, короткие посты, задачи и материалы для разработчиков — удобно открыть ленту и за несколько минут освежить какую-нибудь тему.
