[golang] http server (p. 1)

[golang] http server (p. 1)

Всем привет! В этой статье будет разобрано, как написать веб сервер на golang с помощью стандартной библиотеки http. В качестве примера будет создаваться простая версия приложения ToDo для управления задачами.


Содание проекта

Первое, что необходимо сделать, это выполнить команды для инициализации проекта и для создания структуры проекта, для чего можно воспользоваться инструкциями, приведенными ниже.

# инициализация проекта
go mod init hw

# создание директории, точки входа в приложение, и файла main.go
mkdir cmd
touch cmd/main.go

# создание директории, содержащей внутренню логику приложения
# а также вложенной директории todo с файлами handler.go, repository.go, model.go
mkdir internal
mkdir internal/todo
touch internal/todo/handler.go
touch internal/todo/repository.go
touch internal/todo/model.go

# создание директории, содержащей пакеты общего использования
# а также вложенной директории db с файлом db.go
mkdir pkg
mkdir pkg/db
touch pkg/db/db.go

# Таким образом, мы получим следующую структуру нашего проекта
.
├── cmd
│   └── main.go
├── go.mod
├── internal
│   └── todo
│       ├── handler.go
│       ├── model.go
│       └── repository.go
└── pkg
    └── db
        └── db.go

Пока что в нашем проекте не будет следующих директорий:

  • configs, отвечающей за конфиги приложения,
  • migrations, отвечающей за миграции.
  • Они будут добавлены позже по мере развития приложения в следующих статьях.

    Хранение данных будет осуществляться в оперативной памяти.


    Написание структуры для хранения данных - internal/todo/model.go

    Пока что в нашем проекте не будет использоваться какая-либо база данных - она будет добавлена в последующих статьях. А в настоящий момент информацию будем хранить с помощью созданной нами структуры Task и такого типа данных как map.

    // internal/todo/model.go
    package todo
    
    import "time"
    
    type Task struct {
    	ID          string
    	Title       string
    	Description string
    	CreateadAt  time.Time
    	UpdatedAt   time.Time
    	Done        bool
    }
    
    // Task - структура
    // с помощью которой будет передаваться и храниться информация о задаче
    // из запроса со стороны клиента
    

    Написание структуры и функционала хранения данных в оперативной памяти - pkg/db/db.go

    На первой итерации реализуем хранение данных в оперативной памяти.

    // pkg/db/db.go
    package db
    
    import "time"
    
    type TaskDb struct {
    	ID          string
    	Title       string
    	Description string
    	CreateadAt  time.Time
    	UpdatedAt   time.Time
    	Done        bool
    }
    
    // Task - структура
    // с помощью которой будет передаваться и храниться информация о задаче
    
    type Db struct {
    	Tasks map[string]TaskDb
    }
    
    // Db - база данных с хранением в оперативной памяти
    
    func NewDb() *Db {
    	return &Db{
    		Tasks: map[string]TaskDb{},
    	}
    }
    
    // NewDb создает новый экземпляр базы данных
    

    Написание репозитория - internal/todo/repository.go

    Репозиторий будет помогать нам осуществлять работу с базой данных

    // internal/todo/repository.go
    package todo
    
    import (
    	"errors"
    	"hw/pkg/db"
    )
    
    type TaskRepository struct {
    	Database *db.Db
    }
    
    // TaskRepository - репозиторий для работы с задачами
    
    func NewTaskRepository(database *db.Db) *TaskRepository {
    	return &TaskRepository{
    		Database: database,
    	}
    }
    
    // NewTaskRepository создает новый экземпляр репозитория
    
    func (repo *TaskRepository) Create(task *Task) (*Task, error) {
    	repo.Database.Tasks[task.ID] = db.TaskDb{
    		ID:          task.ID,
    		Title:       task.Title,
    		Description: task.Description,
    		CreateadAt:  task.CreateadAt,
    		UpdatedAt:   task.UpdatedAt,
    		Done:        task.Done,
    	}
    	return task, nil
    }
    
    // Create добавляет задачу в базу данных
    
    func (repo *TaskRepository) GetByID(id string) (*Task, error) {
    	task, ok := repo.Database.Tasks[id]
    	if !ok {
    		return nil, errors.New("task not found")
    	}
    	data := Task{
    		ID:          task.ID,
    		Title:       task.Title,
    		Description: task.Description,
    		CreateadAt:  task.CreateadAt,
    		UpdatedAt:   task.UpdatedAt,
    		Done:        task.Done,
    	}
    	return &data, nil
    }
    
    // GetByID возвращает задачу из базы данных по идентификатору
    
    func (repo *TaskRepository) GetAll() ([]Task, error) {
    	var tasks []Task
    	for _, task := range repo.Database.Tasks {
    		tasks = append(tasks, Task{
    			ID:          task.ID,
    			Title:       task.Title,
    			Description: task.Description,
    			CreateadAt:  task.CreateadAt,
    			UpdatedAt:   task.UpdatedAt,
    			Done:        task.Done,
    		})
    	}
    	return tasks, nil
    }
    
    // GetAll возвращает все задачи из базы данных
    
    func (repo *TaskRepository) Update(task *Task) (*Task, error) {
    	_, ok := repo.Database.Tasks[task.ID]
    	if !ok {
    		return nil, errors.New("task not found")
    	}
    	repo.Database.Tasks[task.ID] = db.TaskDb{
    		ID:          task.ID,
    		Title:       task.Title,
    		Description: task.Description,
    		CreateadAt:  task.CreateadAt,
    		UpdatedAt:   task.UpdatedAt,
    		Done:        task.Done,
    	}
    	return task, nil
    }
    
    // Update обновляет задачу в базе данных
    
    func (repo *TaskRepository) Delete(id string) error {
    	_, ok := repo.Database.Tasks[id]
    	if !ok {
    		return errors.New("task not found")
    	}
    	delete(repo.Database.Tasks, id)
    	return nil
    }
    
    // Delete удаляет задачу из базы данных
    

    Написание обработчика запросов - internal/todo/handler.go

    Прописываем логику работы ручек нашего приложения

    // internal/todo/handler.go
    package todo
    
    import (
    	"encoding/json"
    	"net/http"
    	"time"
    
    	"github.com/google/uuid"
    )
    
    type TaskHandler struct {
    	TaskRepository *TaskRepository
    }
    
    func NewTaskHandler(router *http.ServeMux, taskRepository *TaskRepository) {
    	handler := &TaskHandler{
    		TaskRepository: taskRepository,
    	}
    	router.HandleFunc("GET /tasks", handler.GetAll())
    	router.HandleFunc("GET /tasks/{id}", handler.GetByID())
    	router.HandleFunc("POST /tasks", handler.Create())
    	router.HandleFunc("PUT /tasks/{id}", handler.Update())
    	router.HandleFunc("DELETE /tasks/{id}", handler.Delete())
    }
    
    func (h *TaskHandler) GetAll() http.HandlerFunc {
    	return func(w http.ResponseWriter, r *http.Request) {
    		data, err := h.TaskRepository.GetAll()
    		if err != nil {
    			http.Error(w, err.Error(), http.StatusInternalServerError)
    			return
    		}
    		w.Header().Set("Content-Type", "application/json")
    		w.WriteHeader(http.StatusOK)
    		json.NewEncoder(w).Encode(data)
    	}
    }
    
    func (h *TaskHandler) GetByID() http.HandlerFunc {
    	return func(w http.ResponseWriter, r *http.Request) {
    		id := r.PathValue("id")
    		data, err := h.TaskRepository.GetByID(id)
    		if err != nil {
    			http.Error(w, err.Error(), http.StatusNotFound)
    			return
    		}
    		w.Header().Set("Content-Type", "application/json")
    		w.WriteHeader(http.StatusOK)
    		json.NewEncoder(w).Encode(data)
    	}
    }
    
    func (h *TaskHandler) Create() http.HandlerFunc {
    	return func(w http.ResponseWriter, r *http.Request) {
    		var task Task
    		if err := json.NewDecoder(r.Body).Decode(&task); err != nil {
    			http.Error(w, err.Error(), http.StatusBadRequest)
    			return
    		}
    		task.ID = uuid.New().String()
    		task.CreateadAt = time.Now()
    		task.UpdatedAt = time.Now()
    		task.Done = false
    		data, err := h.TaskRepository.Create(&task)
    		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(data)
    	}
    }
    
    func (h *TaskHandler) Update() http.HandlerFunc {
    	return func(w http.ResponseWriter, r *http.Request) {
    		id := r.PathValue("id")
    		var task Task
    		if err := json.NewDecoder(r.Body).Decode(&task); err != nil {
    			http.Error(w, err.Error(), http.StatusBadRequest)
    			return
    		}
    		var taskDb *Task
    		taskDb, err := h.TaskRepository.GetByID(id)
    		if err != nil {
    			http.Error(w, err.Error(), http.StatusNotFound)
    			return
    		}
    		task.ID = id
    		task.CreateadAt = taskDb.CreateadAt
    		task.UpdatedAt = time.Now()
    		data, err := h.TaskRepository.Update(&task)
    		if err != nil {
    			http.Error(w, err.Error(), http.StatusInternalServerError)
    			return
    		}
    		w.Header().Set("Content-Type", "application/json")
    		w.WriteHeader(http.StatusOK)
    		json.NewEncoder(w).Encode(data)
    	}
    }
    
    func (h *TaskHandler) Delete() http.HandlerFunc {
    	return func(w http.ResponseWriter, r *http.Request) {
    		id := r.PathValue("id")
    		err := h.TaskRepository.Delete(id)
    		if err != nil {
    			http.Error(w, err.Error(), http.StatusNotFound)
    			return
    		}
    		w.WriteHeader(http.StatusNoContent)
    	}
    }
    

    Написание точки входа - cmd/main.go

    // cmd/main.go
    package main
    
    import (
    	"fmt"
    	"hw/internal/todo"
    	"hw/pkg/db"
    	"net/http"
    )
    
    func main() {
    	db := db.NewDb()
    
    	router := http.NewServeMux()
    
    	taskRepository := todo.NewTaskRepository(db)
    
    	todo.NewTaskHandler(router, taskRepository)
    
    	server := http.Server{
    		Addr:    ":8080",
    		Handler: router,
    	}
    
    	fmt.Println("Server is listening on port 8080")
    	server.ListenAndServe()
    }
    

    GitHub Repo

    https://github.com/rushawx/golang_http_demo