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

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

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

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

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

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

Содержание

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

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

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

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

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

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

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

c7bee252ecdeefa931ecc12099fbd762.pngcmd/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 будут в следующих пунктах, а пока занимаемся настройкой вычитки из конфигурационного файла.

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

32c0c96d20bd8fcefe01d9a8c5c4a5ea.pngconfigs/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, может использоваться только в рамках данного проекта и не может быть использовано как подключаемый пакет. Это ограничение действует автоматически благодаря названию каталога.

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

a95657ac877a88b29ad6bd23c74908ed.pnginternal/config/database/config.go

package database

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

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

4ad4662357ecef06e2ecbbaa783659e8.pnginternal/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 будет далее.

30144eebb54dc0396741f60d408419f1.pnginternal/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))
}

80d74d1d3e0d130d0f04b71ff78e758e.png

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

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

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

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

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

afc0560f4e8914533795354e0290e3b6.pngapi/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
}

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

24762239b39ee4ac7659ad13270cbdc4.pngserver/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

ab753eba2f14f02eb62b4d8c005eb33b.pnginternal/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"

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

d0634c28fa451d1c0dd47766c7716746.pnginternal/entities/book/book_entity.go

package book

import "github.com/google/uuid"

// Entity - модель в БД для нашей книги
type Entity struct {
	Uuid uuid.UUID
	Name string
}

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

8342290ceff5ed2f97dcd1eec165b6af.pnginternal/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)
}

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

7050500f2bfb285d221895f0e1eb558a.pnginternal/models/book/book_create_model.go

package book

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

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

1d70c2fda3e37fe3f9262a09cc2a03c6.pnginternal/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.

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

b5c37a6d8336c2e328713ba608d59a4f.pngapi/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)
}

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

cbfbf1c02492fba63d19d3d443e0405e.pngapi/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 отправляем наш первый запрос и видим, что все заработало.

387dc5ccad744784f503611c3e16d5fb.pngf9d41633fabaec60f29376f8ca43b88b.png

Заключение

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

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

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

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

© Habrahabr.ru