Шаблон Go-микросервиса для начинающих от .NET разработчика. Часть 1
Мотивация для бэкэндеров изучать 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
Теперь создадим структуру, которая будет отражать наш конфигурационный файл. Для этого нам потребуется установить необходимый пакет. В терминале, в корневом каталоге нашего проекта, выполните следующую команду.
got 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 в проект.
Ссылка на сам проект
Спасибо за внимание! Надеюсь кому-то данный материал поможет первоначально разобраться.