Шаблон Go-микросервиса для начинающих от .NET разработчика. Часть 1

от автора

Мотивация для бэкэндеров изучать Golang

Мотивация для бэкэндеров изучать Golang

Привет, Хабр! У многих разработчиков на .NET вызывает интерес относительно свежий язык программирования Go (Golang). Однако при поиске информации и учебных материалов он может отпугивать. Нам предлагается забыть все удобное и красивое, чему нас научила .NET, и принять что-то новое, но кажущееся непривычным и не всегда приятным.

И к проблеме непривычности добавляется отсутствие качественного материала на русском языке. Большинство книг поверхностно рассматривают стандартные для всех языков ключевые слова, не углубляясь в важные аспекты их внутреннего устройства и работы.

В своей статье я хочу поэтапно описать все необходимые шаги для создания простого микросервиса и представить его в виде шаблона. Так как я сам не являюсь опытным разработчиком на Go, а только изучаю этот язык, мой шаблон предназначен для того, чтобы показать, как примерно выглядит микросервис.

Содержание

В данной статье я не буду углубляться в тонкости языка и объяснять все детали. Статья предназначена для опытных разработчиков других языков, которые изучают Go и которым не хватает примеров и шаблонов для построения API в учебных целях.

Мы создадим проект с нуля, добавим вычитывание конфигурации, настройку инфраструктуры, настройку DI и настройку HTTP-сервера, и всё это запустим.

Создаем проект

Для разработки я буду использовать IDE GoLand.

Создаем новый проект и нас встречает пустой каталог с файлом go.mod, который содержит версию языка и нашем случае — это 1.22

Для организации архитектуры каталогов проекта буду использовать на работки из этой статьи — структурирование проекта на golang

Создаем каталог с стартовой точкой нашего приложения, аналог из .NET файл Program.cs.

cmd/app/main.go
package main  import "fmt"  func main() { fmt.Println("Hello World") } 

В языке программирования Go, как и во многих других языках, точкой входа в приложение является функция main. Для создания IDE автоматически профиля сборки эта функция должна быть размещена в пакете main. Наименование пакетов в Go является аналогом неймспейсов в .NET.

Настройка конфигурации

Конфигурация проекта осуществляется с помощью environments. Я не буду задавать их в системе или конфигурационном файле проекта, а для удобства использую библиотеку, которая загружает конфигурацию из файла в переменные окружения (Environment Variables).

База данных, которая будет использоваться в проекте postgresql. Дальнейшая ее настройка и поднятие через docker-compose.yml будут в следующих пунктах, а пока занимаемся настройкой вычитки из конфигурационного файла.

Для начала создадим файл для локального запуска конфигурации.

configs/local.env
DB_CONFIG_USER=golang-template-service DB_CONFIG_PASSWORD=golang-template-service DB_CONFIG_DBNAME=golang-template-service DB_CONFIG_HOST=127.0.0.1 DB_CONFIG_PORT=5432

Теперь создадим структуру, которая будет отражать наш конфигурационный файл. Для этого нам потребуется установить необходимый пакет. В терминале, в корневом каталоге нашего проекта, выполните следующую команду.

go get "github.com/joho/godotenv"

Саму структуру, которую мы будем использовать для конфигурации, поместим в каталог internal. В языке Go все, что помещено в каталог internal, может использоваться только в рамках данного проекта и не может быть использовано как подключаемый пакет. Это ограничение действует автоматически благодаря названию каталога.

Создаем структуру для конфигурации базы данных.

internal/config/database/config.go
package database  // Config - Конфигурация для подключения к БД type Config struct { User     string Password string Host     string Port     int DbName   string }

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

internal/config/config_helper.go
package config  import ( "github.com/joho/godotenv" "log" "os" "strconv" )  // LoadEnvironment - загрузить из файла конфигурацию в environments func LoadEnvironment() { err := godotenv.Load("configs/local.env") if err != nil { log.Fatal("Error loading .env file") } }  // getEnv - считать environment в формете string func getEnv(key string, defaultVal string) string { if value, exists := os.LookupEnv(key); exists { return value }  return defaultVal }  // getEnvAsInt - считать environment в формете int func getEnvAsInt(name string, defaultVal int) int { valueStr := getEnv(name, "") if value, err := strconv.Atoi(valueStr); err == nil { return value }  return defaultVal }

Теперь создаем общий конфиг приложения, который будет с помощью DI внедряться в наши сервисы. Настройка DI будет далее.

internal/config/config.go
package config  import "src/internal/config/database"  // Config - Главный конфиг приложения type Config struct { Database *database.Config }  func NewConfig() *Config { return &Config{ Database: &database.Config{ User:     getEnv("DB_CONFIG_USER", "root"), Password: getEnv("DB_CONFIG_PASSWORD", "root"), Host:     getEnv("DB_CONFIG_HOST", "localhost"), Port:     getEnvAsInt("DB_CONFIG_PORT", 3306), DbName:   getEnv("DB_CONFIG_DBNAME", ""), }, } } 

