
Привет, Хабр! У многих разработчиков на .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 в проект.
Спасибо за внимание! Надеюсь кому-то данный материал поможет первоначально разобраться.
Если понравилось, вот мой телеграмм канал. Там я делюсь анонсами статей, делюсь своими размышлениями по разным моментам в айти, также рассказываю о своем изучении Golang.