Теперь возвращаемся в функцию main и проверяем вычитывание нашей конфигурации

cmd/app/main.go
package main  import ( "fmt" "src/internal/config" )  func main() { // Вызываем подгрузку конфигурации config.LoadEnvironment()  // Создаем конфиг appConfig := config.NewConfig()  fmt.Println(fmt.Sprintf("User: %s", appConfig.Database.User)) fmt.Println(fmt.Sprintf("Host: %s", appConfig.Database.Host)) fmt.Println(fmt.Sprintf("Password: %s", appConfig.Database.Password)) fmt.Println(fmt.Sprintf("DbName: %s", appConfig.Database.DbName)) fmt.Println(fmt.Sprintf("Port: %d", appConfig.Database.Port)) }

Все работает и идем далее.

Настройка HTTP-сервера

Теперь нам необходимо подготовить всё для запуска HTTP-сервера и настройки маршрутизации. Выполните следующую команду в корневом каталоге проекта для установки необходимых пакетов.

go get "github.com/codegangsta/negroni" go get "github.com/gorilla/mux"

Далее создадим файл заготовку для маршрутизации

api/router/router.go
package router  import ( "github.com/gorilla/mux" )  // Router - структура описывающие маршрутизацию контроллеров в нашем приложении type Router struct { }  // NewRouter Метод для инициализации структуры в DI func NewRouter() *Router { return &Router{} }  // InitRoutes - инициализация маршрутизации API func (routes *Router) InitRoutes() *mux.Router { router := mux.NewRouter() return router }

После этого создадим файл с настройками сервера, его инициализацией и запуском.

server/server.go
package server  import ( "github.com/codegangsta/negroni" "src/api/router" "src/internal/config"  "net/http" )  // Server - структура сервера type Server struct { AppConfig *config.Config Router    *router.Router }  // NewServer Метод для инициализации структуры в DI func NewServer(appConfig *config.Config, router *router.Router) *Server { return &Server{ AppConfig: appConfig, Router:    router, } }  // Run - метод для запуска нашего http-сервера func (server *Server) Run() { ngRouter := server.Router.InitRoutes() ngClassic := negroni.Classic() ngClassic.UseHandler(ngRouter) err := http.ListenAndServe(":5000", ngClassic) if err != nil { return } } 

Настройка DI

Сейчас не будем запускать сервер и проверять его работу. Сначала настроим DI (внедрение зависимостей) для нашего сервиса. Выполните следующую команду для установки необходимого пакета.

go get "go.uber.org/dig"

Далее создадим app.go файл в котором будет происходит конфигурация нашего DI

internal/app/app.go
package app  import ( "go.uber.org/dig" "src/api/router" "src/internal/config" "src/server" )  func BuildContainer() *dig.Container { container := dig.New()  _ = container.Provide(config.NewConfig) _ = container.Provide(server.NewServer) _ = container.Provide(router.NewRouter)  return container }

Теперь, когда контейнер настроен, возвращаемся в main.go, собираем контейнер и запускаем наш HTTP-сервер.

cmd/app/main.go
package main  import ( "src/internal/app" "src/internal/config" "src/server" )  func main() { // Вызываем подгрузку конфигурации config.LoadEnvironment()  // Билдим наш контейре с зависимостями container := app.BuildContainer()  // Запускаем наш HTTP-сервер err := container.Invoke(func(server *server.Server) { server.Run() })  if err != nil { panic(err) } } 

Проверяем, что наше приложение не завершается и продолжает работать HTTP-сервер и идем далее.

Настраиваем заготовку репозитория, сервиса, контроллера.

Далее создадим всё необходимое: от репозитория до контроллера, зарегистрируем маршрутизацию и добавим всё это в DI.

Выполняем команду ниже и скачиваем пакет для работы с uuid.

go get "github.com/google/uuid"

Создаем сущность в нашем случае это будет книга

internal/entities/book/book_entity.go
package book  import "github.com/google/uuid"  // Entity - модель в БД для нашей книги type Entity struct { Uuid uuid.UUID Name string } 

Далее создаем репозиторий

internal/repositories/book/book_repository.go
package book  import ( "fmt" "src/internal/entities/book" )  // Repository - Структура репозитория type Repository struct { database []book.Entity }  // NewRepository - Метод для регистрации в DI func NewRepository() *Repository { return &Repository{ database: make([]book.Entity, 0), } }  // Create - добавить книгу func (repository *Repository) Create(entity book.Entity) { repository.database = append(repository.database, entity) fmt.Println(repository.database) }

Теперь создаем модель для нашего сервиса

internal/models/book/book_create_model.go
package book  // CreateModel - Модель создания книги type CreateModel struct { Name string `json:"name" form:"name"` } 

Создадим наш сервис

internal/services/book/book_service.go
package book  import ( "github.com/google/uuid" bookEntities "src/internal/entities/book" bookService "src/internal/models/book" "src/internal/repositories/book" )  // Service - для работы с книгами type Service struct { repository *book.Repository }  // NewService - метод для регистрации в DI func NewService(repository *book.Repository) *Service { return &Service{repository: repository} }  // Create - метод создания книги func (service *Service) Create(model *bookService.CreateModel) { // Создаем сущность bookEntity := bookEntities.Entity{ Uuid: uuid.New(), Name: model.Name, }  service.repository.Create(bookEntity) } 

Базовые вещи подготовлены. Теперь нам нужно создать контроллер, прописать для него маршрутизацию и зарегистрировать всё это в DI.

Создаем наш контроллер

api/controllers/book/book_controller.go
package book  import ( "encoding/json" "net/http" bookModels "src/internal/models/book" "src/internal/services/book" )  // Controller - контроллер для работы с книгами type Controller struct { service *book.Service }  // NewController - мето для регистрации контроллера DI func NewController(service *book.Service) *Controller { return &Controller{ service: service, } }  func (controller *Controller) CreateBook(w http.ResponseWriter, r *http.Request) { request := new(bookModels.CreateModel) decoder := json.NewDecoder(r.Body) _ = decoder.Decode(&request)  controller.service.Create(request)  w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) } 

Теперь создадим маршрутизацию для нашего контроллера

api/controllers/book/book_controller_route.go
package book  import ( "github.com/gorilla/mux" )  // ControllerRoute настройки маршрутизации для нашего контроллера type ControllerRoute struct { Controller *Controller }  // NewControllerRoute Метод для регистрации в DI func NewControllerRoute(controller *Controller) *ControllerRoute { return &ControllerRoute{Controller: controller} }  // Route добавить в роутер маршрут func (route *ControllerRoute) Route(router *mux.Router) *mux.Router { router.HandleFunc("/api/books", route.Controller.CreateBook).Methods("POST") return router } 

Теперь нам необходимо вернуться в наш router.go и добавить наш контроллер и маршрутизацию.

api/router/router.go
package router  import ( "github.com/gorilla/mux" "src/api/controllers/book" )  // Router - структура описывающие маршрутизацию контроллеров в нашем приложении type Router struct { BookRoutes *book.ControllerRoute }  // NewRouter Метод для инициализации структуры в DI func NewRouter(bookRoutes *book.ControllerRoute) *Router { return &Router{ BookRoutes: bookRoutes, } }  // InitRoutes - инициализация маршрутизации API func (routes *Router) InitRoutes() *mux.Router { router := mux.NewRouter() router = routes.BookRoutes.Route(router) return router } 

Далее необходимо вернуться в app.go и добавить регистрацию в DI всего, что мы добавили

api/internal/app/app.go
package app  import ( "go.uber.org/dig" "src/api/controllers/book" "src/api/router" "src/internal/config" bookRepository "src/internal/repositories/book" bookService "src/internal/services/book" "src/server" )  func BuildContainer() *dig.Container { container := dig.New()  _ = container.Provide(config.NewConfig) _ = container.Provide(server.NewServer) _ = container.Provide(router.NewRouter)  buildBook(container)  return container }  func buildBook(container *dig.Container) { _ = container.Provide(book.NewController) _ = container.Provide(book.NewControllerRoute) _ = container.Provide(bookService.NewService) _ = container.Provide(bookRepository.NewRepository) } 

Теперь все готово для нашего первоначального запуска и первого запроса

Полетели

Запускаем и с помощью postman отправляем наш первый запрос и видим, что все заработало.

Заключение

В этой части мы создали всю необходимую первоначальную инфраструктуру для нашего сервиса: настроили конфигурацию, внедрение зависимостей (DI) и запуск HTTP-сервера. В следующей части я планирую подключить базу данных, настроить middleware для логирования, добавить Swagger и, возможно, включить ещё несколько полезных элементов.

Также если есть какие-то замечания или пожелания пишите в комментариях, или создавайте PR в проект.

Ссылка на сам проект

Спасибо за внимание! Надеюсь кому-то данный материал поможет первоначально разобраться.

Для тех кому понравилось
Автор обращается к тем, кому понравилось

Автор обращается к тем, кому понравилось

Поэтому если интересно получать анонсы новых статей, технические истории работяги или выбрать тему следующей статьи, присоединяйся к моему Телеграмм каналу.


ссылка на оригинал статьи https://habr.com/ru/articles/818201/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *